├── .devcontainer ├── Dockerfile ├── README.md ├── devcontainer.json └── py310 │ └── .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 ├── 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 │ ├── chooser.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 │ ├── 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_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 versins. 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-38", 3 | 4 | "build": { 5 | "dockerfile": "./Dockerfile", 6 | "args": { 7 | "NAME": "py_trees", 8 | "POETRY_VERSION": "1.3.2", 9 | "PYTHON_VERSION": "3.8.16", 10 | "DEBIAN_VERSION": "bullseye" 11 | }, 12 | "context": ".." 13 | }, 14 | "extensions": [ 15 | "bierner.github-markdown-preview", 16 | "bungcip.better-toml", 17 | "ms-python.python" 18 | ], 19 | "containerEnv": { 20 | "POETRY_HTTP_BASIC_PYPI_USERNAME": "${localEnv:POETRY_HTTP_BASIC_PYPI_USERNAME}", 21 | "POETRY_HTTP_BASIC_PYPI_PASSWORD": "${localEnv:POETRY_HTTP_BASIC_PYPI_PASSWORD}" 22 | }, 23 | "remoteUser": "zen" 24 | } 25 | -------------------------------------------------------------------------------- /.devcontainer/py310/.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.3.2", 9 | "PYTHON_VERSION": "3.10.9", 10 | "DEBIAN_VERSION": "bullseye" 11 | }, 12 | "context": ".." 13 | }, 14 | "extensions": [ 15 | "bierner.github-markdown-preview", 16 | "bungcip.better-toml", 17 | "ms-python.python" 18 | ], 19 | "containerEnv": { 20 | "POETRY_HTTP_BASIC_PYPI_USERNAME": "${localEnv:POETRY_HTTP_BASIC_PYPI_USERNAME}", 21 | "POETRY_HTTP_BASIC_PYPI_PASSWORD": "${localEnv:POETRY_HTTP_BASIC_PYPI_PASSWORD}" 22 | }, 23 | "remoteUser": "zen", 24 | "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/workspace,type=bind", 25 | "workspaceFolder": "/workspace" 26 | } 27 | -------------------------------------------------------------------------------- /.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-22.04 14 | strategy: 15 | matrix: 16 | python-version: ["3.8", "3.10"] 17 | include: 18 | - python-version: "3.8" 19 | python-py-version: "py38" 20 | - python-version: "3.10" 21 | python-py-version: "py310" 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@v3 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@v3 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.8.16 8 | PYTHON_PRIMARY_TAG: py38 9 | PYTHON_SECONDARY_VERSION: 3.10.9 10 | PYTHON_SECONDARY_TAG: py310 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-22.04 24 | permissions: 25 | contents: read 26 | packages: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Login to GCR 31 | uses: docker/login-action@v2 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@v4 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@v3 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@v3 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-22.04 21 | strategy: 22 | matrix: 23 | python-version: ["3.8", "3.10"] 24 | include: 25 | - python-version: "3.8" 26 | python-py-version: "py38" 27 | - python-version: "3.10" 28 | python-py-version: "py310" 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@v3 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@v3 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 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bierner.github-markdown-preview", 4 | "ms-python.python", 5 | "ms-vscode-remote.vscode-remote-extensionpack" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.flake8Enabled": false 3 | } -------------------------------------------------------------------------------- /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 | ## Pull Requests 6 | 7 | ### Before Submitting 8 | 9 | Some recommendations to help align your contribution and minimise the eventual back and forth on a PR: 10 | 11 | * Engage in an issue thread or in the discussion section. 12 | * Actual code in the form of a minimal PR with a `status:do_not_merge` label is a great tool for generating useful dialogue. 13 | 14 | ### Which Branch? 15 | 16 | * If it's a new feature, or bugfix applicable to the latest code, `devel` 17 | * 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`) 18 | 19 | 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 20 | release branch or mention that you'd like to see it backported in the original PR's description. 21 | 22 | ### Coding 23 | 24 | Running `. ./venv.bash` in the root of the repository can create a nice, self-contained sandbox within which you can develop and test your contribution. 25 | 26 | ### The Pull Request 27 | 28 | Be sure to state clearly in the pull request's **description** (this helps expedite review): 29 | 30 | * The motivation, i.e. what problem is this solving. 31 | * A concise summary of what was done (and why if relevant). 32 | 33 | ### Style 34 | 35 | The repository aims to conform to PEP8, please endeavour to do so. There is a simple script `./lint/flake8` that can be used to check that the 36 | code conforms. This is run as part of the CI checks. 37 | 38 | ### Tests 39 | 40 | See [tests/README.md](tests/README.md) for instructions on how to run the tests all together or manually. If you are contributing new features, or 41 | bugfixing a problem that did not have coverage, it is expected that the pull requests will include a test for regression purposes. 42 | 43 | ### Documentation 44 | 45 | Documentation is auto-generated as part of the PR process, but if you do wish to make changes and check locally: 46 | 47 | ``` 48 | . ./venv.bash 49 | make docs 50 | ... 51 | The HTML pages are in doc/html. 52 | ``` 53 | 54 | ### Changelog 55 | 56 | * Please update the `Forthcoming` section in the [Changelog](Changelog.rst) with your change and a link to your PR. 57 | * The style should be self-explanatory. 58 | * Do not worry about incrementing the version, releases are handled separately. 59 | 60 | ### Review 61 | 62 | Once submitted, a reviewer will be assigned. You do not need to select. If no-one has self-assigned in a reasonable time window, 63 | feel free to append a *friendly bump* comment to your PR. 64 | 65 | ### Merging 66 | 67 | Once the large button has gone `GREEN`, you or the reviewer may merge the pull request. 68 | 69 | ## Releasing 70 | 71 | If you are interested in seeing your changes make it into a release (sooner rather than later) and distributed on 72 | PyPi, PPA or via the ROS ecosystem, please make the request via a comment in your PR or in an issue. 73 | 74 | ## Social Conduct 75 | 76 | Be prepared to be tickled by noodly appendages and at all times, be froody. 77 | -------------------------------------------------------------------------------- /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 doc doc/html 11 | 12 | clean: 13 | -rm -rf doc/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(os.path.join(os.path.abspath(__file__), os.pardir, os.pardir)) 17 | version_file = os.path.join(project_dir, 'py_trees', 'version.py') 18 | with open(version_file) as f: 19 | exec(f.read()) # makes __version__ available 20 | 21 | ################################################################################ 22 | # Autodoc - help it find the project 23 | # https://sphinx-rtd-tutorial.readthedocs.io/en/latest/sphinx-config.html#autodoc-configuration 24 | ################################################################################ 25 | 26 | sys.path.insert(0, project_dir) 27 | 28 | ################################################################################ 29 | # Project Info 30 | ################################################################################ 31 | 32 | project = 'py_trees' 33 | copyright = '2023, Daniel Stonier' 34 | author = 'Daniel Stonier' 35 | 36 | version = __version__ 37 | release = version 38 | 39 | ################################################################################ 40 | # General configuration 41 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 42 | ################################################################################ 43 | 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.coverage', 47 | 'sphinx.ext.doctest', 48 | 'sphinx.ext.ifconfig', 49 | 'sphinx.ext.intersphinx', 50 | 'sphinx.ext.mathjax', 51 | 'sphinx.ext.napoleon', 52 | 'sphinx.ext.todo', 53 | 'sphinx.ext.viewcode', 54 | 'sphinx.ext.graphviz', 55 | 'sphinxarg.ext', 56 | ] 57 | 58 | templates_path = ['_templates'] 59 | exclude_patterns = ['_build', 'weblinks.rst'] 60 | 61 | language = 'en' 62 | 63 | ################################################################################ 64 | # Extensions Configuration 65 | ################################################################################ 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 | # HTML 75 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 76 | ################################################################################ 77 | 78 | html_theme = 'sphinx_rtd_theme' 79 | 80 | # Add any paths that contain custom static files (such as style sheets) here, 81 | # relative to this directory. They are copied after the builtin static files, 82 | # so a file named "default.css" will overwrite the builtin "default.css". 83 | html_static_path = ['_static'] 84 | 85 | # If true, “(C) Copyright …” is shown in the HTML footer. Default is True. 86 | html_show_copyright = False 87 | 88 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 89 | html_show_sphinx = False 90 | 91 | ################################################################################ 92 | # Intersphinx 93 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration 94 | ################################################################################ 95 | 96 | # Refer to the Python standard library. 97 | intersphinx_mapping = { 98 | 'python': ('https://docs.python.org/3', None), 99 | } 100 | -------------------------------------------------------------------------------- /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 | 3 | import py_trees 4 | 5 | py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100) 6 | reader = py_trees.blackboard.Client(name="Reader") 7 | reader.register_key(key="foo", access=py_trees.common.Access.READ) 8 | writer = py_trees.blackboard.Client(name="Writer") 9 | writer.register_key(key="foo", access=py_trees.common.Access.WRITE) 10 | writer.foo = "bar" 11 | writer.foo = "foobar" 12 | unused_result = reader.foo 13 | print(py_trees.display.unicode_blackboard_activity_stream()) 14 | py_trees.blackboard.Blackboard.activity_stream.clear() 15 | -------------------------------------------------------------------------------- /docs/examples/blackboard_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | 6 | class Foo(py_trees.behaviour.Behaviour): 7 | 8 | def __init__(self, name): 9 | super().__init__(name=name) 10 | self.blackboard = self.attach_blackboard_client(name="Foo Global") 11 | self.parameters = self.attach_blackboard_client(name="Foo Params", namespace="foo_parameters_") 12 | self.state = self.attach_blackboard_client(name="Foo State", namespace="foo_state_") 13 | 14 | # create a key 'foo_parameters_init' on the blackboard 15 | self.parameters.register_key("init", access=py_trees.common.Access.READ) 16 | # create a key 'foo_state_number_of_noodles' on the blackboard 17 | self.state.register_key("number_of_noodles", access=py_trees.common.Access.WRITE) 18 | 19 | def initialise(self): 20 | self.state.number_of_noodles = self.parameters.init 21 | 22 | def update(self): 23 | self.state.number_of_noodles += 1 24 | self.feedback_message = self.state.number_of_noodles 25 | if self.state.number_of_noodles > 5: 26 | return py_trees.common.Status.SUCCESS 27 | else: 28 | return py_trees.common.Status.RUNNING 29 | 30 | 31 | # could equivalently do directly via the Blackboard static methods if 32 | # not interested in tracking / visualising the application configuration 33 | configuration = py_trees.blackboard.Client(name="App Config") 34 | configuration.register_key("foo_parameters_init", access=py_trees.common.Access.WRITE) 35 | configuration.foo_parameters_init = 3 36 | 37 | foo = Foo(name="The Foo") 38 | for i in range(1, 8): 39 | foo.tick_once() 40 | print("Number of Noodles: {}".format(foo.feedback_message)) 41 | -------------------------------------------------------------------------------- /docs/examples/blackboard_disconnected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | 6 | def check_foo(): 7 | blackboard = py_trees.blackboard.Client(name="Reader") 8 | blackboard.register_key(key="foo", access=py_trees.common.Access.READ) 9 | print("Foo: {}".format(blackboard.foo)) 10 | 11 | 12 | blackboard = py_trees.blackboard.Client(name="Writer") 13 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 14 | blackboard.foo = "bar" 15 | check_foo() 16 | -------------------------------------------------------------------------------- /docs/examples/blackboard_display.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | writer = py_trees.blackboard.Client(name="Writer") 6 | for key in {"foo", "bar", "dude", "dudette"}: 7 | writer.register_key(key=key, access=py_trees.common.Access.WRITE) 8 | 9 | reader = py_trees.blackboard.Client(name="Reader") 10 | for key in {"foo", "bar"}: 11 | reader.register_key(key=key, access=py_trees.common.Access.READ) 12 | 13 | writer.foo = "foo" 14 | writer.bar = "bar" 15 | writer.dude = "bob" 16 | 17 | # all key-value pairs 18 | print(py_trees.display.unicode_blackboard()) 19 | # various filtered views 20 | print(py_trees.display.unicode_blackboard(key_filter={"foo"})) 21 | print(py_trees.display.unicode_blackboard(regex_filter="dud*")) 22 | print(py_trees.display.unicode_blackboard(client_filter={reader.unique_identifier})) 23 | # list the clients associated with each key 24 | print(py_trees.display.unicode_blackboard(display_only_key_metadata=True)) 25 | -------------------------------------------------------------------------------- /docs/examples/blackboard_namespaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | blackboard = py_trees.blackboard.Client(name="Global") 6 | parameters = py_trees.blackboard.Client(name="Parameters", namespace="parameters") 7 | 8 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 9 | blackboard.register_key(key="/bar", access=py_trees.common.Access.WRITE) 10 | blackboard.register_key(key="/parameters/default_speed", access=py_trees.common.Access.WRITE) 11 | parameters.register_key(key="aggressive_speed", access=py_trees.common.Access.WRITE) 12 | 13 | blackboard.foo = "foo" 14 | blackboard.bar = "bar" 15 | blackboard.parameters.default_speed = 20.0 16 | parameters.aggressive_speed = 60.0 17 | 18 | miss_daisy = blackboard.parameters.default_speed 19 | van_diesel = parameters.aggressive_speed 20 | 21 | print(blackboard) 22 | print(parameters) 23 | -------------------------------------------------------------------------------- /docs/examples/blackboard_nested.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | 6 | class Nested(object): 7 | def __init__(self): 8 | self.foo = None 9 | self.bar = None 10 | 11 | def __str__(self): 12 | return str(self.__dict__) 13 | 14 | 15 | writer = py_trees.blackboard.Client(name="Writer") 16 | writer.register_key(key="nested", access=py_trees.common.Access.WRITE) 17 | reader = py_trees.blackboard.Client(name="Reader") 18 | reader.register_key(key="nested", access=py_trees.common.Access.READ) 19 | 20 | writer.nested = Nested() 21 | writer.nested.foo = "I am foo" 22 | writer.nested.bar = "I am bar" 23 | 24 | foo = reader.nested.foo 25 | print(writer) 26 | print(reader) 27 | -------------------------------------------------------------------------------- /docs/examples/blackboard_read_write.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import py_trees 4 | 5 | 6 | blackboard = py_trees.blackboard.Client(name="Client") 7 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 8 | blackboard.register_key(key="bar", access=py_trees.common.Access.READ) 9 | blackboard.foo = "foo" 10 | print(blackboard) 11 | -------------------------------------------------------------------------------- /docs/examples/chooser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/examples/chooser.py -------------------------------------------------------------------------------- /docs/examples/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees.decorators 5 | import py_trees.display 6 | 7 | if __name__ == '__main__': 8 | 9 | root = py_trees.composites.Sequence(name="Life") 10 | timeout = py_trees.decorators.Timeout( 11 | name="Timeout", 12 | child=py_trees.behaviours.Success(name="Have a Beer!") 13 | ) 14 | failure_is_success = py_trees.decorators.Inverter( 15 | name="Inverter", 16 | child=py_trees.behaviours.Success(name="Busy?") 17 | ) 18 | root.add_children([failure_is_success, timeout]) 19 | py_trees.display.render_dot_tree(root) 20 | -------------------------------------------------------------------------------- /docs/examples/oneshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | sequence = py_trees.composites.Sequence("Sequence") 8 | guard = py_trees.behaviours.Success(name="Guard") 9 | a1 = py_trees.behaviours.Success(name="Action 1") 10 | a2 = py_trees.behaviours.Success(name="Action 2") 11 | a3 = py_trees.behaviours.Success(name="Action 3") 12 | sequence.add_children([guard, a1, a2, a3]) 13 | root = py_trees.idioms.oneshot( 14 | name="OneShot", 15 | variable_name="oneshot", 16 | behaviour=sequence, 17 | policy=py_trees.common.OneShotPolicy.ON_COMPLETION) 18 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 19 | -------------------------------------------------------------------------------- /docs/examples/parallel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | b1 = py_trees.behaviours.Success(name="B1") 8 | b2 = py_trees.behaviours.Success(name="B2") 9 | b3 = py_trees.behaviours.Success(name="B3") 10 | root = py_trees.composites.Parallel( 11 | policy=py_trees.common.ParallelPolicy.SuccessOnSelected( 12 | synchronise=True, 13 | children=[b1, b2] 14 | ) 15 | ) 16 | root.add_children([b1, b2, b3]) 17 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 18 | -------------------------------------------------------------------------------- /docs/examples/pickup_where_you_left_off.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | task_one = py_trees.behaviours.StatusQueue( 8 | name="Task 1", 9 | queue=[ 10 | py_trees.common.Status.RUNNING, 11 | py_trees.common.Status.RUNNING, 12 | ], 13 | eventually=py_trees.common.Status.SUCCESS 14 | ) 15 | task_two = py_trees.behaviours.StatusQueue( 16 | name="Task 2", 17 | queue=[ 18 | py_trees.common.Status.RUNNING, 19 | py_trees.common.Status.RUNNING, 20 | ], 21 | eventually=py_trees.common.Status.SUCCESS 22 | ) 23 | high_priority_interrupt = py_trees.decorators.RunningIsFailure( 24 | child=py_trees.behaviours.Periodic( 25 | name="High Priority", 26 | n=3 27 | ) 28 | ) 29 | piwylo = py_trees.idioms.pick_up_where_you_left_off( 30 | name="Tasks", 31 | tasks=[task_one, task_two] 32 | ) 33 | root = py_trees.composites.Selector(name="Pick Up\nWhere You\nLeft Off") 34 | root.add_children([high_priority_interrupt, piwylo]) 35 | py_trees.display.render_dot_tree( 36 | root, 37 | py_trees.common.string_to_visibility_level("all")) 38 | -------------------------------------------------------------------------------- /docs/examples/selector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | root = py_trees.composites.Selector("Selector") 8 | high = py_trees.behaviours.Success(name="High Priority") 9 | med = py_trees.behaviours.Success(name="Med Priority") 10 | low = py_trees.behaviours.Success(name="Low Priority") 11 | root.add_children([high, med, low]) 12 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 13 | -------------------------------------------------------------------------------- /docs/examples/selector_with_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | root = py_trees.composites.Selector("Selector With Memory", memory=True) 8 | high = py_trees.behaviours.Success(name="High Priority") 9 | med = py_trees.behaviours.Success(name="Med Priority") 10 | low = py_trees.behaviours.Success(name="Low Priority") 11 | root.add_children([high, med, low]) 12 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 13 | -------------------------------------------------------------------------------- /docs/examples/sequence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | root = py_trees.composites.Sequence("Sequence") 8 | guard = py_trees.behaviours.Success(name="Guard") 9 | a1 = py_trees.behaviours.Success(name="Action 1") 10 | a2 = py_trees.behaviours.Success(name="Action 2") 11 | a3 = py_trees.behaviours.Success(name="Action 3") 12 | root.add_children([guard, a1, a2, a3]) 13 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 14 | -------------------------------------------------------------------------------- /docs/examples/sequence_with_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | root = py_trees.composites.Sequence(name="Sequence with Memory", memory=True) 8 | guard = py_trees.behaviours.Success(name="Guard") 9 | a1 = py_trees.behaviours.Success(name="Action 1") 10 | a2 = py_trees.behaviours.Success(name="Action 2") 11 | a3 = py_trees.behaviours.Success(name="Action 3") 12 | root.add_children([guard, a1, a2, a3]) 13 | py_trees.display.render_dot_tree(root, py_trees.common.string_to_visibility_level("all")) 14 | -------------------------------------------------------------------------------- /docs/examples/skeleton_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | import random 6 | 7 | 8 | class Foo(py_trees.behaviour.Behaviour): 9 | def __init__(self, name): 10 | """ 11 | Minimal one-time initialisation. A good rule of thumb is 12 | to only include the initialisation relevant for being able 13 | to insert this behaviour in a tree for offline rendering to 14 | dot graphs. 15 | 16 | Other one-time initialisation requirements should be met via 17 | the setup() method. 18 | """ 19 | super(Foo, self).__init__(name) 20 | 21 | def setup(self): 22 | """ 23 | When is this called? 24 | This function should be either manually called by your program 25 | to setup this behaviour alone, or more commonly, via 26 | :meth:`~py_trees.behaviour.Behaviour.setup_with_descendants` 27 | or :meth:`~py_trees.trees.BehaviourTree.setup`, both of which 28 | will iterate over this behaviour, it's children (it's children's 29 | children ...) calling :meth:`~py_trees.behaviour.Behaviour.setup` 30 | on each in turn. 31 | 32 | If you have vital initialisation necessary to the success 33 | execution of your behaviour, put a guard in your 34 | :meth:`~py_trees.behaviour.Behaviour.initialise` method 35 | to protect against entry without having been setup. 36 | 37 | What to do here? 38 | Delayed one-time initialisation that would otherwise interfere 39 | with offline rendering of this behaviour in a tree to dot graph 40 | or validation of the behaviour's configuration. 41 | 42 | Good examples include: 43 | 44 | - Hardware or driver initialisation 45 | - Middleware initialisation (e.g. ROS pubs/subs/services) 46 | - A parallel checking for a valid policy configuration after 47 | children have been added or removed 48 | """ 49 | self.logger.debug(" %s [Foo::setup()]" % self.name) 50 | 51 | def initialise(self): 52 | """ 53 | When is this called? 54 | The first time your behaviour is ticked and anytime the 55 | status is not RUNNING thereafter. 56 | 57 | What to do here? 58 | Any initialisation you need before putting your behaviour 59 | to work. 60 | """ 61 | self.logger.debug(" %s [Foo::initialise()]" % self.name) 62 | 63 | def update(self): 64 | """ 65 | When is this called? 66 | Every time your behaviour is ticked. 67 | 68 | What to do here? 69 | - Triggering, checking, monitoring. Anything...but do not block! 70 | - Set a feedback message 71 | - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] 72 | """ 73 | self.logger.debug(" %s [Foo::update()]" % self.name) 74 | ready_to_make_a_decision = random.choice([True, False]) 75 | decision = random.choice([True, False]) 76 | if not ready_to_make_a_decision: 77 | return py_trees.common.Status.RUNNING 78 | elif decision: 79 | self.feedback_message = "We are not bar!" 80 | return py_trees.common.Status.SUCCESS 81 | else: 82 | self.feedback_message = "Uh oh" 83 | return py_trees.common.Status.FAILURE 84 | 85 | def terminate(self, new_status): 86 | """ 87 | When is this called? 88 | Whenever your behaviour switches to a non-running state. 89 | - SUCCESS || FAILURE : your behaviour's work cycle has finished 90 | - INVALID : a higher priority branch has interrupted, or shutting down 91 | """ 92 | self.logger.debug(" %s [Foo::terminate().terminate()][%s->%s]" % (self.name, self.status, new_status)) 93 | -------------------------------------------------------------------------------- /docs/examples/skeleton_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import py_trees 5 | 6 | if __name__ == '__main__': 7 | 8 | root = py_trees.composites.Selector("Selector") 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( 15 | root=root 16 | ) 17 | print(py_trees.display.unicode_tree(root=root)) 18 | behaviour_tree.setup(timeout=15) 19 | 20 | def print_tree(tree): 21 | print(py_trees.display.unicode_tree(root=tree.root, show_status=True)) 22 | 23 | try: 24 | behaviour_tree.tick_tock( 25 | period_ms=500, 26 | number_of_iterations=py_trees.trees.CONTINUOUS_TICK_TOCK, 27 | pre_tick_handler=None, 28 | post_tick_handler=print_tree 29 | ) 30 | except KeyboardInterrupt: 31 | behaviour_tree.interrupt() 32 | -------------------------------------------------------------------------------- /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/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/action.gif -------------------------------------------------------------------------------- /docs/images/ascii_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/ascii_tree.png -------------------------------------------------------------------------------- /docs/images/ascii_tree_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/ascii_tree_simple.png -------------------------------------------------------------------------------- /docs/images/blackboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard.jpg -------------------------------------------------------------------------------- /docs/images/blackboard_activity_stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_activity_stream.png -------------------------------------------------------------------------------- /docs/images/blackboard_client_instantiation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_client_instantiation.png -------------------------------------------------------------------------------- /docs/images/blackboard_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_demo.png -------------------------------------------------------------------------------- /docs/images/blackboard_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_display.png -------------------------------------------------------------------------------- /docs/images/blackboard_namespaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_namespaces.png -------------------------------------------------------------------------------- /docs/images/blackboard_nested.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_nested.png -------------------------------------------------------------------------------- /docs/images/blackboard_read_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_read_write.png -------------------------------------------------------------------------------- /docs/images/blackboard_remappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_remappings.png -------------------------------------------------------------------------------- /docs/images/blackboard_trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/blackboard_trees.png -------------------------------------------------------------------------------- /docs/images/context_switching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/context_switching.gif -------------------------------------------------------------------------------- /docs/images/crazy_hospital.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/crazy_hospital.jpg -------------------------------------------------------------------------------- /docs/images/display_modes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/display_modes.png -------------------------------------------------------------------------------- /docs/images/either_or.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/either_or.gif -------------------------------------------------------------------------------- /docs/images/eternal_guard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/eternal_guard.gif -------------------------------------------------------------------------------- /docs/images/lifecycle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/lifecycle.gif -------------------------------------------------------------------------------- /docs/images/logging.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/logging.gif -------------------------------------------------------------------------------- /docs/images/many-hats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/many-hats.png -------------------------------------------------------------------------------- /docs/images/pick_up_where_you_left_off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/pick_up_where_you_left_off.gif -------------------------------------------------------------------------------- /docs/images/render.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/render.gif -------------------------------------------------------------------------------- /docs/images/selector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/selector.gif -------------------------------------------------------------------------------- /docs/images/sequence.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/sequence.gif -------------------------------------------------------------------------------- /docs/images/tree_stewardship.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/docs/images/tree_stewardship.gif -------------------------------------------------------------------------------- /docs/images/yggdrasil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/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/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 | Add an image of a ticking tree here. 69 | 70 | .. _pastafarianism: http://www.venganza.org/ 71 | -------------------------------------------------------------------------------- /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.2.0 7 | 8 | Pythonic implementation of behaviour trees. 9 | 10 | Daniel Stonier 11 | Michal Staniaszek 12 | Naveed Usmani 13 | 14 | Daniel Stonier 15 | 16 | BSD 17 | 18 | https://py-trees.readthedocs.io/en/devel/ 19 | https://github.com/splintered-reality/py_trees 20 | https://github.com/splintered-reality/py_trees/issues 21 | 22 | python3-setuptools 23 | 24 | python3-pydot 25 | 26 | 27 | ament_python 28 | 29 | 30 | -------------------------------------------------------------------------------- /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 | """ 10 | This is the top-level namespace of the py_trees package. 11 | """ 12 | ############################################################################## 13 | # Imports 14 | ############################################################################## 15 | 16 | from . import behaviour # noqa 17 | from . import behaviours # noqa 18 | from . import blackboard # noqa 19 | from . import common # noqa 20 | from . import composites # noqa 21 | from . import console # noqa 22 | from . import decorators # noqa 23 | from . import display # noqa 24 | from . import idioms # noqa 25 | from . import logging # noqa 26 | from . import meta # noqa 27 | from . import syntax_highlighting # noqa 28 | from . import tests # noqa 29 | from . import timers # noqa 30 | from . import trees # noqa 31 | from . import utilities # noqa 32 | from . import version # noqa 33 | from . import visitors # noqa 34 | 35 | from . import demos # noqa 36 | from . import programs # noqa 37 | -------------------------------------------------------------------------------- /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 | from . import action # noqa 16 | from . import blackboard # noqa 17 | from . import blackboard_namespaces # noqa 18 | from . import blackboard_remappings # noqa 19 | from . import context_switching # noqa 20 | from . import display_modes # noqa 21 | from . import dot_graphs # noqa 22 | from . import either_or # noqa 23 | from . import lifecycle # noqa 24 | from . import selector # noqa 25 | from . import sequence # noqa 26 | from . import stewardship # noqa 27 | -------------------------------------------------------------------------------- /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 | Demonstrates usage of blackboard namespaces. 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 | content = "Demonstrates usage of blackboard namespaces.\n" 41 | content += "\n" 42 | 43 | if py_trees.console.has_colours: 44 | banner_line = console.green + "*" * 79 + "\n" + console.reset 45 | s = banner_line 46 | s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset 47 | s += banner_line 48 | s += "\n" 49 | s += content 50 | s += "\n" 51 | s += banner_line 52 | else: 53 | s = content 54 | return s 55 | 56 | 57 | def epilog() -> typing.Optional[str]: 58 | if py_trees.console.has_colours: 59 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 60 | else: 61 | return None 62 | 63 | 64 | def command_line_argument_parser() -> argparse.ArgumentParser: 65 | parser = argparse.ArgumentParser(description=description(), 66 | epilog=epilog(), 67 | formatter_class=argparse.RawDescriptionHelpFormatter, 68 | ) 69 | return parser 70 | 71 | 72 | ############################################################################## 73 | # Main 74 | ############################################################################## 75 | 76 | 77 | def main() -> None: 78 | """Entry point for the demo script.""" 79 | _ = command_line_argument_parser().parse_args() # configuration only, no args to process 80 | print(description()) 81 | print("-------------------------------------------------------------------------------") 82 | print("$ py_trees.blackboard.Client(name='Blackboard')") 83 | print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)") 84 | print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)") 85 | print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)") 86 | print("-------------------------------------------------------------------------------") 87 | blackboard = py_trees.blackboard.Client(name="Blackboard") 88 | blackboard.register_key(key="dude", access=py_trees.common.Access.WRITE) 89 | blackboard.register_key(key="/dudette", access=py_trees.common.Access.WRITE) 90 | blackboard.register_key(key="/foo/bar/wow", access=py_trees.common.Access.WRITE) 91 | print(blackboard) 92 | print("-------------------------------------------------------------------------------") 93 | print("$ blackboard.dude = 'Bob'") 94 | print("$ blackboard.dudette = 'Jade'") 95 | print("-------------------------------------------------------------------------------") 96 | blackboard.dude = "Bob" 97 | blackboard.dudette = "Jade" 98 | print(py_trees.display.unicode_blackboard()) 99 | print("-------------------------------------------------------------------------------") 100 | print("$ blackboard.foo.bar.wow = 'foobar'") 101 | print("-------------------------------------------------------------------------------") 102 | blackboard.foo.bar.wow = "foobar" 103 | print(py_trees.display.unicode_blackboard()) 104 | print("-------------------------------------------------------------------------------") 105 | print("$ py_trees.blackboard.Client(name='Foo', namespace='foo')") 106 | print("$ foo.register_key(key='awesome', access=py_trees.common.Access.WRITE)") 107 | print("$ foo.register_key(key='/brilliant', access=py_trees.common.Access.WRITE)") 108 | print("$ foo.register_key(key='/foo/clever', access=py_trees.common.Access.WRITE)") 109 | print("-------------------------------------------------------------------------------") 110 | foo = py_trees.blackboard.Client(name="Foo", namespace="foo") 111 | foo.register_key(key="awesome", access=py_trees.common.Access.WRITE) 112 | # TODO: should /brilliant be namespaced or go directly to root? 113 | foo.register_key(key="/brilliant", access=py_trees.common.Access.WRITE) 114 | # absolute names are ok, so long as they include the namespace 115 | foo.register_key(key="/foo/clever", access=py_trees.common.Access.WRITE) 116 | print(foo) 117 | print("-------------------------------------------------------------------------------") 118 | print("$ foo.awesome = True") 119 | print("$ foo.set('/brilliant', False)") 120 | print("$ foo.clever = True") 121 | print("-------------------------------------------------------------------------------") 122 | foo.awesome = True 123 | # Only accessable via set since it's not in the namespace 124 | foo.set("/brilliant", False) 125 | # This will fail since it looks for the namespaced /foo/brilliant key 126 | # foo.brilliant = False 127 | foo.clever = True 128 | print(py_trees.display.unicode_blackboard()) 129 | -------------------------------------------------------------------------------- /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 | Demonstrates usage of blackboard remappings. 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 | content = "Demonstrates usage of blackbord remappings.\n" 41 | content += "\n" 42 | content += "Demonstration is via an exemplar behaviour making use of remappings..\n" 43 | 44 | if py_trees.console.has_colours: 45 | banner_line = console.green + "*" * 79 + "\n" + console.reset 46 | s = banner_line 47 | s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset 48 | s += banner_line 49 | s += "\n" 50 | s += content 51 | s += "\n" 52 | s += banner_line 53 | else: 54 | s = content 55 | return s 56 | 57 | 58 | def epilog() -> typing.Optional[str]: 59 | if py_trees.console.has_colours: 60 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 61 | else: 62 | return None 63 | 64 | 65 | def command_line_argument_parser() -> argparse.ArgumentParser: 66 | parser = argparse.ArgumentParser(description=description(), 67 | epilog=epilog(), 68 | formatter_class=argparse.RawDescriptionHelpFormatter, 69 | ) 70 | return parser 71 | 72 | 73 | class Remap(py_trees.behaviour.Behaviour): 74 | """Custom writer that submits a more complicated variable to the blackboard.""" 75 | 76 | def __init__(self, name: str, remap_to: typing.Dict[str, str]): 77 | super().__init__(name=name) 78 | self.logger.debug("%s.__init__()" % (self.__class__.__name__)) 79 | self.blackboard = self.attach_blackboard_client() 80 | self.blackboard.register_key( 81 | key="/foo/bar/wow", 82 | access=py_trees.common.Access.WRITE, 83 | remap_to=remap_to["/foo/bar/wow"] 84 | ) 85 | 86 | def update(self) -> py_trees.common.Status: 87 | """Write a dictionary to the blackboard. 88 | 89 | This beaviour always returns :data:`~py_trees.common.Status.SUCCESS`. 90 | """ 91 | self.logger.debug("%s.update()" % (self.__class__.__name__)) 92 | self.blackboard.foo.bar.wow = "colander" 93 | 94 | return py_trees.common.Status.SUCCESS 95 | 96 | ############################################################################## 97 | # Main 98 | ############################################################################## 99 | 100 | 101 | def main() -> None: 102 | """Entry point for the demo script.""" 103 | _ = command_line_argument_parser().parse_args() # configuration only, no arg processing 104 | print(description()) 105 | py_trees.logging.level = py_trees.logging.Level.DEBUG 106 | py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100) 107 | root = Remap(name="Remap", remap_to={"/foo/bar/wow": "/parameters/wow"}) 108 | 109 | #################### 110 | # Execute 111 | #################### 112 | root.tick_once() 113 | print(root.blackboard) 114 | print(py_trees.display.unicode_blackboard()) 115 | print(py_trees.display.unicode_blackboard_activity_stream()) 116 | -------------------------------------------------------------------------------- /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 | Demonstrates usage of the ascii/unicode display modes. 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 | content = "Demonstrates usage of the ascii/unicode display modes.\n" 42 | content += "\n" 43 | content += "...\n" 44 | content += "...\n" 45 | 46 | if py_trees.console.has_colours: 47 | banner_line = console.green + "*" * 79 + "\n" + console.reset 48 | s = banner_line 49 | s += console.bold_white + "Display Modes".center(79) + "\n" + console.reset 50 | s += banner_line 51 | s += "\n" 52 | s += content 53 | s += "\n" 54 | s += banner_line 55 | else: 56 | s = content 57 | return s 58 | 59 | 60 | def epilog() -> typing.Optional[str]: 61 | if py_trees.console.has_colours: 62 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 63 | else: 64 | return None 65 | 66 | 67 | def command_line_argument_parser() -> argparse.ArgumentParser: 68 | parser = argparse.ArgumentParser(description=description(), 69 | epilog=epilog(), 70 | formatter_class=argparse.RawDescriptionHelpFormatter, 71 | ) 72 | return parser 73 | 74 | 75 | def create_root() -> py_trees.behaviour.Behaviour: 76 | """ 77 | Create the tree to be ticked/displayed. 78 | 79 | Returns: 80 | the root of the tree 81 | """ 82 | root = py_trees.composites.Sequence(name="root", memory=True) 83 | child = py_trees.composites.Sequence(name="child1", memory=True) 84 | child2 = py_trees.composites.Sequence(name="child2", memory=True) 85 | child3 = py_trees.composites.Sequence(name="child3", memory=True) 86 | root.add_child(child) 87 | root.add_child(child2) 88 | root.add_child(child3) 89 | queue = [py_trees.common.Status.RUNNING] 90 | eventually = py_trees.common.Status.SUCCESS 91 | child.add_child(py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually)) 92 | child2.add_child(py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually)) 93 | child2_child1 = py_trees.composites.Sequence(name="Child2_child1", memory=True) 94 | child2_child1.add_child(py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually)) 95 | child2.add_child(child2_child1) 96 | child3.add_child(py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually)) 97 | return root 98 | 99 | 100 | ############################################################################## 101 | # Main 102 | ############################################################################## 103 | 104 | 105 | def main() -> None: 106 | """Entry point for the demo script.""" 107 | _ = command_line_argument_parser().parse_args() # configuration only, no args to process 108 | print(description()) 109 | print("-------------------------------------------------------------------------------") 110 | print("$ py_trees.blackboard.Client(name='Blackboard')") 111 | print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)") 112 | print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)") 113 | print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)") 114 | print("-------------------------------------------------------------------------------") 115 | 116 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 117 | tree = py_trees.trees.BehaviourTree(create_root()) 118 | tree.add_visitor(snapshot_visitor) 119 | 120 | for tick in range(2): 121 | tree.tick() 122 | for show_visited, show_status in itertools.product([False, True], [False, True]): 123 | console.banner("Tick {} / show_only_visited=={} / show_status=={}".format(tick, show_visited, show_status)) 124 | print( 125 | py_trees.display.unicode_tree( 126 | tree.root, 127 | show_status=show_status, 128 | show_only_visited=show_visited, 129 | visited=snapshot_visitor.visited, 130 | previously_visited=snapshot_visitor.previously_visited 131 | ) 132 | ) 133 | print() 134 | -------------------------------------------------------------------------------- /py_trees/demos/dot_graphs.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 | Renders a dot graph for a simple tree, with blackboxes. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.dot_graphs 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-dot-graphs 17 | 18 | .. graphviz:: dot/demo-dot-graphs.dot 19 | 20 | """ 21 | 22 | ############################################################################## 23 | # Imports 24 | ############################################################################## 25 | 26 | import argparse 27 | import subprocess 28 | import typing 29 | 30 | import py_trees 31 | import py_trees.console as console 32 | 33 | ############################################################################## 34 | # Classes 35 | ############################################################################## 36 | 37 | 38 | def description() -> str: 39 | name = "py-trees-demo-dot-graphs" 40 | content = "Renders a dot graph for a simple tree, with blackboxes.\n" 41 | if py_trees.console.has_colours: 42 | banner_line = console.green + "*" * 79 + "\n" + console.reset 43 | s = banner_line 44 | s += console.bold_white + "Dot Graphs".center(79) + "\n" + console.reset 45 | s += banner_line 46 | s += "\n" 47 | s += content 48 | s += "\n" 49 | s += console.white 50 | s += console.bold + " Generate Full Dot Graph" + console.reset + "\n" 51 | s += "\n" 52 | s += console.cyan + " {0}".format(name) + console.reset + "\n" 53 | s += "\n" 54 | s += console.bold + " With Varying Visibility Levels" + console.reset + "\n" 55 | s += "\n" 56 | s += console.cyan + " {0}".format(name) + console.yellow + " --level=all" + console.reset + "\n" 57 | s += console.cyan + " {0}".format(name) + console.yellow + " --level=detail" + console.reset + "\n" 58 | s += console.cyan + " {0}".format(name) + console.yellow + " --level=component" + console.reset + "\n" 59 | s += console.cyan + " {0}".format(name) + console.yellow + " --level=big_picture" + console.reset + "\n" 60 | s += "\n" 61 | s += banner_line 62 | else: 63 | s = content 64 | return s 65 | 66 | 67 | def epilog() -> typing.Optional[str]: 68 | if py_trees.console.has_colours: 69 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 70 | else: 71 | return None 72 | 73 | 74 | def command_line_argument_parser() -> argparse.ArgumentParser: 75 | parser = argparse.ArgumentParser(description=description(), 76 | epilog=epilog(), 77 | formatter_class=argparse.RawDescriptionHelpFormatter, 78 | ) 79 | parser.add_argument('-l', '--level', action='store', 80 | default='fine_detail', 81 | choices=['all', 'fine_detail', 'detail', 'component', 'big_picture'], 82 | help='visibility level') 83 | return parser 84 | 85 | 86 | def create_tree(level: str) -> py_trees.behaviour.Behaviour: 87 | root = py_trees.composites.Selector(name="Demo Dot Graphs %s" % level, memory=False) 88 | first_blackbox = py_trees.composites.Sequence(name="BlackBox 1", memory=True) 89 | first_blackbox.add_child(py_trees.behaviours.Running("Worker")) 90 | first_blackbox.add_child(py_trees.behaviours.Running("Worker")) 91 | first_blackbox.add_child(py_trees.behaviours.Running("Worker")) 92 | first_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.BIG_PICTURE 93 | second_blackbox = py_trees.composites.Sequence(name="Blackbox 2", memory=True) 94 | second_blackbox.add_child(py_trees.behaviours.Running("Worker")) 95 | second_blackbox.add_child(py_trees.behaviours.Running("Worker")) 96 | second_blackbox.add_child(py_trees.behaviours.Running("Worker")) 97 | second_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.COMPONENT 98 | third_blackbox = py_trees.composites.Sequence(name="Blackbox 3", memory=True) 99 | third_blackbox.add_child(py_trees.behaviours.Running("Worker")) 100 | third_blackbox.add_child(py_trees.behaviours.Running("Worker")) 101 | third_blackbox.add_child(py_trees.behaviours.Running("Worker")) 102 | third_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL 103 | root.add_child(first_blackbox) 104 | root.add_child(second_blackbox) 105 | first_blackbox.add_child(third_blackbox) 106 | return root 107 | 108 | 109 | ############################################################################## 110 | # Main 111 | ############################################################################## 112 | 113 | def main() -> None: 114 | """Entry point for the demo script.""" 115 | args = command_line_argument_parser().parse_args() 116 | args.enum_level = py_trees.common.string_to_visibility_level(args.level) 117 | print(description()) 118 | py_trees.logging.level = py_trees.logging.Level.DEBUG 119 | 120 | root = create_tree(args.level) 121 | py_trees.display.render_dot_tree(root, args.enum_level) 122 | 123 | if py_trees.utilities.which("xdot"): 124 | try: 125 | subprocess.call(["xdot", "demo_dot_graphs_%s.dot" % args.level]) 126 | except KeyboardInterrupt: 127 | pass 128 | else: 129 | print("") 130 | console.logerror("No xdot viewer found, skipping display [hint: sudo apt install xdot]") 131 | print("") 132 | -------------------------------------------------------------------------------- /py_trees/demos/eternal_guard.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 demonstration of the 'eternal_guard' concept. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.eternal_guard 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-eternal-guard 17 | 18 | .. graphviz:: dot/demo-eternal-guard.dot 19 | 20 | .. image:: images/eternal_guard.gif 21 | """ 22 | 23 | ############################################################################## 24 | # Imports 25 | ############################################################################## 26 | 27 | import argparse 28 | import functools 29 | import sys 30 | import time 31 | import typing 32 | 33 | import py_trees 34 | 35 | import py_trees.console as console 36 | 37 | ############################################################################## 38 | # Classes 39 | ############################################################################## 40 | 41 | 42 | def description(root: py_trees.behaviour.Behaviour) -> str: 43 | content = "A demonstration of the 'eternal guard' concept.\n\n" 44 | content += "Two binary (F|S) conditional checks will fire every\n" 45 | content += "tick, thus providing a fail-fast mechanism for the\n" 46 | content += "long running sequence of tasks behind them.\n" 47 | content += "\n" 48 | if py_trees.console.has_colours: 49 | banner_line = console.green + "*" * 79 + "\n" + console.reset 50 | s = banner_line 51 | s += console.bold_white + "Eternal Guard".center(79) + "\n" + console.reset 52 | s += banner_line 53 | s += "\n" 54 | s += content 55 | s += "\n" 56 | s += py_trees.display.unicode_tree(root) 57 | s += "\n" 58 | s += banner_line 59 | else: 60 | s = content 61 | return s 62 | 63 | 64 | def epilog() -> typing.Optional[str]: 65 | if py_trees.console.has_colours: 66 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 67 | else: 68 | return None 69 | 70 | 71 | def command_line_argument_parser() -> argparse.ArgumentParser: 72 | parser = argparse.ArgumentParser( 73 | description=description(create_root()), 74 | epilog=epilog(), 75 | formatter_class=argparse.RawDescriptionHelpFormatter, 76 | ) 77 | group = parser.add_mutually_exclusive_group() 78 | group.add_argument('-r', '--render', action='store_true', help='render dot tree to file') 79 | group.add_argument('-i', '--interactive', action='store_true', help='pause and wait for keypress at each tick') 80 | return parser 81 | 82 | 83 | def pre_tick_handler(behaviour_tree: py_trees.trees.BehaviourTree) -> None: 84 | print("\n--------- Run %s ---------\n" % behaviour_tree.count) 85 | 86 | 87 | def post_tick_handler( 88 | snapshot_visitor: py_trees.visitors.SnapshotVisitor, 89 | behaviour_tree: py_trees.trees.BehaviourTree 90 | ) -> None: 91 | print( 92 | "\n" + py_trees.display.unicode_tree( 93 | root=behaviour_tree.root, 94 | visited=snapshot_visitor.visited, 95 | previously_visited=snapshot_visitor.previously_visited 96 | ) 97 | ) 98 | print(py_trees.display.unicode_blackboard()) 99 | 100 | 101 | def create_root() -> py_trees.behaviour.Behaviour: 102 | eternal_guard = py_trees.composites.Sequence(name="Eternal Guard", memory=False) 103 | condition_one = py_trees.behaviours.StatusQueue( 104 | name="Condition 1", 105 | queue=[ 106 | py_trees.common.Status.SUCCESS, 107 | py_trees.common.Status.FAILURE, 108 | py_trees.common.Status.SUCCESS, 109 | ], 110 | eventually=py_trees.common.Status.SUCCESS 111 | ) 112 | condition_two = py_trees.behaviours.StatusQueue( 113 | name="Condition 2", 114 | queue=[ 115 | py_trees.common.Status.SUCCESS, 116 | py_trees.common.Status.SUCCESS, 117 | py_trees.common.Status.FAILURE, 118 | ], 119 | eventually=py_trees.common.Status.SUCCESS 120 | ) 121 | task_sequence = py_trees.composites.Sequence(name="Task Sequence", memory=True) 122 | task_one = py_trees.behaviours.Success(name="Worker 1") 123 | task_two = py_trees.behaviours.Running(name="Worker 2") 124 | 125 | eternal_guard.add_children([condition_one, condition_two, task_sequence]) 126 | task_sequence.add_children([task_one, task_two]) 127 | return eternal_guard 128 | 129 | 130 | ############################################################################## 131 | # Main 132 | ############################################################################## 133 | 134 | 135 | def main() -> None: 136 | """Entry point for the demo script.""" 137 | args = command_line_argument_parser().parse_args() 138 | # py_trees.logging.level = py_trees.logging.Level.DEBUG 139 | root = create_root() 140 | print(description(root)) 141 | 142 | #################### 143 | # Rendering 144 | #################### 145 | if args.render: 146 | py_trees.display.render_dot_tree(root) 147 | sys.exit() 148 | 149 | #################### 150 | # Tree Stewardship 151 | #################### 152 | behaviour_tree = py_trees.trees.BehaviourTree(root) 153 | behaviour_tree.add_pre_tick_handler(pre_tick_handler) 154 | behaviour_tree.visitors.append(py_trees.visitors.DebugVisitor()) 155 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 156 | behaviour_tree.add_post_tick_handler(functools.partial(post_tick_handler, snapshot_visitor)) 157 | behaviour_tree.visitors.append(snapshot_visitor) 158 | behaviour_tree.setup(timeout=15) 159 | 160 | #################### 161 | # Tick Tock 162 | #################### 163 | if args.interactive: 164 | py_trees.console.read_single_keypress() 165 | for _unused_i in range(1, 11): 166 | try: 167 | behaviour_tree.tick() 168 | if args.interactive: 169 | py_trees.console.read_single_keypress() 170 | else: 171 | time.sleep(0.5) 172 | except KeyboardInterrupt: 173 | break 174 | print("\n") 175 | -------------------------------------------------------------------------------- /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 | Demonstrates the behaviour lifecycle. 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 | content = "Demonstrates a typical day in the life of a behaviour.\n\n" 39 | content += "This behaviour will count from 1 to 3 and then reset and repeat. As it does\n" 40 | content += "so, it logs and displays the methods as they are called - construction, setup,\n" 41 | content += "initialisation, ticking and termination.\n" 42 | if py_trees.console.has_colours: 43 | banner_line = console.green + "*" * 79 + "\n" + console.reset 44 | s = banner_line 45 | s += console.bold_white + "Behaviour Lifecycle".center(79) + "\n" + console.reset 46 | s += banner_line 47 | s += "\n" 48 | s += content 49 | s += "\n" 50 | s += banner_line 51 | else: 52 | s = content 53 | return s 54 | 55 | 56 | def epilog() -> typing.Optional[str]: 57 | if py_trees.console.has_colours: 58 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 59 | else: 60 | return None 61 | 62 | 63 | def command_line_argument_parser() -> argparse.ArgumentParser: 64 | return argparse.ArgumentParser(description=description(), 65 | epilog=epilog(), 66 | formatter_class=argparse.RawDescriptionHelpFormatter, 67 | ) 68 | 69 | 70 | class Counter(py_trees.behaviour.Behaviour): 71 | """Simple counting behaviour. 72 | 73 | * Increments a counter from zero at each tick 74 | * Finishes with success if the counter reaches three 75 | * Resets the counter in the initialise() method. 76 | """ 77 | 78 | def __init__(self, name: str = "Counter"): 79 | """Configure the name of the behaviour.""" 80 | super(Counter, self).__init__(name) 81 | self.logger.debug("%s.__init__()" % (self.__class__.__name__)) 82 | 83 | def setup(self, **kwargs: int) -> None: 84 | """No delayed initialisation required for this example.""" 85 | self.logger.debug("%s.setup()" % (self.__class__.__name__)) 86 | 87 | def initialise(self) -> None: 88 | """Reset a counter variable.""" 89 | self.logger.debug("%s.initialise()" % (self.__class__.__name__)) 90 | self.counter = 0 91 | 92 | def update(self) -> py_trees.common.Status: 93 | """Increment the counter and decide on a new status.""" 94 | self.counter += 1 95 | new_status = py_trees.common.Status.SUCCESS if self.counter == 3 else py_trees.common.Status.RUNNING 96 | if new_status == py_trees.common.Status.SUCCESS: 97 | self.feedback_message = "counting...{0} - phew, thats enough for today".format(self.counter) 98 | else: 99 | self.feedback_message = "still counting" 100 | self.logger.debug( 101 | "%s.update()[%s->%s][%s]" % ( 102 | self.__class__.__name__, 103 | self.status, new_status, 104 | self.feedback_message 105 | ) 106 | ) 107 | return new_status 108 | 109 | def terminate(self, new_status: py_trees.common.Status) -> None: 110 | """Nothing to clean up in this example.""" 111 | self.logger.debug("%s.terminate()[%s->%s]" % (self.__class__.__name__, self.status, new_status)) 112 | 113 | 114 | ############################################################################## 115 | # Main 116 | ############################################################################## 117 | 118 | def main() -> None: 119 | """Entry point for the demo script.""" 120 | command_line_argument_parser().parse_args() 121 | 122 | print(description()) 123 | 124 | py_trees.logging.level = py_trees.logging.Level.DEBUG 125 | 126 | counter = Counter() 127 | counter.setup() 128 | try: 129 | for _unused_i in range(0, 7): 130 | counter.tick_once() 131 | time.sleep(0.5) 132 | print("\n") 133 | except KeyboardInterrupt: 134 | print("") 135 | pass 136 | -------------------------------------------------------------------------------- /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 | Demonstrates priority switching and interruption in selectors. 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 | content = "Higher priority switching and interruption in the children of a selector.\n" 42 | content += "\n" 43 | content += "In this example the higher priority child is setup to fail initially,\n" 44 | content += "falling back to the continually running second child. On the third\n" 45 | content += "tick, the first child succeeds and cancels the hitherto running child.\n" 46 | if py_trees.console.has_colours: 47 | banner_line = console.green + "*" * 79 + "\n" + console.reset 48 | s = banner_line 49 | s += console.bold_white + "Selectors".center(79) + "\n" + console.reset 50 | s += banner_line 51 | s += "\n" 52 | s += content 53 | s += "\n" 54 | s += banner_line 55 | else: 56 | s = content 57 | return s 58 | 59 | 60 | def epilog() -> typing.Optional[str]: 61 | if py_trees.console.has_colours: 62 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 63 | else: 64 | return None 65 | 66 | 67 | def command_line_argument_parser() -> argparse.ArgumentParser: 68 | parser = argparse.ArgumentParser(description=description(), 69 | epilog=epilog(), 70 | formatter_class=argparse.RawDescriptionHelpFormatter, 71 | ) 72 | parser.add_argument('-r', '--render', action='store_true', help='render dot tree to file') 73 | return parser 74 | 75 | 76 | def create_root() -> py_trees.behaviour.Behaviour: 77 | root = py_trees.composites.Selector(name="Selector", memory=False) 78 | ffs = py_trees.behaviours.StatusQueue( 79 | name="FFS", 80 | queue=[ 81 | py_trees.common.Status.FAILURE, 82 | py_trees.common.Status.FAILURE, 83 | py_trees.common.Status.SUCCESS, 84 | ], 85 | eventually=py_trees.common.Status.SUCCESS 86 | ) 87 | always_running = py_trees.behaviours.Running(name="Running") 88 | root.add_children([ffs, always_running]) 89 | return root 90 | 91 | 92 | ############################################################################## 93 | # Main 94 | ############################################################################## 95 | 96 | def main() -> None: 97 | """Entry point for the demo script.""" 98 | args = command_line_argument_parser().parse_args() 99 | print(description()) 100 | py_trees.logging.level = py_trees.logging.Level.DEBUG 101 | 102 | root = create_root() 103 | 104 | #################### 105 | # Rendering 106 | #################### 107 | if args.render: 108 | py_trees.display.render_dot_tree(root) 109 | sys.exit() 110 | 111 | #################### 112 | # Execute 113 | #################### 114 | root.setup_with_descendants() 115 | for i in range(1, 4): 116 | try: 117 | print("\n--------- Tick {0} ---------\n".format(i)) 118 | root.tick_once() 119 | print("\n") 120 | print(py_trees.display.unicode_tree(root=root, show_status=True)) 121 | time.sleep(1.0) 122 | except KeyboardInterrupt: 123 | break 124 | print("\n") 125 | -------------------------------------------------------------------------------- /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 | Demonstrates sequences in action. 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 | content = "Demonstrates sequences in action.\n\n" 42 | content += "A sequence is populated with 2-tick jobs that are allowed to run through to\n" 43 | content += "completion.\n" 44 | 45 | if py_trees.console.has_colours: 46 | banner_line = console.green + "*" * 79 + "\n" + console.reset 47 | s = banner_line 48 | s += console.bold_white + "Sequences".center(79) + "\n" + console.reset 49 | s += banner_line 50 | s += "\n" 51 | s += content 52 | s += "\n" 53 | s += banner_line 54 | else: 55 | s = content 56 | return s 57 | 58 | 59 | def epilog() -> typing.Optional[str]: 60 | if py_trees.console.has_colours: 61 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 62 | else: 63 | return None 64 | 65 | 66 | def command_line_argument_parser() -> argparse.ArgumentParser: 67 | parser = argparse.ArgumentParser(description=description(), 68 | epilog=epilog(), 69 | formatter_class=argparse.RawDescriptionHelpFormatter, 70 | ) 71 | parser.add_argument('-r', '--render', action='store_true', help='render dot tree to file') 72 | return parser 73 | 74 | 75 | def create_root() -> py_trees.behaviour.Behaviour: 76 | root = py_trees.composites.Sequence(name="Sequence", memory=True) 77 | for action in ["Action 1", "Action 2", "Action 3"]: 78 | rssss = py_trees.behaviours.StatusQueue( 79 | name=action, 80 | queue=[ 81 | py_trees.common.Status.RUNNING, 82 | py_trees.common.Status.SUCCESS, 83 | ], 84 | eventually=py_trees.common.Status.SUCCESS 85 | ) 86 | root.add_child(rssss) 87 | return root 88 | 89 | 90 | ############################################################################## 91 | # Main 92 | ############################################################################## 93 | 94 | def main() -> None: 95 | """Entry point for the demo script.""" 96 | args = command_line_argument_parser().parse_args() 97 | print(description()) 98 | py_trees.logging.level = py_trees.logging.Level.DEBUG 99 | 100 | root = create_root() 101 | 102 | #################### 103 | # Rendering 104 | #################### 105 | if args.render: 106 | py_trees.display.render_dot_tree(root) 107 | sys.exit() 108 | 109 | #################### 110 | # Execute 111 | #################### 112 | root.setup_with_descendants() 113 | for i in range(1, 6): 114 | try: 115 | print("\n--------- Tick {0} ---------\n".format(i)) 116 | root.tick_once() 117 | print("\n") 118 | print(py_trees.display.unicode_tree(root=root, show_status=True)) 119 | time.sleep(1.0) 120 | except KeyboardInterrupt: 121 | break 122 | print("\n") 123 | -------------------------------------------------------------------------------- /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 | global level 88 | if level < Level.INFO: 89 | console.logdebug(self.prefix + msg) 90 | 91 | def info(self, msg: str) -> None: 92 | global level 93 | if level < Level.WARN: 94 | console.loginfo(self.prefix + msg) 95 | 96 | def warning(self, msg: str) -> None: 97 | global level 98 | if level < Level.ERROR: 99 | console.logwarn(self.prefix + msg) 100 | 101 | def error(self, msg: str) -> None: 102 | console.logerror(self.prefix + msg) 103 | -------------------------------------------------------------------------------- /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 | """ 11 | Meta methods to create behaviours without creating behaviours themselves. 12 | """ 13 | 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | import typing 19 | 20 | from . import behaviour 21 | from . import common 22 | 23 | ############################################################################## 24 | # Utility Methods 25 | ############################################################################## 26 | 27 | 28 | # BehaviourUpdateMethod = typing.Callable[[BehaviourSubClass], common.Status] 29 | # BehaviourUpdateMethod = typing.TypeVar( 30 | # 'BehaviourUpdateMethod', 31 | # bound=typing.Callable[[behaviour.BehaviourSubClass], common.Status] 32 | # ) 33 | 34 | BehaviourUpdateMethod = typing.TypeVar('BehaviourUpdateMethod', bound=typing.Callable) 35 | 36 | 37 | def create_behaviour_from_function( 38 | func: BehaviourUpdateMethod, 39 | module: typing.Optional[str] = None 40 | ) -> "typing.Type[behaviour.Behaviour]": 41 | """ 42 | Create a behaviour from the specified function. 43 | 44 | This takes the specified function and drops it in to serve as the 45 | the Behaviour :meth:`~py_trees.behaviour.Behaviour.update` method. 46 | 47 | The user provided fucntion must include the `self` 48 | argument and return a :class:`~py_trees.behaviours.common.Status` value. 49 | 50 | It also automatically registers a method for the :meth:`~py_trees.behaviour.Behaviour.terminate` 51 | method that clears the feedback message. Other methods are left untouched. 52 | 53 | Args: 54 | func: a drop-in for the :meth:`~py_trees.behaviour.Behaviour.update` method 55 | module: suppliment it with a __module__ name if required (otherwise it will default to 'abc.') 56 | """ 57 | class_name = func.__name__.capitalize() 58 | 59 | def init(self: behaviour.Behaviour, name: str = class_name) -> None: 60 | behaviour.Behaviour.__init__(self, name=name) 61 | 62 | def terminate( 63 | self: behaviour.Behaviour, 64 | new_status: common.Status 65 | ) -> None: 66 | if new_status == common.Status.INVALID: 67 | self.feedback_message = "" 68 | 69 | class_type = type( 70 | class_name, 71 | (behaviour.Behaviour,), 72 | dict(__init__=init, update=func, terminate=terminate) 73 | ) 74 | 75 | # When module is None, it will default to 'abc.' since behaviour.Behaviour is an ABC. 76 | # If that does matter (e.g. you're creating a class for an actual module, not a script), then 77 | # use the module argument. NB: this is better than relying on magic inspect methods that aren't 78 | # consistently available across different python implementations. 79 | if module is not None: 80 | class_type.__module__ = module 81 | return class_type 82 | -------------------------------------------------------------------------------- /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/programs/render.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 | Program to render dot/svg/png graphs of methods that return a pytree. 12 | 13 | .. argparse:: 14 | :module: py_trees.programs.render 15 | :func: command_line_argument_parser 16 | :prog: py-trees-render 17 | 18 | .. image:: images/render.gif 19 | """ 20 | 21 | ############################################################################## 22 | # Imports 23 | ############################################################################## 24 | 25 | import argparse 26 | import importlib 27 | import json 28 | import sys 29 | import typing 30 | 31 | import py_trees 32 | import py_trees.console as console 33 | 34 | ############################################################################## 35 | # Classes 36 | ############################################################################## 37 | 38 | 39 | def examples() -> typing.List[str]: 40 | prefix = console.cyan + "py-trees-render" + console.yellow 41 | examples = [ 42 | prefix + " py_trees.demos.stewardship.create_tree" + console.reset, 43 | prefix + " --with-blackboard-variables" + console.reset, 44 | prefix + " --name=foo py_trees.demos.stewardship.create_tree" + console.reset, 45 | prefix + " --kwargs='{\"level\":\"all\"}' py_trees.demos.dot_graphs.create_tree" + console.reset 46 | ] 47 | return examples 48 | 49 | 50 | def description() -> str: 51 | short = "Point this program at a method which creates a root to render to dot/svg/png.\n\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 + "Trees".center(79) + "\n" + console.reset 57 | s += banner_line 58 | s += "\n" 59 | s += short 60 | s += "\n" 61 | s += console.bold + "Examples" + console.reset + "\n\n" 62 | s += '\n'.join([" $ " + example for example in examples()]) 63 | s += "\n\n" 64 | s += banner_line 65 | else: 66 | # for sphinx documentation (doesn't like raw text) 67 | s = short 68 | s += "\n" 69 | s += console.bold + "**Examples**" + console.reset + "\n\n" 70 | s += ".. code-block:: bash\n" 71 | s += " \n" 72 | s += '\n'.join([" $ {0}".format(example) for example in examples()]) 73 | s += "\n" 74 | return s 75 | 76 | 77 | def epilog() -> typing.Optional[str]: 78 | if py_trees.console.has_colours: 79 | return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset 80 | else: 81 | return None 82 | 83 | 84 | def command_line_argument_parser() -> argparse.ArgumentParser: 85 | parser = argparse.ArgumentParser(description=description(), 86 | epilog=epilog(), 87 | formatter_class=argparse.RawDescriptionHelpFormatter, 88 | ) 89 | parser.add_argument( 90 | 'method', default=None, 91 | help='space separated list of blackboard variables to watch' 92 | ) 93 | parser.add_argument( 94 | '-l', '--level', action='store', 95 | default='fine_detail', 96 | choices=['all', 'fine_detail', 'detail', 'component', 'big_picture'], 97 | help='visibility level' 98 | ) 99 | parser.add_argument( 100 | '-n', '--name', default=None, 101 | help='name to use for the created files (defaults to the root behaviour name)' 102 | ) 103 | parser.add_argument( 104 | '-k', '--kwargs', default="{}", type=json.loads, 105 | help='dictionary of keyword arguments to the method' 106 | ) 107 | parser.add_argument( 108 | '-b', '--with-blackboard-variables', default=False, action='store_true', 109 | help='add nodes for the blackboard variables' 110 | ) 111 | parser.add_argument( 112 | '-v', '--verbose', default=False, action='store_true', 113 | help="embellish each node in the dot graph with extra information" 114 | ) 115 | return parser 116 | 117 | 118 | ############################################################################## 119 | # Main 120 | ############################################################################## 121 | 122 | def main() -> None: 123 | """Entry point.""" 124 | args = command_line_argument_parser().parse_args() 125 | args.enum_level = py_trees.common.string_to_visibility_level(args.level) 126 | (module_or_class_name, method_name) = args.method.rsplit(".", 1) 127 | class_name = None 128 | 129 | try: 130 | module_itself = importlib.import_module(module_or_class_name) 131 | module_name = module_or_class_name 132 | except ImportError: 133 | # maybe it's a class? 134 | (module_name, class_name) = module_or_class_name.rsplit(".", 1) 135 | try: 136 | module_itself = importlib.import_module(module_name) 137 | except ImportError: 138 | console.logerror("Could not import module [{0}]".format(module_or_class_name)) 139 | sys.exit(1) 140 | if class_name is not None: 141 | class_type = getattr(module_itself, class_name) 142 | # first guess - it's a static method 143 | method_itself = getattr(class_type, method_name) 144 | try: 145 | root = method_itself(**(args.kwargs)) 146 | except TypeError: # oops, it's an instance method 147 | try: 148 | method_itself = getattr(class_type(), method_name) 149 | except TypeError: 150 | console.logerror( 151 | "Can only instantiate class methods if the class __init__ has no non-default arguments" 152 | ) 153 | sys.exit(1) 154 | root = method_itself(**(args.kwargs)) 155 | else: 156 | method_itself = getattr(module_itself, method_name) 157 | root = method_itself(**(args.kwargs)) 158 | 159 | # TODO figure out how to insert keyword arguments 160 | py_trees.display.render_dot_tree( 161 | root, 162 | args.enum_level, 163 | args.name, 164 | with_blackboard_variables=args.with_blackboard_variables, 165 | with_qualified_names=args.verbose 166 | ) 167 | -------------------------------------------------------------------------------- /py_trees/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/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 25 | from . import console 26 | 27 | ############################################################################## 28 | # Syntax Highlighting 29 | ############################################################################## 30 | 31 | 32 | _status_colour_strings = {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 = {common.Status.SUCCESS: console.green, 39 | common.Status.RUNNING: console.cyan, 40 | common.Status.FAILURE: console.red, 41 | common.Status.INVALID: console.yellow 42 | } 43 | 44 | 45 | def status(status: common.Status) -> str: 46 | """ 47 | Retrieve a coloured string representing a :py:class:`~py_trees.common.Status`. 48 | 49 | This is used for syntax highlighting in various modes. 50 | 51 | Args: 52 | status: behaviour status 53 | 54 | Returns: 55 | syntax highlighted string representation of the status 56 | """ 57 | return _status_colour_strings[status] 58 | 59 | 60 | def status_colour_code(status: common.Status) -> str: 61 | """ 62 | Retrieve the colour code associated with a :py:class:`~py_trees.common.Status`. 63 | 64 | This is used for syntax highlighting in various modes. 65 | 66 | Args: 67 | Status: behaviour status 68 | 69 | Returns: 70 | console colour code associated with the status 71 | """ 72 | return _status_colour_codes[status] 73 | -------------------------------------------------------------------------------- /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 24 | from . import blackboard 25 | from . import console 26 | from . import display 27 | from . import trees 28 | from . import visitors 29 | 30 | ############################################################################## 31 | # Methods 32 | ############################################################################## 33 | 34 | 35 | def print_assert_banner() -> None: 36 | print(console.green + "\n--------- Assertions ---------\n" + console.reset) 37 | 38 | 39 | AssertResultType = typing.TypeVar("AssertResultType") 40 | 41 | 42 | def print_assert_details( 43 | text: str, 44 | expected: AssertResultType, 45 | result: AssertResultType 46 | ) -> None: 47 | print(console.green + text 48 | + "." * (70 - len(text)) 49 | + console.cyan + "{}".format(expected) 50 | + console.yellow + " [{}]".format(result) 51 | + console.reset) 52 | 53 | 54 | def pre_tick_visitor(behaviour_tree: trees.BehaviourTree) -> None: 55 | print("\n--------- Run %s ---------\n" % behaviour_tree.count) 56 | 57 | 58 | def tick_tree( 59 | root: behaviour.Behaviour, 60 | from_tick: int, 61 | to_tick: int, 62 | *, 63 | visitors: typing.Optional[typing.List[visitors.VisitorBase]] = None, 64 | print_snapshot: bool = False, 65 | print_blackboard: bool = False 66 | ) -> None: 67 | if visitors is None: 68 | visitors = [] 69 | print("\n================== Iteration {}-{} ==================\n".format(from_tick, to_tick)) 70 | for i in range(from_tick, to_tick + 1): 71 | for visitor in visitors: 72 | visitor.initialise() 73 | print(("\n--------- Run %s ---------\n" % i)) 74 | for node in root.tick(): 75 | for visitor in visitors: 76 | node.visit(visitor) 77 | if print_snapshot: 78 | print(console.green + "\nTree Snapshot" + console.reset) 79 | print(display.unicode_tree(root=root, show_status=True)) 80 | if print_blackboard: 81 | print(display.unicode_blackboard()) 82 | 83 | 84 | def clear_blackboard() -> None: 85 | # Useful between tests 86 | blackboard.Blackboard.storage = {} 87 | blackboard.Blackboard.clients = {} 88 | blackboard.Blackboard.metadata = {} 89 | 90 | 91 | def print_summary(nodes: typing.List[behaviour.Behaviour]) -> None: 92 | print("\n--------- Summary ---------\n") 93 | for node in nodes: 94 | print("%s" % node) 95 | -------------------------------------------------------------------------------- /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 | """ 11 | Time related behaviours. 12 | """ 13 | 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | import time 19 | 20 | from . import behaviour 21 | from . import common 22 | 23 | ############################################################################## 24 | # Behaviours 25 | ############################################################################## 26 | 27 | 28 | class Timer(behaviour.Behaviour): 29 | """ 30 | A simple, blocking timer behaviour running off python time.time(). 31 | 32 | This behaviour is :py:data:`~py_trees.common.Status.RUNNING` until the timer 33 | runs out, at which point it is :data:`~py_trees.common.Status.SUCCESS`. This can be 34 | used in a wide variety of situations - pause, duration, timeout depending on how 35 | it is wired into the tree (e.g. pause in a sequence, duration/timeout in 36 | a parallel). 37 | 38 | The timer gets reset either upon entry (:meth:`~py_trees.behaviour.Behaviour.initialise`) 39 | if it hasn't already been set and gets cleared when it either runs out, or the behaviour is 40 | interrupted by a higher priority or parent cancelling it. 41 | 42 | Args: 43 | name: name of the behaviour 44 | duration: length of time to run (in seconds) 45 | 46 | Raises: 47 | TypeError: if the provided duration is not a real number 48 | 49 | .. note:: 50 | This succeeds the first time the behaviour is ticked **after** the expected 51 | finishing time. 52 | 53 | .. tip:: 54 | Use the :func:`~py_trees.decorators.RunningIsFailure` decorator if you need 55 | :data:`~py_trees.common.Status.FAILURE` until the timer finishes. 56 | """ 57 | 58 | def __init__(self, name: str = "Timer", duration: float = 5.0): 59 | super(Timer, self).__init__(name) 60 | if not isinstance(duration, float): 61 | raise TypeError("Timer: duration should be int or float, but you passed in {}".format(type(duration))) 62 | self.duration: float = duration 63 | self.finish_time: float = 0.0 64 | self.feedback_message: str = "duration set to '{0}'s".format(self.duration) 65 | 66 | def initialise(self) -> None: 67 | """ 68 | Store the expected finishing time. 69 | """ 70 | self.logger.debug("%s.initialise()" % self.__class__.__name__) 71 | self.finish_time = time.time() + self.duration 72 | self.feedback_message = "configured to fire in '{0}' seconds".format(self.duration) 73 | 74 | def update(self) -> common.Status: 75 | """ 76 | Check the timer and update the behaviour result accordingly. 77 | 78 | Returns: 79 | :data:`~py_trees.common.Status.RUNNING` until timer expires, then 80 | :data:`~py_trees.common.Status.SUCCESS`. 81 | 82 | """ 83 | self.logger.debug("%s.update()" % self.__class__.__name__) 84 | current_time = time.time() 85 | if current_time > self.finish_time: 86 | self.feedback_message = "timer ran out [{0}]".format(self.duration) 87 | return common.Status.SUCCESS 88 | else: 89 | # do not show the time, it causes the tree to be 'changed' every tick 90 | # and we don't want to spam visualisations with almost meaningless updates 91 | self.feedback_message = "still running" # (%s)" % (self.finish_time - current_time) 92 | return common.Status.RUNNING 93 | -------------------------------------------------------------------------------- /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 | """ 11 | Assorted utility functions. 12 | """ 13 | 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | from __future__ import annotations 19 | 20 | import multiprocessing 21 | import os 22 | import re 23 | import traceback 24 | import typing 25 | 26 | ############################################################################## 27 | # Python Helpers 28 | ############################################################################## 29 | 30 | C = typing.TypeVar('C', bound=typing.Callable) 31 | 32 | # TODO: This currently doesn't work well with mypy - dynamic typing 33 | # is not its thing. Need to find a way to make this work without 34 | # creating errors on the user side. In the docstring's example, usage 35 | # of the static 'counter' variable results in: 36 | # 37 | # error: "Callable[[], Any]" has no attribute "counter" [attr-defined] 38 | 39 | 40 | def static_variables( 41 | **kwargs: typing.Any 42 | ) -> typing.Callable[[C], C]: 43 | """ 44 | Attach initialised static variables to a python method. 45 | 46 | .. code-block:: python 47 | 48 | @static_variables(counter=0) 49 | def foo(): 50 | foo.counter += 1 51 | print("Counter: {}".format(foo.counter)) 52 | """ 53 | def decorate( 54 | func: C 55 | ) -> C: 56 | for k in kwargs: 57 | setattr(func, k, kwargs[k]) 58 | return func 59 | return decorate 60 | 61 | 62 | @static_variables(primitives={bool, str, int, float}) 63 | def is_primitive(incoming: typing.Any) -> bool: 64 | """ 65 | Check if an incoming argument is a primitive type with no esoteric accessors. 66 | 67 | That is, it has no class attributes or container style [] accessors. 68 | 69 | Args: 70 | incoming: the instance to check 71 | Returns: 72 | True or false, depending on the check against the reserved primitives 73 | """ 74 | return type(incoming) in is_primitive.primitives # type: ignore[attr-defined] 75 | 76 | 77 | def truncate(original: str, length: int) -> str: 78 | """ 79 | Provide an elided (...) version of a string if it is longer than desired. 80 | 81 | Args: 82 | original: string to elide 83 | length: constrain the elided string to this 84 | """ 85 | s = (original[:length - 3] + '...') if len(original) > length else original 86 | return s 87 | 88 | ############################################################################## 89 | # System Tools 90 | ############################################################################## 91 | 92 | 93 | class Process(multiprocessing.Process): 94 | def __init__(self, *args: typing.Any, **kwargs: typing.Any): 95 | multiprocessing.Process.__init__(self, *args, **kwargs) 96 | self._pconn, self._cconn = multiprocessing.Pipe() 97 | self._exception = None 98 | 99 | def run(self) -> None: 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 | if self._pconn.poll(): 110 | self._exception = self._pconn.recv() 111 | return self._exception 112 | 113 | 114 | def which(program: str) -> typing.Optional[str]: 115 | """ 116 | Call the command line 'which' tool (convenience wrapper). 117 | 118 | Args: 119 | program: name of the program to find. 120 | 121 | Returns: 122 | path to the program or None if it doesnt exist. 123 | """ 124 | def is_exe(fpath: str) -> bool: 125 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 126 | 127 | fpath, unused_fname = os.path.split(program) 128 | if fpath: 129 | if is_exe(program): 130 | return program 131 | else: 132 | for path in os.environ["PATH"].split(os.pathsep): 133 | path = path.strip('"') 134 | exe_file = os.path.join(path, program) 135 | if is_exe(exe_file): 136 | return exe_file 137 | 138 | return None 139 | 140 | 141 | def get_valid_filename(s: str) -> str: 142 | """ 143 | Clean up and style a string so that it can be used as a filename. 144 | 145 | This is valid only from the perspective of the py_trees package. It does 146 | place a few extra constraints on strings to keep string handling and 147 | manipulation complexities to a minimum so that sanity prevails. 148 | 149 | * Removes leading and trailing spaces 150 | * Convert other spaces and newlines to underscores 151 | * Remove anything that is not an alphanumeric, dash, underscore, or dot 152 | 153 | .. code-block:: python 154 | 155 | >>> utilities.get_valid_filename("john's portrait in 2004.jpg") 156 | 'johns_portrait_in_2004.jpg' 157 | 158 | Args: 159 | program (:obj:`str`): string to convert to a valid filename 160 | 161 | Returns: 162 | :obj:`str`: a representation of the specified string as a valid filename 163 | """ 164 | s = str(s).strip().lower().replace(' ', '_').replace('\n', '_') 165 | return re.sub(r'(?u)[^-\w.]', '', s) 166 | 167 | 168 | def get_fully_qualified_name(instance: object) -> str: 169 | """ 170 | Retrieve the fully qualified name of an object. 171 | 172 | For example, an instance of 173 | :class:`~py_trees.composites.Sequence` becomes 'py_trees.composites.Sequence'. 174 | 175 | Args: 176 | instance (:obj:`object`): an instance of any class 177 | 178 | Returns: 179 | :obj:`str`: the fully qualified name 180 | """ 181 | module = instance.__class__.__module__ 182 | # if there is no module, it will report builtin, get that 183 | # string via what should remain constant, the 'str' class 184 | # and check against that. 185 | builtin = str.__class__.__module__ 186 | if module is None or module == builtin: 187 | return instance.__class__.__name__ 188 | else: 189 | return module + '.' + instance.__class__.__name__ 190 | -------------------------------------------------------------------------------- /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 | """ 10 | Package version number. 11 | """ 12 | 13 | ############################################################################## 14 | # Version 15 | ############################################################################## 16 | 17 | # When changing, Also update setup.py and package.xml 18 | # TODO: use pkg_resources to fetch the version from setup.py 19 | __version__ = '2.2.0' 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py_trees" 3 | version = "2.2.0" 4 | description = "pythonic implementation of behaviour trees" 5 | authors = ["Daniel Stonier", "Naveed Usmani", "Michal Staniaszek"] 6 | maintainers = ["Daniel Stonier "] 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.8" 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.7" } 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-bandit = ">=4.1" # secure code 51 | flake8-bugbear = ">=22.9" # bugs & design not strictly pep8 52 | 53 | [tool.poetry.group.docs.dependencies] 54 | sphinx = "<6" 55 | sphinx-rtd-theme = ">=1.1" # not yet ready for sphinx 6 56 | sphinx-argparse = ">=0.4" 57 | 58 | [tool.poetry.scripts] 59 | py-trees-render = "py_trees.programs.render:main" 60 | py-trees-demo-action-behaviour = "py_trees.demos.action:main" 61 | py-trees-demo-behaviour-lifecycle = "py_trees.demos.lifecycle:main" 62 | py-trees-demo-blackboard = "py_trees.demos.blackboard:main" 63 | py-trees-demo-blackboard-namespaces = "py_trees.demos.blackboard_namespaces:main" 64 | py-trees-demo-blackboard-remappings = "py_trees.demos.blackboard_remappings:main" 65 | py-trees-demo-context-switching = "py_trees.demos.context_switching:main" 66 | py-trees-demo-display-modes = "py_trees.demos.display_modes:main" 67 | py-trees-demo-dot-graphs = "py_trees.demos.dot_graphs:main" 68 | py-trees-demo-either-or = "py_trees.demos.either_or:main" 69 | py-trees-demo-eternal-guard = "py_trees.demos.eternal_guard:main" 70 | py-trees-demo-logging = "py_trees.demos.logging:main" 71 | py-trees-demo-pick-up-where-you-left-off = "py_trees.demos.pick_up_where_you_left_off:main" 72 | py-trees-demo-selector = "py_trees.demos.selector:main" 73 | py-trees-demo-sequence = "py_trees.demos.sequence:main" 74 | py-trees-demo-tree-stewardship = "py_trees.demos.stewardship:main" 75 | 76 | [build-system] 77 | requires = ["poetry-core>=1.0.0"] 78 | build-backend = "poetry.core.masonry.api" 79 | -------------------------------------------------------------------------------- /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 | import os 11 | 12 | install_requires = [ 13 | 'setuptools', 14 | 'pydot' 15 | ] 16 | 17 | # Some duplication of properties in: 18 | # - setup.py, (ros / legacy) 19 | # - package.xml (ros) 20 | # - pyproject.toml (poetry) 21 | # - py_trees/version.py (common) 22 | # Keep them in sync. 23 | d = setup( 24 | name='py_trees', 25 | version='2.2.0', 26 | packages=find_packages(exclude=['tests*', 'docs*']), 27 | package_data={"py_trees": ["py.typed"]}, 28 | install_requires=install_requires, 29 | author='Daniel Stonier, Naveed Usmani, Michal Staniaszek', 30 | maintainer='Daniel Stonier ', 31 | url='https://github.com/splintered-reality/py_trees', 32 | keywords='behaviour-trees', 33 | zip_safe=True, 34 | classifiers=[ 35 | 'Environment :: Console', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: BSD License', 38 | 'Programming Language :: Python', 39 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 40 | 'Topic :: Software Development :: Libraries' 41 | ], 42 | description="pythonic implementation of behaviour trees", 43 | long_description="A behaviour tree implementation for rapid development of small scale decision making engines that don't need to be real time reactive.", 44 | license='BSD', 45 | entry_points={ 46 | 'console_scripts': [ 47 | 'py-trees-render = py_trees.programs.render:main', 48 | 'py-trees-demo-action-behaviour = py_trees.demos.action:main', 49 | 'py-trees-demo-behaviour-lifecycle = py_trees.demos.lifecycle:main', 50 | 'py-trees-demo-blackboard = py_trees.demos.blackboard:main', 51 | 'py-trees-demo-blackboard-namespaces = py_trees.demos.blackboard_namespaces:main', 52 | 'py-trees-demo-blackboard-remappings = py_trees.demos.blackboard_remappings:main', 53 | 'py-trees-demo-context-switching = py_trees.demos.context_switching:main', 54 | 'py-trees-demo-display-modes = py_trees.demos.display_modes:main', 55 | 'py-trees-demo-dot-graphs = py_trees.demos.dot_graphs:main', 56 | 'py-trees-demo-either-or = py_trees.demos.either_or:main', 57 | 'py-trees-demo-eternal-guard = py_trees.demos.eternal_guard:main', 58 | 'py-trees-demo-logging = py_trees.demos.logging:main', 59 | 'py-trees-demo-pick-up-where-you-left-off = py_trees.demos.pick_up_where_you_left_off:main', 60 | 'py-trees-demo-selector = py_trees.demos.selector:main', 61 | 'py-trees-demo-sequence = py_trees.demos.sequence:main', 62 | 'py-trees-demo-tree-stewardship = py_trees.demos.stewardship:main', 63 | ], 64 | }, 65 | ) 66 | -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonier/py_trees/50a1cab4c13be332ad6286a967c0e5f13b7b3559/tests/__init__.py -------------------------------------------------------------------------------- /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 pytest 12 | import typing 13 | 14 | import py_trees 15 | import py_trees.console as console 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 | AssertResultType = typing.TypeVar("AssertResultType") 33 | 34 | def assert_details( 35 | text: str, 36 | expected: AssertResultType, 37 | result: AssertResultType 38 | ) -> None: 39 | print(console.green + text + 40 | "." * (70 - len(text)) + 41 | console.cyan + "{}".format(expected) + 42 | console.yellow + " [{}]".format(result) + 43 | console.reset) 44 | 45 | 46 | ############################################################################## 47 | # Tests 48 | ############################################################################## 49 | 50 | 51 | def test_replacing_children() -> None: 52 | console.banner("Replacing Children") 53 | parent = py_trees.composites.Sequence(name="Parent", memory=True) 54 | front = py_trees.behaviours.Success(name="Front") 55 | back = py_trees.behaviours.Success(name="Back") 56 | old_child = py_trees.behaviours.Success(name="Old Child") 57 | new_child = py_trees.behaviours.Success(name="New Child") 58 | parent.add_children([front, old_child, back]) 59 | print(py_trees.display.unicode_tree(parent, show_status=True)) 60 | parent.replace_child(old_child, new_child) 61 | print(py_trees.display.unicode_tree(parent, show_status=True)) 62 | print("\n--------- Assertions ---------\n") 63 | print("old_child.parent is None") 64 | assert old_child.parent is None 65 | print("new_child.parent is parent") 66 | assert new_child.parent is parent 67 | 68 | 69 | def test_removing_children() -> None: 70 | console.banner("Removing Children") 71 | parent = py_trees.composites.Sequence(name="Parent", memory=True) 72 | child = py_trees.behaviours.Success(name="Child") 73 | print("\n--------- Assertions ---------\n") 74 | print("child.parent is None after removing by instance") 75 | parent.add_child(child) 76 | parent.remove_child(child) 77 | assert child.parent is None 78 | print("child.parent is None after removing by id") 79 | parent.add_child(child) 80 | parent.remove_child_by_id(child.id) 81 | assert child.parent is None 82 | print("child.parent is None after removing all children") 83 | parent.add_child(child) 84 | parent.remove_all_children() 85 | assert child.parent is None 86 | 87 | 88 | def test_composite_add_child_exception() -> None: 89 | console.banner("Composite Add Child Exception - Invalid Type") 90 | root = py_trees.composites.Selector(name="Selector", memory=False) 91 | with pytest.raises(TypeError) as context: # if raised, context survives 92 | # intentional error - silence mypy 93 | root.add_child(5.0) # type: ignore[arg-type] 94 | py_trees.tests.print_assert_details("TypeError raised", "raised", "not raised") 95 | py_trees.tests.print_assert_details("TypeError raised", "yes", "yes") 96 | assert "TypeError" == context.typename 97 | py_trees.tests.print_assert_details("Substring match", "behaviours", f"{context.value}") 98 | assert "behaviours" in str(context.value) 99 | 100 | 101 | def test_protect_against_multiple_parents() -> None: 102 | console.banner("Protect Against Multiple Parents") 103 | child = py_trees.behaviours.Success(name="Success") 104 | first_parent = py_trees.composites.Selector(name="Selector", memory=False) 105 | second_parent = py_trees.composites.Sequence(name="Sequence", memory=True) 106 | with pytest.raises(RuntimeError) as context: # if raised, context survives 107 | # Adding a child to two parents - expecting a RuntimeError 108 | for parent in [first_parent, second_parent]: 109 | parent.add_child(child) 110 | py_trees.tests.print_assert_details("RuntimeError raised", "raised", "not raised") 111 | py_trees.tests.print_assert_details("RuntimeError raised", "yes", "yes") 112 | assert "RuntimeError" == context.typename 113 | py_trees.tests.print_assert_details("Substring match", "parent", f"{context.value}") 114 | assert "parent" in str(context.value) 115 | 116 | 117 | def test_remove_nonexistant_child() -> None: 118 | console.banner("Remove non-existant child") 119 | root = py_trees.composites.Sequence(name="Sequence", memory=True) 120 | child = py_trees.behaviours.Success(name="Success") 121 | root.add_child(child) 122 | non_existant_child = py_trees.behaviours.Success(name="Ooops") 123 | 124 | with pytest.raises(IndexError) as context: # if raised, context survives 125 | root.remove_child_by_id(non_existant_child.id) 126 | py_trees.tests.print_assert_details("IndexError raised", "raised", "not raised") 127 | py_trees.tests.print_assert_details("IndexError raised", "yes", "yes") 128 | assert "IndexError" == context.typename 129 | py_trees.tests.print_assert_details("Substring match", "not found", f"{context.value}") 130 | assert "not found" in str(context.value) 131 | -------------------------------------------------------------------------------- /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(u'\u26A1', "a", "ascii") == "a" 21 | assert py_trees.console.define_symbol_or_fallback(u'\u26A1', "a", "utf-8") == u'\u26A1' 22 | -------------------------------------------------------------------------------- /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 | import py_trees.console as console 15 | 16 | ############################################################################## 17 | # Either Or 18 | ############################################################################## 19 | 20 | 21 | def create_root() -> typing.Tuple[py_trees.behaviour.Behaviour, py_trees.behaviour.Behaviour, py_trees.behaviour.Behaviour]: 22 | trigger_one = py_trees.decorators.FailureIsRunning( 23 | name="FisR", 24 | child=py_trees.behaviours.SuccessEveryN( 25 | name="Joystick 1", 26 | n=4 27 | ) 28 | ) 29 | trigger_two = py_trees.decorators.FailureIsRunning( 30 | name="FisR", 31 | child=py_trees.behaviours.SuccessEveryN( 32 | name="Joystick 2", 33 | n=7 34 | ) 35 | ) 36 | enable_joystick_one = py_trees.behaviours.SetBlackboardVariable( 37 | name="Joy1 - Enabled", 38 | variable_name="joystick_one", 39 | variable_value="enabled", 40 | overwrite=True) 41 | enable_joystick_two = py_trees.behaviours.SetBlackboardVariable( 42 | name="Joy2 - Enabled", 43 | variable_name="joystick_two", 44 | variable_value="enabled", 45 | overwrite=True) 46 | reset_joystick_one = py_trees.behaviours.SetBlackboardVariable( 47 | name="Joy1 - Disabled", 48 | variable_name="joystick_one", 49 | variable_value="disabled", 50 | overwrite=True) 51 | reset_joystick_two = py_trees.behaviours.SetBlackboardVariable( 52 | name="Joy2 - Disabled", 53 | variable_name="joystick_two", 54 | variable_value="disabled", 55 | overwrite=True) 56 | task_one = py_trees.behaviours.TickCounter( 57 | name="Task 1", 58 | duration=2, 59 | completion_status=py_trees.common.Status.SUCCESS 60 | ) 61 | task_two = py_trees.behaviours.TickCounter( 62 | name="Task 2", 63 | duration=2, 64 | completion_status=py_trees.common.Status.SUCCESS 65 | ) 66 | idle = py_trees.behaviours.Running(name="Idle") 67 | either_or = py_trees.idioms.either_or( 68 | name="EitherOr", 69 | conditions=[ 70 | py_trees.common.ComparisonExpression("joystick_one", "enabled", operator.eq), 71 | py_trees.common.ComparisonExpression("joystick_two", "enabled", operator.eq), 72 | ], 73 | subtrees=[task_one, task_two], 74 | namespace="either_or", 75 | ) 76 | root = py_trees.composites.Parallel( 77 | name="Root", 78 | policy=py_trees.common.ParallelPolicy.SuccessOnAll(synchronise=False) 79 | ) 80 | reset = py_trees.composites.Sequence(name="Reset", memory=True) 81 | reset.add_children([reset_joystick_one, reset_joystick_two]) 82 | joystick_one_events = py_trees.composites.Sequence(name="Joy1 Events", memory=True) 83 | joystick_one_events.add_children([trigger_one, enable_joystick_one]) 84 | joystick_two_events = py_trees.composites.Sequence(name="Joy2 Events", memory=True) 85 | joystick_two_events.add_children([trigger_two, enable_joystick_two]) 86 | tasks = py_trees.composites.Selector(name="Tasks", memory=False) 87 | tasks.add_children([either_or, idle]) 88 | root.add_children([reset, joystick_one_events, joystick_two_events, tasks]) 89 | return (root, task_one, task_two) 90 | 91 | 92 | def test_basic_workflow() -> None: 93 | # same as py-trees-demo-idiom-either-or 94 | root, task_one, task_two = create_root() 95 | # tree = py_trees.trees.BehaviourTree(root=root) 96 | root.tick_once() 97 | # tree.tick() 98 | py_trees.tests.print_assert_banner() 99 | py_trees.tests.print_assert_details( 100 | text="Tick 1 - tasks not yet ticked", 101 | expected=py_trees.common.Status.INVALID, 102 | result=task_one.status, 103 | ) 104 | assert task_one.status == py_trees.common.Status.INVALID 105 | root.tick_once() 106 | root.tick_once() 107 | root.tick_once() 108 | py_trees.tests.print_assert_details( 109 | text="Tick 4 - task one running", 110 | expected=py_trees.common.Status.RUNNING, 111 | result=task_one.status, 112 | ) 113 | assert task_one.status == py_trees.common.Status.RUNNING 114 | root.tick_once() 115 | root.tick_once() 116 | py_trees.tests.print_assert_details( 117 | text="Tick 6 - task one finished", 118 | expected=py_trees.common.Status.SUCCESS, 119 | result=task_one.status, 120 | ) 121 | assert task_one.status == py_trees.common.Status.SUCCESS 122 | root.tick_once() 123 | py_trees.tests.print_assert_details( 124 | text="Tick 7 - task two starts", 125 | expected=py_trees.common.Status.RUNNING, 126 | result=task_two.status, 127 | ) 128 | assert task_two.status == py_trees.common.Status.RUNNING 129 | root.tick_once() 130 | py_trees.tests.print_assert_details( 131 | text="Tick 8 - task one ignored", 132 | expected=py_trees.common.Status.INVALID, 133 | result=task_one.status, 134 | ) 135 | assert task_one.status == py_trees.common.Status.INVALID 136 | root.tick_once() 137 | py_trees.tests.print_assert_details( 138 | text="Tick 7 - task two finished", 139 | expected=py_trees.common.Status.SUCCESS, 140 | result=task_two.status, 141 | ) 142 | assert task_two.status == py_trees.common.Status.SUCCESS 143 | -------------------------------------------------------------------------------- /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 | foo_instance = py_trees.meta.create_behaviour_from_function(foo)(name="Foo") 32 | success = py_trees.behaviours.Success(name="Success") 33 | named_success = py_trees.meta.create_behaviour_from_function(py_trees.behaviours.success)(name="Woohoo") 34 | 35 | print("\n--------- Assertions ---------\n") 36 | print("foo_instance.name = {} [Foo]".format(foo_instance.name)) 37 | assert foo_instance.name == "Foo" 38 | print("success.name = {}".format(success.name)) 39 | assert success.name == "Success" 40 | print("named_success.name == {} Woohoo".format(named_success.name)) 41 | assert named_success.name == "Woohoo" 42 | -------------------------------------------------------------------------------- /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", 46 | child=py_trees.behaviours.Periodic(name="Periodic", n=3) 47 | ) 48 | piwylo = py_trees.idioms.pick_up_where_you_left_off( 49 | name="Pick Up\nWhere You\nLeft Off", 50 | tasks=tasks 51 | ) 52 | root = py_trees.composites.Selector(name="Root", memory=False) 53 | root.add_children([high_priority_interrupt, piwylo]) 54 | 55 | print(py_trees.display.unicode_tree(root)) 56 | visitor = py_trees.visitors.DebugVisitor() 57 | py_trees.tests.tick_tree(root, 1, 3, visitors=[visitor]) 58 | print() 59 | 60 | print("\n--------- Assertions ---------\n") 61 | print("high_priority_interrupt.status == py_trees.common.Status.FAILURE") 62 | assert high_priority_interrupt.status == py_trees.common.Status.FAILURE 63 | print("piwylo.status == py_trees.common.Status.RUNNING") 64 | assert piwylo.status == py_trees.common.Status.RUNNING 65 | print("task_one.status == py_trees.common.Status.SUCCESS") 66 | assert task_one.status == py_trees.common.Status.SUCCESS 67 | print("task_two.status == py_trees.common.Status.RUNNING") 68 | assert task_two.status == py_trees.common.Status.RUNNING 69 | 70 | py_trees.tests.tick_tree(root, 4, 5, visitors=[visitor]) 71 | 72 | print("\n--------- Assertions ---------\n") 73 | print("high_priority_interrupt.status == py_trees.common.Status.SUCCESS") 74 | assert high_priority_interrupt.status == py_trees.common.Status.SUCCESS 75 | print("piwylo.status == py_trees.common.Status.INVALID") 76 | assert piwylo.status == py_trees.common.Status.INVALID 77 | print("task_one.status == py_trees.common.Status.INVALID") 78 | assert task_one.status == py_trees.common.Status.INVALID 79 | print("task_two.status == py_trees.common.Status.INVALID") 80 | assert task_two.status == py_trees.common.Status.INVALID 81 | 82 | py_trees.tests.tick_tree(root, 6, 8, visitors=[visitor]) 83 | 84 | print("\n--------- Assertions ---------\n") 85 | print("high_priority_interrupt.status == py_trees.common.Status.FAILURE") 86 | assert high_priority_interrupt.status == py_trees.common.Status.FAILURE 87 | print("piwylo.status == py_trees.common.Status.RUNNING") 88 | assert piwylo.status == py_trees.common.Status.RUNNING 89 | print("task_one.status == py_trees.common.Status.INVALID") 90 | assert task_one.status == py_trees.common.Status.INVALID 91 | print("task_two.status == py_trees.common.Status.RUNNING") 92 | assert task_two.status == py_trees.common.Status.RUNNING 93 | -------------------------------------------------------------------------------- /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 pytest 12 | 13 | import py_trees 14 | import py_trees.tests 15 | import py_trees.console as console 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 | # Tests 26 | ############################################################################## 27 | 28 | 29 | def test_timer_invalid_duration() -> None: 30 | console.banner("Timer Exceptions - Invalid Duration") 31 | with pytest.raises(TypeError) as context: # if raised, context survives 32 | # intentional error -> silence mypy 33 | unused_timer = py_trees.timers.Timer( 34 | name="Timer", 35 | duration="invalid_type" # type: ignore[arg-type] 36 | ) 37 | py_trees.tests.print_assert_details("TypeError raised", "raised", "not raised") 38 | py_trees.tests.print_assert_details("TypeError raised", "yes", "yes") 39 | assert "TypeError" == context.typename 40 | py_trees.tests.print_assert_details(" substring match", "duration", f"{context.value}") 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(console.green + "------------------ Assertions ------------------\n" + console.reset) 29 | for name, expected_name in names.items(): 30 | print(console.cyan + repr(name) + ": " + console.yellow + expected_name + " [" + utilities.get_valid_filename(name) + "]" + console.reset) 31 | assert utilities.get_valid_filename(name) == expected_name 32 | 33 | 34 | def test_get_fully_qualified_name() -> None: 35 | console.banner("Fully Qualified Names") 36 | pairs = { 37 | "py_trees.behaviours.Periodic": py_trees.behaviours.Periodic( 38 | name="foo", 39 | n=3 40 | ), 41 | "py_trees.decorators.Inverter": py_trees.decorators.Inverter( 42 | name="Inverter", 43 | child=py_trees.behaviours.Success(name="Success") 44 | ), 45 | "py_trees.behaviours.Success": py_trees.behaviours.Success( 46 | name="Success" 47 | ), 48 | } 49 | print(console.green + "------------------ Assertions ------------------\n" + console.reset) 50 | for expected_name, object_instance in pairs.items(): 51 | print(console.cyan + expected_name + console.white + " == " + console.yellow + utilities.get_fully_qualified_name(object_instance) + console.reset) 52 | assert expected_name == utilities.get_fully_qualified_name(object_instance) 53 | -------------------------------------------------------------------------------- /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( 28 | name: str 29 | ) -> py_trees.behaviours.StatusQueue: 30 | return py_trees.behaviours.StatusQueue( 31 | name=name, 32 | queue=[ 33 | py_trees.common.Status.FAILURE, 34 | py_trees.common.Status.FAILURE, 35 | py_trees.common.Status.FAILURE, 36 | py_trees.common.Status.RUNNING, 37 | py_trees.common.Status.RUNNING, 38 | py_trees.common.Status.SUCCESS, 39 | ], 40 | eventually=None 41 | ) 42 | 43 | ############################################################################## 44 | # Tests 45 | ############################################################################## 46 | 47 | 48 | def test_snapshot_visitor() -> None: 49 | console.banner("Snapshot Visitor") 50 | 51 | root = py_trees.composites.Selector(name='Selector', memory=False) 52 | a = create_fffrrs_repeat_status_queue(name="A") 53 | b = create_fffrrs_repeat_status_queue(name="B") 54 | c = py_trees.behaviours.StatusQueue( 55 | name="C", 56 | queue=[ 57 | py_trees.common.Status.RUNNING, 58 | py_trees.common.Status.RUNNING, 59 | py_trees.common.Status.RUNNING, 60 | ], 61 | eventually=py_trees.common.Status.SUCCESS 62 | ) 63 | root.add_child(a) 64 | root.add_child(b) 65 | root.add_child(c) 66 | print(py_trees.display.unicode_tree(root)) 67 | 68 | debug_visitor = py_trees.visitors.DebugVisitor() 69 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 70 | 71 | for i, result in zip(range(1, 5), [True, False, False, True]): 72 | py_trees.tests.tick_tree( 73 | root, i, i, 74 | visitors=[debug_visitor, 75 | snapshot_visitor], 76 | print_snapshot=True 77 | ) 78 | print("--------- Assertions ---------\n") 79 | print("snapshot_visitor.changed == {}".format(result)) 80 | assert snapshot_visitor.changed is result 81 | 82 | print("Done") 83 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Tox Configuration 3 | ################################################################################ 4 | 5 | [constants] 6 | source_locations = 7 | py_trees 8 | tests 9 | # doc/examples 10 | 11 | [tox] 12 | envlist = py38, py310, ufmt, check, mypy38, mypy310 13 | 14 | ################################################################################ 15 | # PyTest 16 | ################################################################################ 17 | 18 | [testenv] 19 | require_locked_deps = true 20 | require_poetry = true 21 | locked_deps = 22 | pytest 23 | pytest-console-scripts 24 | pytest-cov 25 | commands = 26 | pytest -s tests/ 27 | pytest --cov 28 | 29 | [coverage:run] 30 | # ensure conditional branches are explored (important) 31 | # https://coverage.readthedocs.io/en/latest/branch.html#branch 32 | branch = true 33 | 34 | ###################################################################### 35 | # Ufmt (black + isort) 36 | ###################################################################### 37 | 38 | [testenv:ufmt] 39 | description = Un-opinionated auto-formatting. 40 | locked_deps = 41 | ufmt 42 | commands = 43 | ufmt format {[constants]source_locations} 44 | 45 | ################################################################################ 46 | # Flake 8 47 | ################################################################################ 48 | 49 | [testenv:check] 50 | skip_install = true 51 | description = Formatting checks and linting (flake8 & ufmt check). 52 | locked_deps = 53 | darglint 54 | flake8 55 | flake8-bandit 56 | flake8-docstrings 57 | ufmt 58 | commands = 59 | flake8 {[constants]source_locations} 60 | # ufmt check {[constants]source_locations} 61 | 62 | 63 | ################################################################################ 64 | # Flake 8 Configuration 65 | # 66 | # Don't require docstrings, but parse them correctly if they are there. 67 | # 68 | # D100 Missing docstring in public module 69 | # D101 Missing docstring in public class 70 | # D102 Missing docstring in public method 71 | # D103 Missing docstring in public function 72 | # D105 Missing docstring in magic method 73 | # D107 Missing docstring in __init__ 74 | # 75 | # Jamming docstrings into a single line looks cluttered. 76 | # 77 | # D200 One-line docstring should fit on one line with quotes 78 | # 79 | # Weakly prefer breaking before a binary operator, so suppress that warning. 80 | # See https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b 81 | # 82 | # W503 line break before binary operator 83 | # 84 | ################################################################################ 85 | 86 | [flake8] 87 | # Relax checks in the tests dir 88 | # - D*** documentation (docstrings) 89 | # - S101 use of assert warning (bandit) 90 | per-file-ignores = 91 | tests/*: D, S101 92 | 93 | # Match black line lengths 94 | # max-line-length = 88 95 | max-line-length = 120 96 | 97 | # Avoid overly complex functions 98 | # NB: this option by default is off, recommend complexity is 10 99 | # https://en.wikipedia.org/wiki/Cyclomatic_complexity 100 | max-complexity: 15 101 | 102 | # darglint docstring matching implementation checks 103 | # - short: one liners are not checked 104 | # - long: one liners and descriptions without args/returns are not checked 105 | strictness = long 106 | docstring_style=sphinx 107 | 108 | # From 2.1.x -> unwind these checks 109 | ignore = D100, D101, D102, D103, D105, D107, D200, W503, E501, F841, E251, E275, W504, E123, E302, E305, S314, E201, E202, E121, E126, F401, S10, S405, E221, S603, S607, S404, C901, S101 110 | 111 | ################################################################################ 112 | # Mypy 113 | ################################################################################ 114 | 115 | [testenv:mypy38] 116 | skip_install = true 117 | description = Static type checking against python 3.8. 118 | locked_deps = 119 | mypy 120 | pytest 121 | commands = 122 | mypy --config-file {toxinidir}/tox.ini --python-version 3.8 {[constants]source_locations} 123 | 124 | [testenv:mypy310] 125 | skip_install = true 126 | description = Static type checking against python 3.10. 127 | commands = 128 | mypy --config-file {toxinidir}/tox.ini --python-version 3.10 {[constants]source_locations} 129 | locked_deps = 130 | mypy 131 | pytest 132 | 133 | [mypy] 134 | # With no options you get light coverage on some basics. 135 | # In general, you'll want to go further. Even for new code, it's 136 | # a great idea to enforce typehints on func args and returns as 137 | # it immensively improves the readability and traceability of 138 | # your code (and catches bugs!). 139 | 140 | #################### 141 | # Good to have 142 | #################### 143 | 144 | # Typehint all function args and returns 145 | disallow_untyped_defs = True 146 | 147 | # Unneeded # type: ignore comments. 148 | warn_unused_ignores = True 149 | 150 | # Complain if a function doesn't return anything when it should 151 | warn_no_return = True 152 | 153 | #################### 154 | # Stricter Options 155 | #################### 156 | 157 | # Catch zombie code 158 | warn_unreachable = True 159 | 160 | # Type correctness between variables inside functions 161 | check_untyped_defs = True 162 | 163 | # Complain if a func isn't configured to return 'Any' 164 | warn_return_any = True 165 | 166 | # Check on casting operations 167 | warn_redundant_casts = True 168 | 169 | [mypy-pydot.*] 170 | ignore_missing_imports = True 171 | --------------------------------------------------------------------------------