py_branches
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)
| File | Size | Uploaded | |
|---|---|---|---|
| py_branches-1.7.0.tar.gz | 51.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|