Skip to main content

py_branches

CI codecov PyPI Docs

Reusable behavior-tree behaviors and decorators for Python, built on py_trees.

py_branches provides higher-level functionality designed to sit on top of the py_trees library. py_trees supplies the machinery for behavior trees; this package supplies the patterns that otherwise get rewritten on top of it — alternating execution, probabilistic selection, blackboard-driven conditionals, cooldowns, run caps, latching, retries, timeouts, and time-based pausing.

Installation

From PyPI:

pip install py_branches

From source (editable):

git clone https://github.com/snwu1996/py_branches.git
cd py_branches
pip install -e .

Modules

Module Description
alternating Cycle through behaviors in fixed patterns, or run a child every N ticks
blackboard Read, write, and gate execution on py_trees blackboard variables
clock Injectable time sources, so timed behaviors are testable
cooldown Enforce a minimum time gap between runs of a child
counter Cap the total number of times a child runs
latch Make a child's first SUCCESS permanent
pause Time-based pauses — random, sampled, keyboard, or YAML-scheduled
random Probabilistic execution and weighted random selection
retry Re-run a child that fails, optionally with a delay
runtime Run a tree as a process — rate, signals, exit codes, teardown
surgery Edit a built tree — walk, find, replace, prune, graft, reset
timeout Fail a child that stays RUNNING too long
visitors Log status transitions and time spent RUNNING

Basic Usage

Alternating — cycle through behaviors

import py_trees
from py_branches.alternating import run_alternating

a = py_trees.behaviours.Success(name="A")
b = py_trees.behaviours.Success(name="B")
c = py_trees.behaviours.Success(name="C")

# Run A for 3 ticks, then B for 2 ticks, then C for 4 ticks, then repeat
root = run_alternating("Alternating", [a, b, c], [3, 2, 4])

Alternating — run a child every N ticks

from py_branches.alternating import RunEveryX, RunEveryRange

child = py_trees.behaviours.Success(name="Child")

# Run child every 5th tick
every_5 = RunEveryX(child, name="Every5", every_x_range=(5, 5))

# Run child only on iterations 4–6 out of every 10
windowed = RunEveryRange(child, name="Window", max_range=10, run_range=(4, 6))

Blackboard — conditional execution and variable management

import py_trees
from py_branches.blackboard import (
    IncrementBlackboardVariable,
    RunIfBlackboardVariableEquals,
    SetBlackboardVariableIfCondition,
)

# Set up blackboard
py_trees.blackboard.Blackboard.enable_activity_stream()
client = py_trees.blackboard.Client(name="setup")
client.register_key("counter", access=py_trees.common.Access.WRITE)
client.counter = 0

# Increment a blackboard counter each tick
increment = IncrementBlackboardVariable(
    name="Increment", variable_name="counter", increment_by=1
)

# Only run a child behavior when counter == 5
child = py_trees.behaviours.Success(name="AtFive")
gate = RunIfBlackboardVariableEquals(
    child, name="RunAt5", variable_name="counter", equals=5
)

Pause — random duration pause

from py_branches.pause import PauseUniform

# Pause for a random duration between 1.0 and 3.0 seconds
pause = PauseUniform(name="RandomPause", low=1.0, high=3.0)

Pause — schedule-based pause

from py_branches.pause import load_schedule_file, PauseSchedule

schedule = load_schedule_file("configs/schedules/example_schedule.yaml")
if schedule is None:
    raise SystemExit("schedule file is empty")

# Pauses until the current scheduled window ends; SUCCESS immediately if
# outside all windows.
pause = PauseSchedule(name="PauseSchedule", schedule=schedule)

# As a Selector child that interrupts the work after it: FAILURE (fall
# through) outside all windows, RUNNING during one.
pause = PauseSchedule(name="PauseSchedule", schedule=schedule, fail_outside_window=True)

Random — probabilistic execution

import py_trees
from py_branches.random import RandomRun, random_selector

child = py_trees.behaviours.Success(name="Child")

# Execute child with 70% probability; return FAILURE otherwise
maybe = RandomRun(child, name="Maybe", probability=0.7)

# Weighted random selector: a=20%, b=30%, c=50%
a = py_trees.behaviours.Success(name="A")
b = py_trees.behaviours.Success(name="B")
c = py_trees.behaviours.Success(name="C")
selector = random_selector("WeightedSel", [a, b, c], [0.2, 0.3, 0.5])

Cooldown, Counter, Latch — limit how often a child runs

import py_trees
from py_branches.cooldown import Cooldown
from py_branches.counter import Counter
from py_branches.latch import Latch

child = py_trees.behaviours.Success(name="Child")

# At most one run every 10 seconds; FAILURE while still cooling down
cooled = Cooldown(child, name="Cooldown", duration=10.0)

# Run at most 3 times, then report SUCCESS without ticking the child again
capped = Counter(child, name="Counter", num_runs=3)

# Once the child succeeds, keep reporting SUCCESS forever
latched = Latch(child, name="Latch")

Retry and Timeout — bound failure and duration

from py_branches.retry import Retry
from py_branches.timeout import Timeout

# Re-run on FAILURE up to 3 attempts, waiting 0.5s between them
retried = Retry(child, name="Retry", max_attempts=3, delay=0.5)

# FAILURE if the child stays RUNNING for more than 2 seconds
bounded = Timeout(child, name="Timeout", duration=2.0)

Surgery — edit a tree after it is built

from py_branches import surgery
from py_branches.pause import PauseUniform, PauseUntilKey

# Replace every real pause with "press space to continue", including the ones
# hiding under decorators - a plain isinstance(node, Composite) walk misses
# those, because a py_trees Decorator is not a Composite.
swapped = surgery.swap_type(
    tree.root,
    (PauseUniform,),
    lambda old: PauseUntilKey(name=old.name, key="space"),
)

surgery.find_by_name(tree.root, "random_break_pause")
surgery.replace(old_node, new_node)  # re-parents, and fixes decorator.decorated
surgery.reset_subtree(tree.root)  # re-arm every Latch and Counter

Visitors — see what the tree actually did

from py_branches.visitors import StatusTransitionVisitor, TimerVisitor

tree = py_trees.trees.BehaviourTree(root=child)

# Log a line only when a leaf changes status, and time every RUNNING stretch
tree.visitors.append(StatusTransitionVisitor())
tree.visitors.append(TimerVisitor())

Clock — test timing without sleeping

Every behavior that measures elapsed time takes a keyword-only clock. It defaults to the real one, so production trees need not pass it; tests hand it a ManualClock and step time forward instead of waiting.

from py_branches.clock import ManualClock
from py_branches.cooldown import Cooldown

clock = ManualClock()
cooled = Cooldown(child, name="Cooldown", duration=5.0, clock=clock)

cooled.tick_once()  # runs
cooled.tick_once()  # cooling, child not ticked
clock.advance(5.0)  # no real time passes
cooled.tick_once()  # re-armed, exactly on the boundary

Running Tests

pytest tests/

Documentation

Full documentation, including an API reference generated from the source and rendered behavior-tree diagrams, is at py-branches.readthedocs.io.

To build it locally:

poetry install --with docs
poetry run sphinx-build -b html docs docs/_build/html

Rendering the diagrams needs the dot binary (apt install graphviz).

License

BSD License. See LICENSE for details.

Metadata

Release files for py_branches 1.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py_branches 1.7.0
File Size Uploaded
py_branches-1.7.0.tar.gz 51.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py_branches 1.7.0
File Interpreter ABI Platform
py_branches-1.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.4 kB

Release files / py_branches-1.7.0.tar.gz

Download URL py_branches-1.7.0.tar.gz
Size 51.1 kB
Tags Source
SHA-256 checksum
How to use checksums
205bc984e599b7443429d73461ef4f9bfc2f1b08954a7b59d6ffa1ae8ae4a7b8
BLAKE2b-256 checksum
How to use checksums
53c925206aa85603203d98c9b0db6f547891934177fd863dab974816fa881634
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.2 CPython/3.12.14 Linux/6.17.0-1022-azure

Release files / py_branches-1.7.0-py3-none-any.whl

Download URL py_branches-1.7.0-py3-none-any.whl
Size 56.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e341500e576cedc835c5b7650b86809f9e2456939b0d8903634e0fccdc659f30
BLAKE2b-256 checksum
How to use checksums
7d523fa8bc0e4fddd25362621b465837bbfc5d081f0a3b4f13887ba2902bed6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.2 CPython/3.12.14 Linux/6.17.0-1022-azure

Release history Release notifications | RSS feed

1.8.0

2 release files

This release

1.7.0 This release

2 release files

1.6.0

2 release files

1.5.3

2 release files

1.5.2

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page