Skip to content

Commit 9fdcda1

Browse files
committed
Implement minimal Python bindings to SyncActionNode
1 parent 56a8e8b commit 9fdcda1

File tree

5 files changed

+302
-0
lines changed

5 files changed

+302
-0
lines changed

CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON)
2929
option(BTCPP_UNIT_TESTS "Build the unit tests" ON)
3030
option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON)
3131
option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON)
32+
option(BTCPP_PYTHON "Add Python bindings" ON)
3233

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

@@ -134,6 +135,13 @@ if(BTCPP_SQLITE_LOGGING)
134135
list(APPEND BT_SOURCE src/loggers/bt_sqlite_logger.cpp )
135136
endif()
136137

138+
if(BTCPP_PYTHON)
139+
find_package(Python COMPONENTS Interpreter Development)
140+
find_package(pybind11 CONFIG)
141+
pybind11_add_module(btpy_cpp src/python_bindings.cpp)
142+
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})
143+
endif()
144+
137145
######################################################
138146

139147
if (UNIX)

python_examples/btpy.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Top-level module of the BehaviorTree.CPP Python bindings.
5+
"""
6+
7+
from btpy_cpp import * # re-export
8+
9+
10+
def ports(inputs=[], outputs=[]):
11+
"""Decorator to specify input and outputs ports for an action node."""
12+
13+
def specify_ports(cls):
14+
cls.input_ports = list(inputs)
15+
cls.output_ports = list(outputs)
16+
return cls
17+
18+
return specify_ports

python_examples/ex01_sample.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Demo adapted from [1].
5+
6+
To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH`
7+
variable. It is probably located in your build directory if you're building from
8+
source.
9+
10+
[1]: https://github.com/BehaviorTree/btcpp_sample
11+
"""
12+
13+
from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports
14+
15+
16+
xml_text = """
17+
<root BTCPP_format="4" >
18+
19+
<BehaviorTree ID="MainTree">
20+
<Sequence name="root">
21+
<AlwaysSuccess/>
22+
<SaySomething message="this works too" />
23+
<ThinkWhatToSay text="{the_answer}"/>
24+
<SaySomething message="{the_answer}" />
25+
</Sequence>
26+
</BehaviorTree>
27+
28+
</root>
29+
"""
30+
31+
32+
@ports(inputs=["message"])
33+
class SaySomething(SyncActionNode):
34+
def tick(self):
35+
msg = self.get_input("message")
36+
print(msg)
37+
return NodeStatus.SUCCESS
38+
39+
40+
@ports(outputs=["text"])
41+
class ThinkWhatToSay(SyncActionNode):
42+
def tick(self):
43+
self.set_output("text", "The answer is 42")
44+
return NodeStatus.SUCCESS
45+
46+
47+
factory = BehaviorTreeFactory()
48+
factory.register(SaySomething)
49+
factory.register(ThinkWhatToSay)
50+
51+
tree = factory.create_tree_from_text(xml_text)
52+
tree.tick_while_running()

python_examples/ex02_generic_data.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Demonstration of passing generic data between nodes.
5+
6+
To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH`
7+
variable. It is probably located in your build directory if you're building from
8+
source.
9+
"""
10+
11+
import numpy as np
12+
from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports
13+
14+
15+
xml_text = """
16+
<root BTCPP_format="4" >
17+
18+
<BehaviorTree ID="MainTree">
19+
<Sequence name="root">
20+
<AlwaysSuccess/>
21+
<Rotate position="[1.0, 0.0]" theta="90." out="{rotated}" />
22+
<Translate position="{rotated}" offset="[0.1, 0.1]" out="{translated}" />
23+
<Print value="{translated}" />
24+
</Sequence>
25+
</BehaviorTree>
26+
27+
</root>
28+
"""
29+
30+
31+
@ports(inputs=["position", "theta"], outputs=["out"])
32+
class Rotate(SyncActionNode):
33+
def tick(self):
34+
# Build a rotation matrix which rotates points by `theta` degrees.
35+
theta = np.deg2rad(self.get_input("theta"))
36+
c, s = np.cos(theta), np.sin(theta)
37+
M = np.array([[c, -s], [s, c]])
38+
39+
# Apply the rotation to the input position.
40+
position = self.get_input("position")
41+
rotated = M @ position
42+
43+
# Set the output.
44+
self.set_output("out", rotated)
45+
46+
return NodeStatus.SUCCESS
47+
48+
49+
@ports(inputs=["position", "offset"], outputs=["out"])
50+
class Translate(SyncActionNode):
51+
def tick(self):
52+
offset = np.asarray(self.get_input("offset"))
53+
54+
# Apply the translation to the input position.
55+
position = np.asarray(self.get_input("position"))
56+
translated = position + offset
57+
58+
# Set the output.
59+
self.set_output("out", translated)
60+
61+
return NodeStatus.SUCCESS
62+
63+
64+
@ports(inputs=["value"])
65+
class Print(SyncActionNode):
66+
def tick(self):
67+
print(self.get_input("value"))
68+
return NodeStatus.SUCCESS
69+
70+
71+
factory = BehaviorTreeFactory()
72+
factory.register(Rotate)
73+
factory.register(Translate)
74+
factory.register(Print)
75+
76+
tree = factory.create_tree_from_text(xml_text)
77+
tree.tick_while_running()

src/python_bindings.cpp

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#include <memory>
2+
3+
#include <pybind11/pybind11.h>
4+
#include <pybind11/gil.h>
5+
#include <pybind11/eval.h>
6+
#include <pybind11/chrono.h>
7+
#include <pybind11/pytypes.h>
8+
#include <pybind11/stl.h>
9+
10+
#include "behaviortree_cpp/basic_types.h"
11+
#include "behaviortree_cpp/bt_factory.h"
12+
#include "behaviortree_cpp/action_node.h"
13+
#include "behaviortree_cpp/tree_node.h"
14+
15+
namespace BT
16+
{
17+
18+
namespace py = pybind11;
19+
20+
class Py_SyncActionNode : public SyncActionNode
21+
{
22+
public:
23+
Py_SyncActionNode(const std::string& name, const NodeConfig& config) :
24+
SyncActionNode(name, config)
25+
{}
26+
27+
NodeStatus tick() override
28+
{
29+
py::gil_scoped_acquire gil;
30+
return py::get_overload(this, "tick")().cast<NodeStatus>();
31+
}
32+
33+
py::object Py_getInput(const std::string& name)
34+
{
35+
py::object obj;
36+
getInput(name, obj);
37+
return obj;
38+
}
39+
40+
void Py_setOutput(const std::string& name, const py::object& value)
41+
{
42+
setOutput(name, value);
43+
}
44+
};
45+
46+
// Add a conversion specialization from string values into general py::objects
47+
// by evaluating as a Python expression.
48+
template <>
49+
inline py::object convertFromString(StringView str)
50+
{
51+
try
52+
{
53+
// First, try evaluating the string as-is. Maybe it's a number, a list, a
54+
// dict, an object, etc.
55+
return py::eval(str);
56+
}
57+
catch (py::error_already_set& e)
58+
{
59+
// If that fails, then assume it's a string literal with quotation marks
60+
// omitted.
61+
return py::str(str);
62+
}
63+
}
64+
65+
PYBIND11_MODULE(btpy_cpp, m)
66+
{
67+
py::class_<PortInfo>(m, "PortInfo");
68+
m.def("input_port",
69+
[](const std::string& name) { return InputPort<py::object>(name); });
70+
m.def("output_port",
71+
[](const std::string& name) { return OutputPort<py::object>(name); });
72+
73+
m.def(
74+
"ports2",
75+
[](const py::list& inputs, const py::list& outputs) -> auto {
76+
return [](py::type type) -> auto { return type; };
77+
},
78+
py::kw_only(), py::arg("inputs") = py::none(), py::arg("outputs") = py::none());
79+
80+
py::class_<BehaviorTreeFactory>(m, "BehaviorTreeFactory")
81+
.def(py::init())
82+
.def("register",
83+
[](BehaviorTreeFactory& factory, const py::type type) {
84+
const std::string name = type.attr("__name__").cast<std::string>();
85+
86+
TreeNodeManifest manifest;
87+
manifest.type = NodeType::ACTION;
88+
manifest.registration_ID = name;
89+
manifest.ports = {};
90+
manifest.description = "";
91+
92+
const auto input_ports = type.attr("input_ports").cast<py::list>();
93+
for (const auto& name : input_ports)
94+
{
95+
manifest.ports.insert(InputPort<py::object>(name.cast<std::string>()));
96+
}
97+
98+
const auto output_ports = type.attr("output_ports").cast<py::list>();
99+
for (const auto& name : output_ports)
100+
{
101+
manifest.ports.insert(OutputPort<py::object>(name.cast<std::string>()));
102+
}
103+
104+
factory.registerBuilder(
105+
manifest,
106+
[type](const std::string& name,
107+
const NodeConfig& config) -> std::unique_ptr<TreeNode> {
108+
py::object obj = type(name, config);
109+
// TODO: Increment the object's reference count or else it
110+
// will be GC'd at the end of this scope. The downside is
111+
// that, unless we can decrement the ref when the unique_ptr
112+
// is destroyed, then the object will live forever.
113+
obj.inc_ref();
114+
115+
return std::unique_ptr<Py_SyncActionNode>(
116+
obj.cast<Py_SyncActionNode*>());
117+
});
118+
})
119+
.def("create_tree_from_text",
120+
[](BehaviorTreeFactory& factory, const std::string& text) -> Tree {
121+
return factory.createTreeFromText(text);
122+
});
123+
124+
py::class_<Tree>(m, "Tree")
125+
.def("tick_once", &Tree::tickOnce)
126+
.def("tick_exactly_once", &Tree::tickExactlyOnce)
127+
.def("tick_while_running", &Tree::tickWhileRunning,
128+
py::arg("sleep_time") = std::chrono::milliseconds(10));
129+
130+
py::enum_<NodeStatus>(m, "NodeStatus")
131+
.value("SUCCESS", NodeStatus::SUCCESS)
132+
.value("FAILURE", NodeStatus::FAILURE)
133+
.value("IDLE", NodeStatus::IDLE)
134+
.value("RUNNING", NodeStatus::RUNNING)
135+
.value("SKIPPED", NodeStatus::SKIPPED)
136+
.export_values();
137+
138+
py::class_<NodeConfig>(m, "NodeConfig");
139+
140+
py::class_<Py_SyncActionNode>(m, "SyncActionNode")
141+
.def(py::init<const std::string&, const NodeConfig&>())
142+
.def("tick", &Py_SyncActionNode::tick)
143+
.def("get_input", &Py_SyncActionNode::Py_getInput)
144+
.def("set_output", &Py_SyncActionNode::Py_setOutput);
145+
}
146+
147+
} // namespace BT

0 commit comments

Comments
 (0)