Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Python bindings #634

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9fdcda1
Implement minimal Python bindings to SyncActionNode
kylc Aug 13, 2023
f560500
Add stateful action bindings.
kylc Aug 13, 2023
1f9db33
Eliminate some code duplication.
kylc Aug 13, 2023
351c33a
Use proper PYBIND11_OVERRIDE macros.
kylc Aug 13, 2023
4ff5673
Export minimal set of identifiers to Python lib.
kylc Aug 13, 2023
17da541
Clean up port handling.
kylc Aug 13, 2023
b22772e
Put generic methods on abstract base class.
kylc Aug 13, 2023
6188c21
Ignore pycache.
kylc Aug 13, 2023
9493f10
Return None if blackboard value doesn't exist.
kylc Aug 13, 2023
404a195
Add builder args to be passed to node ctors.
kylc Aug 13, 2023
adf5cef
Add ROS2 interop example.
kylc Aug 13, 2023
37ae114
Add simple README.
kylc Aug 13, 2023
2a22fa8
Add note about Py_getInput return value.
kylc Aug 13, 2023
ed78e84
Fix NodeStatus enum value ordering.
kylc Aug 13, 2023
dc9e953
Fix typo in ex04.
kylc Aug 13, 2023
712370b
Add useful command for ex04.
kylc Aug 13, 2023
7927e67
Fix onHalted override copy-paste error.
kylc Aug 19, 2023
cfa553a
Disable zero variadic arg warning.
kylc Aug 19, 2023
d1fe0e3
Implement C++ <-> Python type interop via JSON.
kylc Aug 20, 2023
21d450e
Add `BehaviorTreeFactory.register_from_plugin` binding.
kylc Aug 20, 2023
4448376
Add pybind11 conan dependency.
kylc Aug 20, 2023
c703efd
Implement coroutine-based Python nodes.
kylc Aug 20, 2023
83caef7
Add missing pybind11 dependency to package.xml
kylc Sep 2, 2023
0e35ac0
Move some dummy_nodes definitions to cpp file to fix linker error
kylc Sep 2, 2023
1a69d3a
Clean up Python ex06.
kylc Aug 27, 2023
2c1b18a
Use docstring as tree node description.
kylc Sep 2, 2023
fdc2232
Add pyproject.toml/setup.py for building wheels.
kylc Sep 2, 2023
ee7f464
Modify py::type argument to support older pybind
kylc Sep 2, 2023
9d8db3c
Clean up Python example XMLs.
kylc Sep 3, 2023
46929a8
Move Python-related source files into subdirectory.
kylc Sep 3, 2023
84ae12d
Add some type hints to the Python code
kylc Sep 4, 2023
4ad738c
Add some docs to Python ex06.
kylc Sep 4, 2023
0786475
Don't make Py_StatefulActionNode final.
kylc Sep 5, 2023
0ee0a20
Fix some string-embedded XML indentation.
kylc Sep 5, 2023
93b58c3
Formatting.
kylc Sep 5, 2023
535ea88
Improve python example README
kylc Sep 5, 2023
04f435d
Add `halt_tree` binding and use in demo
kylc Sep 5, 2023
2584aec
Add default impl of AsyncActionNode#on_halted
kylc Sep 5, 2023
b425c91
Add docs for `JsonExporter::fromJson`.
kylc Sep 5, 2023
1a7ac0a
Properly specify __all__ for btpy module.
kylc Sep 6, 2023
632eb66
Clean up dummy node use in ex05.
kylc Sep 6, 2023
fb33788
Add useful note for ex05 on shared lib location.
kylc Sep 6, 2023
890396c
Fix setup.py package attributes.
kylc Sep 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build*
site/*
/.vscode/
.vs/
__pycache__

# clangd cache
/.cache/*
Expand Down
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON)
option(BTCPP_UNIT_TESTS "Build the unit tests" ON)
option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON)
option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON)
option(BTCPP_PYTHON "Add Python bindings" ON)

option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF)

Expand Down Expand Up @@ -134,6 +135,13 @@ if(BTCPP_SQLITE_LOGGING)
list(APPEND BT_SOURCE src/loggers/bt_sqlite_logger.cpp )
endif()

if(BTCPP_PYTHON)
find_package(Python COMPONENTS Interpreter Development)
find_package(pybind11 CONFIG)
pybind11_add_module(btpy_cpp src/python_bindings.cpp)
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})
endif()

######################################################

if (UNIX)
Expand Down
4 changes: 4 additions & 0 deletions python_examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1. Ensure that BehaviorTree.CPP is build with `BTCPP_PYTHON=ON`.
2. Add the build directory containing the `btpy_cpp.*.so` Python extension to
your `PYTHONPATH`.
3. Run an example, e.g. `python3 ex01_sample.py`
25 changes: 25 additions & 0 deletions python_examples/btpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3

"""
Top-level module of the BehaviorTree.CPP Python bindings.
"""

# re-export
from btpy_cpp import (
BehaviorTreeFactory,
NodeStatus,
StatefulActionNode,
SyncActionNode,
Tree,
)


def ports(inputs=[], outputs=[]):
"""Decorator to specify input and outputs ports for an action node."""

def specify_ports(cls):
cls.input_ports = list(inputs)
cls.output_ports = list(outputs)
return cls

return specify_ports
46 changes: 46 additions & 0 deletions python_examples/ex01_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3

"""
Demo adapted from [btcpp_sample](https://github.com/BehaviorTree/btcpp_sample).
"""

from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports


xml_text = """
<root BTCPP_format="4" >

<BehaviorTree ID="MainTree">
<Sequence name="root">
<AlwaysSuccess/>
<SaySomething message="this works too" />
<ThinkWhatToSay text="{the_answer}"/>
<SaySomething message="{the_answer}" />
</Sequence>
</BehaviorTree>

</root>
"""


@ports(inputs=["message"])
class SaySomething(SyncActionNode):
def tick(self):
msg = self.get_input("message")
print(msg)
return NodeStatus.SUCCESS


@ports(outputs=["text"])
class ThinkWhatToSay(SyncActionNode):
def tick(self):
self.set_output("text", "The answer is 42")
return NodeStatus.SUCCESS


factory = BehaviorTreeFactory()
factory.register(SaySomething)
factory.register(ThinkWhatToSay)

tree = factory.create_tree_from_text(xml_text)
tree.tick_while_running()
75 changes: 75 additions & 0 deletions python_examples/ex02_generic_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3

"""
Demonstration of passing generic data between nodes.
"""

import numpy as np
from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports


xml_text = """
<root BTCPP_format="4" >

<BehaviorTree ID="MainTree">
<Sequence name="root">
<AlwaysSuccess/>
<Rotate position="[1.0, 0.0]" theta="90." out="{rotated}" />
<Translate position="{rotated}" offset="[0.1, 0.1]" out="{translated}" />
<Print value="{translated}" />
</Sequence>
</BehaviorTree>

</root>
"""


@ports(inputs=["position", "theta"], outputs=["out"])
class Rotate(SyncActionNode):
def tick(self):
# Build a rotation matrix which rotates points by `theta` degrees.
theta = np.deg2rad(self.get_input("theta"))
c, s = np.cos(theta), np.sin(theta)
M = np.array([[c, -s], [s, c]])

# Apply the rotation to the input position.
position = self.get_input("position")
rotated = M @ position

# Set the output.
self.set_output("out", rotated)

return NodeStatus.SUCCESS


@ports(inputs=["position", "offset"], outputs=["out"])
class Translate(SyncActionNode):
def tick(self):
offset = np.asarray(self.get_input("offset"))

# Apply the translation to the input position.
position = np.asarray(self.get_input("position"))
translated = position + offset

# Set the output.
self.set_output("out", translated)

return NodeStatus.SUCCESS


@ports(inputs=["value"])
class Print(SyncActionNode):
def tick(self):
value = self.get_input("value")
if value is not None:
print(value)
return NodeStatus.SUCCESS


factory = BehaviorTreeFactory()
factory.register(Rotate)
factory.register(Translate)
factory.register(Print)

tree = factory.create_tree_from_text(xml_text)
tree.tick_while_running()
70 changes: 70 additions & 0 deletions python_examples/ex03_stateful_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3

"""
Demonstration of stateful action nodes.
"""

import numpy as np
from btpy import (
BehaviorTreeFactory,
StatefulActionNode,
SyncActionNode,
NodeStatus,
ports,
)


xml_text = """
<root BTCPP_format="4" >

<BehaviorTree ID="MainTree">
<Sequence>
<!-- Interpolate from the initial position to the final one printing
at each step. -->
<ReactiveSequence name="root">
<Print value="{interpolated}" />
<Interpolate x0="[1.0, 0.0]" x1="[0.0, 1.0]" out="{interpolated}" />
</ReactiveSequence>
</Sequence>
</BehaviorTree>

</root>
"""


@ports(inputs=["x0", "x1"], outputs=["out"])
class Interpolate(StatefulActionNode):
def on_start(self):
self.t = 0.0
self.x0 = np.asarray(self.get_input("x0"))
self.x1 = np.asarray(self.get_input("x1"))
return NodeStatus.RUNNING

def on_running(self):
if self.t < 1.0:
x = (1.0 - self.t) * self.x0 + self.t * self.x1
self.set_output("out", x)
self.t += 0.1
return NodeStatus.RUNNING
else:
return NodeStatus.SUCCESS

def on_halted(self):
pass


@ports(inputs=["value"])
class Print(SyncActionNode):
def tick(self):
value = self.get_input("value")
if value is not None:
print(value)
return NodeStatus.SUCCESS


factory = BehaviorTreeFactory()
factory.register(Interpolate)
factory.register(Print)

tree = factory.create_tree_from_text(xml_text)
tree.tick_while_running()
89 changes: 89 additions & 0 deletions python_examples/ex04_ros_interop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

"""
Demonstrates interop of BehaviorTree.CPP Python bindings and ROS2 via rclpy.

You can publish the transform expected in the tree below using this command:

ros2 run tf2_ros static_transform_publisher \
--frame-id odom --child-frame-id base_link \
--x 1.0 --y 2.0
"""

import rclpy
from rclpy.node import Node
from tf2_ros.buffer import Buffer
from tf2_ros.transform_listener import TransformListener

from btpy import (
BehaviorTreeFactory,
StatefulActionNode,
SyncActionNode,
NodeStatus,
ports,
)


xml_text = """
<root BTCPP_format="4" >

<BehaviorTree ID="MainTree">
<Sequence>
<ReactiveSequence name="root">
<Print value="{tf}" />
<GetRosTransform frame_id="odom" child_frame_id="base_link" tf="{tf}" />
</ReactiveSequence>
</Sequence>
</BehaviorTree>

</root>
"""


@ports(inputs=["frame_id", "child_frame_id"], outputs=["tf"])
class GetRosTransform(StatefulActionNode):
def __init__(self, name, config, node):
super().__init__(name, config)

self.node = node
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self.node)

def on_start(self):
return NodeStatus.RUNNING

def on_running(self):
frame_id = self.get_input("frame_id")
child_frame_id = self.get_input("child_frame_id")

time = self.node.get_clock().now()
if self.tf_buffer.can_transform(frame_id, child_frame_id, time):
tf = self.tf_buffer.lookup_transform(frame_id, child_frame_id, time)
self.set_output("tf", tf)

return NodeStatus.RUNNING

def on_halted(self):
pass


@ports(inputs=["value"])
class Print(SyncActionNode):
def tick(self):
value = self.get_input("value")
if value is not None:
print(value)
return NodeStatus.SUCCESS


rclpy.init()
node = Node("ex04_ros_interop")

factory = BehaviorTreeFactory()
factory.register(GetRosTransform, node)
factory.register(Print)

tree = factory.create_tree_from_text(xml_text)

node.create_timer(0.01, lambda: tree.tick_once())
rclpy.spin(node)
Loading