diff --git a/WORKSPACE b/WORKSPACE index 93d50750d5a7..5b7464513f08 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -280,8 +280,8 @@ github_archive( github_archive( name = "pybind11", repository = "RobotLocomotion/pybind11", - commit = "ffcf754ae9e766632610975d22372a86a7b63014", - sha256 = "7cd6f4efb02bf9ae17eeb2afba68023af913e61ae76e8b4254203d0eec019525", # noqa + commit = "48999b69bde29cdf8d616d4fbd3d6ab1c561027d", + sha256 = "2ea18adfb608948cab1b5978081dc8c318ed47573ccd66f1603a37fbdbfc56da", # noqa build_file = "tools/workspace/pybind11/pybind11.BUILD.bazel", ) diff --git a/bindings/pydrake/BUILD.bazel b/bindings/pydrake/BUILD.bazel index a49cb3952971..c96951aaf4b9 100644 --- a/bindings/pydrake/BUILD.bazel +++ b/bindings/pydrake/BUILD.bazel @@ -116,13 +116,16 @@ PYBIND_LIBRARIES = adjust_labels_for_drake_hoist([ ":rbtree_py", ":symbolic_py", "//drake/bindings/pydrake/solvers", + "//drake/bindings/pydrake/systems", ]) +PY_LIBRARIES = [ + ":util_py", +] + install( name = "install", - targets = [ - ":util_py", - ], + targets = PY_LIBRARIES, py_dest = get_pybind_library_dest(), visibility = ["//visibility:public"], deps = get_drake_pybind_installs(PYBIND_LIBRARIES), @@ -131,7 +134,7 @@ install( drake_py_library( name = "pydrake", visibility = ["//visibility:public"], - deps = PYBIND_LIBRARIES, + deps = PYBIND_LIBRARIES + PY_LIBRARIES, ) # Test ODR (One Definition Rule). diff --git a/bindings/pydrake/solvers/BUILD.bazel b/bindings/pydrake/solvers/BUILD.bazel index deccf05e5ed2..ee6d54aede8d 100644 --- a/bindings/pydrake/solvers/BUILD.bazel +++ b/bindings/pydrake/solvers/BUILD.bazel @@ -81,18 +81,18 @@ PYBIND_LIBRARIES = [ ":mosek_py", ] +PY_LIBRARIES = [ + ":module_py", +] + drake_py_library( name = "solvers", - deps = PYBIND_LIBRARIES + [ - ":module_py", - ], + deps = PYBIND_LIBRARIES + PY_LIBRARIES, ) install( name = "install", - targets = [ - ":module_py", - ], + targets = PY_LIBRARIES, py_dest = get_pybind_library_dest(), deps = get_drake_pybind_installs(PYBIND_LIBRARIES), ) diff --git a/bindings/pydrake/solvers/__init__.py b/bindings/pydrake/solvers/__init__.py index e69de29bb2d1..8df4b86ebc5a 100644 --- a/bindings/pydrake/solvers/__init__.py +++ b/bindings/pydrake/solvers/__init__.py @@ -0,0 +1 @@ +# Blank Python module. diff --git a/bindings/pydrake/systems/BUILD.bazel b/bindings/pydrake/systems/BUILD.bazel new file mode 100644 index 000000000000..dbbce7c7740c --- /dev/null +++ b/bindings/pydrake/systems/BUILD.bazel @@ -0,0 +1,167 @@ +# -*- python -*- + +load("@drake//tools/install:install.bzl", "install") +load("//tools/lint:lint.bzl", "add_lint_tests") +load( + "//tools/skylark:pybind.bzl", + "drake_pybind_library", + "get_drake_pybind_installs", + "get_pybind_library_dest", +) +load( + "//tools/skylark:drake_py.bzl", + "drake_py_binary", + "drake_py_library", + "drake_py_test", +) +load("//tools/skylark:6996.bzl", "adjust_labels_for_drake_hoist") + +package(default_visibility = adjust_labels_for_drake_hoist([ + "//drake/bindings/pydrake:__subpackages__", +])) + +# @note Symbols are NOT imported directly into +# `__init__.py` to simplify dependency management, meaning that +# classes are organized by their directory structure rather than +# by C++ namespace. If you want all symbols, use `all.py`. +drake_py_library( + name = "module_py", + srcs = ["__init__.py"], + deps = [ + "//drake/bindings/pydrake:common_py", + ], +) + +drake_pybind_library( + name = "framework_py", + cc_so_name = "framework", + cc_srcs = ["framework_py.cc"], + py_deps = [ + ":module_py", + ], +) + +drake_pybind_library( + name = "primitives_py", + cc_so_name = "primitives", + cc_srcs = ["primitives_py.cc"], + py_deps = [ + ":framework_py", + ":module_py", + ], +) + +drake_pybind_library( + name = "analysis_py", + cc_so_name = "analysis", + cc_srcs = ["analysis_py.cc"], + py_deps = [ + ":framework_py", + ":module_py", + ], +) + +drake_py_library( + name = "drawing_py", + srcs = ["drawing.py"], + deps = [":module_py"], + # TODO(eric.cousineau): Expose information to allow `imports = ...` to be + # defined, rather than rely on `module_py`. +) + +drake_py_library( + name = "all_py", + deps = [ + ":analysis_py", + ":drawing_py", + ":framework_py", + ":primitives_py", + ], +) + +PYBIND_LIBRARIES = [ + ":analysis_py", + ":framework_py", + ":primitives_py", +] + +PY_LIBRARIES = [ + ":all_py", + ":drawing_py", + ":module_py", +] + +drake_py_library( + name = "systems", + deps = PYBIND_LIBRARIES + PY_LIBRARIES, +) + +install( + name = "install", + targets = PY_LIBRARIES, + py_dest = get_pybind_library_dest(), + deps = get_drake_pybind_installs(PYBIND_LIBRARIES), +) + +drake_py_test( + name = "general_test", + size = "small", + deps = [ + ":analysis_py", + ":framework_py", + ":primitives_py", + ], +) + +# TODO(eric.cousineau): Convert this to a workflow test once `pydot` is added +# to `install_prereqs.sh`. +drake_py_binary( + name = "graphviz_example", + srcs = ["test/graphviz_example.py"], + deps = [ + ":drawing_py", + ":framework_py", + ":primitives_py", + ], +) + +drake_pybind_library( + name = "lifetime_test_util", + testonly = 1, + add_install = False, + cc_so_name = "test/lifetime_test_util", + cc_srcs = ["test/lifetime_test_util_py.cc"], + py_deps = [ + ":primitives_py", + ], +) + +drake_py_test( + name = "lifetime_test", + deps = [ + ":analysis_py", + ":framework_py", + ":lifetime_test_util", + ":primitives_py", + ], +) + +drake_py_test( + name = "custom_test", + size = "small", + deps = [ + ":analysis_py", + ":framework_py", + ":primitives_py", + ], +) + +drake_py_test( + name = "vector_test", + size = "small", + deps = [ + ":framework_py", + ], +) + +add_lint_tests() diff --git a/bindings/pydrake/systems/__init__.py b/bindings/pydrake/systems/__init__.py new file mode 100644 index 000000000000..8df4b86ebc5a --- /dev/null +++ b/bindings/pydrake/systems/__init__.py @@ -0,0 +1 @@ +# Blank Python module. diff --git a/bindings/pydrake/systems/all.py b/bindings/pydrake/systems/all.py new file mode 100644 index 000000000000..6370a8a99263 --- /dev/null +++ b/bindings/pydrake/systems/all.py @@ -0,0 +1,8 @@ +from .analysis import * +from .framework import * +from .primitives import * + +try: + from .drawing import * +except ImportError: + pass diff --git a/bindings/pydrake/systems/analysis_py.cc b/bindings/pydrake/systems/analysis_py.cc new file mode 100644 index 000000000000..a38543148931 --- /dev/null +++ b/bindings/pydrake/systems/analysis_py.cc @@ -0,0 +1,26 @@ +#include + +#include "drake/systems/analysis/simulator.h" + +namespace py = pybind11; + +using std::unique_ptr; + +PYBIND11_MODULE(analysis, m) { + // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. + using namespace drake::systems; + + auto py_iref = py::return_value_policy::reference_internal; + + m.doc() = "Bindings for the analysis portion of the Systems framework."; + + using T = double; + + py::class_>(m, "Simulator") + .def(py::init&>()) + .def(py::init&, unique_ptr>>()) + .def("Initialize", &Simulator::Initialize) + .def("StepTo", &Simulator::StepTo) + .def("get_context", &Simulator::get_context, py_iref) + .def("get_mutable_context", &Simulator::get_mutable_context, py_iref); +} diff --git a/bindings/pydrake/systems/drawing.py b/bindings/pydrake/systems/drawing.py new file mode 100644 index 000000000000..c176692a87bf --- /dev/null +++ b/bindings/pydrake/systems/drawing.py @@ -0,0 +1,40 @@ +""" +Provides general visualization utilities. This is NOT related to `rendering`. +@note This is an optional module, dependent on `pydot` and `matplotlib` being +installed. +""" + +from StringIO import StringIO + +import matplotlib.image as mpimg +import matplotlib.pyplot as plt +import pydot + + +# TODO(eric.cousineau): Move `plot_graphviz` to something more accessible to +# `call_python_client`. + + +def plot_graphviz(dot_text): + """Renders a DOT graph in matplotlib.""" + # @ref https://stackoverflow.com/a/18522941/7829525 + # Tried (reason ignored): pydotplus (`pydot` works), networkx + # (`read_dot` does not work robustly?), pygraphviz (coupled with + # `networkx`). + g = pydot.graph_from_dot_data(dot_text) + if isinstance(g, list): + # Per Ioannis's follow-up comment in the above link, in pydot >= 1.2.3 + # `graph_from_dot_data` returns a list of graphs. + # Handle this case for now. + assert len(g) == 1 + g = g[0] + s = StringIO() + g.write_png(s) + s.seek(0) + plt.axis('off') + return plt.imshow(plt.imread(s), aspect="equal") + + +def plot_system_graphviz(system): + """Renders a System's Graphviz representation in `matplotlib`. """ + return plot_graphviz(system.GetGraphvizString()) diff --git a/bindings/pydrake/systems/framework_py.cc b/bindings/pydrake/systems/framework_py.cc new file mode 100644 index 000000000000..0a84b74635e8 --- /dev/null +++ b/bindings/pydrake/systems/framework_py.cc @@ -0,0 +1,222 @@ +#include +#include +#include +#include +#include + +#include "drake/systems/framework/abstract_values.h" +#include "drake/systems/framework/basic_vector.h" +#include "drake/systems/framework/context.h" +#include "drake/systems/framework/diagram.h" +#include "drake/systems/framework/diagram_builder.h" +#include "drake/systems/framework/leaf_context.h" +#include "drake/systems/framework/leaf_system.h" +#include "drake/systems/framework/output_port_value.h" +#include "drake/systems/framework/subvector.h" +#include "drake/systems/framework/supervector.h" +#include "drake/systems/framework/system.h" + +namespace py = pybind11; + +using std::make_unique; +using std::unique_ptr; +using std::vector; + +PYBIND11_MODULE(framework, m) { + // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. + using namespace drake; + // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. + using namespace drake::systems; + + // Aliases for commonly used return value policies. + // `py_ref` is used when `keep_alive` is explicitly used (e.g. for extraction + // methods, like `GetMutableSubsystemState`). + auto py_ref = py::return_value_policy::reference; + // `py_iref` is used when pointers / lvalue references are returned (no need + // for `keep_alive`, as it is implicit. + auto py_iref = py::return_value_policy::reference_internal; + + m.doc() = "Bindings for the core Systems framework."; + + // TODO(eric.cousineau): At present, we only bind doubles. + // In the future, we will bind more scalar types, and enable scalar + // conversion. + using T = double; + + // TODO(eric.cousineau): Resolve `str_py` workaround. + auto str_py = py::eval("str"); + + py::setattr(m, "kAutoSize", py::cast(kAutoSize)); + + py::enum_(m, "PortDataType") + .value("kVectorValued", kVectorValued) + .value("kAbstractValued", kAbstractValued); + + class PySystem : public py::wrapper> { + public: + using Base = py::wrapper>; + using Base::Base; + // Expose protected methods for binding. + using Base::DeclareInputPort; + }; + + // TODO(eric.cousineau): Show constructor, but somehow make sure `pybind11` + // knows this is abstract? + py::class_, PySystem>(m, "System") + .def("set_name", &System::set_name) + // Topology. + .def("get_input_port", &System::get_input_port, py_iref) + .def("get_output_port", &System::get_output_port, py_iref) + .def( + "_DeclareInputPort", + [](PySystem* self, PortDataType arg1, int arg2) -> auto&& { + return self->DeclareInputPort(arg1, arg2); + }, py_iref) + // Context. + .def("CreateDefaultContext", &System::CreateDefaultContext) + .def("AllocateOutput", &System::AllocateOutput) + .def( + "EvalVectorInput", + [](const System* self, const Context& arg1, int arg2) { + return self->EvalVectorInput(arg1, arg2); + }, py_iref) + .def("CalcOutput", &System::CalcOutput) + // Sugar. + .def( + "GetGraphvizString", + [str_py](const System* self) { + // @note This is a workaround; for some reason, + // casting this using `py::str` does not work, but directly + // calling the Python function (`str_py`) does. + return str_py(self->GetGraphvizString()); + }); + + class PyLeafSystem : public py::wrapper> { + public: + using Base = py::wrapper>; + using Base::Base; + // Expose protected methods for binding. + using Base::DeclareVectorOutputPort; + }; + + // Don't use a const-rvalue as a function handle parameter, as pybind11 wants + // to copy it? + // TODO(eric.cousineau): Make a helper wrapper for this; file a bug in + // pybind11 (since these are arguments). + using CalcVectorPtrCallback = + std::function*, BasicVector*)>; + + py::class_, PyLeafSystem, System>(m, "LeafSystem") + .def(py::init<>()) + .def( + "_DeclareVectorOutputPort", + [](PyLeafSystem* self, const BasicVector& arg1, + CalcVectorPtrCallback arg2) -> auto&& { + typename LeafOutputPort::CalcVectorCallback wrapped = + [arg2](const Context& nest_arg1, BasicVector* nest_arg2) { + return arg2(&nest_arg1, nest_arg2); + }; + return self->DeclareVectorOutputPort(arg1, wrapped); + }, py_iref); + + py::class_>(m, "Context") + .def("get_num_input_ports", &Context::get_num_input_ports) + .def("FixInputPort", + py::overload_cast>>( + &Context::FixInputPort)) + .def("get_time", &Context::get_time) + .def("Clone", &Context::Clone) + .def("__copy__", &Context::Clone) + .def("get_state", &Context::get_state, py_iref) + .def("get_mutable_state", &Context::get_mutable_state, py_iref); + + py::class_, Context>(m, "LeafContext"); + + py::class_, System>(m, "Diagram") + .def("GetMutableSubsystemState", + [](Diagram* self, const System& arg1, Context* arg2) + -> auto&& { + // @note Use `auto&&` to get perfect forwarding. + // @note Compiler does not like `py::overload_cast` with this setup? + return self->GetMutableSubsystemState(arg1, arg2); + }, py_ref, py::keep_alive<0, 3>()); + + // Glue mechanisms. + py::class_>(m, "DiagramBuilder") + .def(py::init<>()) + .def( + "AddSystem", + [](DiagramBuilder* self, unique_ptr> arg1) { + return self->AddSystem(std::move(arg1)); + }) + .def("Connect", + py::overload_cast&, const InputPortDescriptor&>( + &DiagramBuilder::Connect)) + .def("ExportInput", &DiagramBuilder::ExportInput) + .def("ExportOutput", &DiagramBuilder::ExportOutput) + .def("Build", &DiagramBuilder::Build) + .def("BuildInto", &DiagramBuilder::BuildInto); + + py::class_>(m, "OutputPort"); + + py::class_>(m, "SystemOutput") + .def("get_num_ports", &SystemOutput::get_num_ports) + .def("get_vector_data", &SystemOutput::get_vector_data, py_iref); + + py::class_>(m, "InputPortDescriptor"); + + // Value types. + py::class_>(m, "VectorBase") + .def("CopyToVector", &VectorBase::CopyToVector) + .def("SetFromVector", &VectorBase::SetFromVector); + + // TODO(eric.cousineau): Make a helper function for the Eigen::Ref<> patterns. + py::class_, VectorBase>(m, "BasicVector") + .def(py::init()) + .def(py::init>()) + .def("get_value", + [](const BasicVector* self) -> Eigen::Ref> { + return self->get_value(); + }, py_iref) + .def("get_mutable_value", + [](BasicVector* self) -> Eigen::Ref> { + return self->get_mutable_value(); + }, py_iref); + + py::class_, VectorBase>(m, "Supervector"); + + py::class_, VectorBase>(m, "Subvector"); + + // TODO(eric.cousineau): Interfacing with the C++ abstract value types may be + // a tad challenging. This should be more straightforward once + // scalar-type conversion is supported, as the template-exposure mechanisms + // should be relatively similar. + py::class_(m, "AbstractValue"); + + // Parameters. + // TODO(eric.cousineau): Fill this out. + py::class_>(m, "Parameters"); + + // State. + py::class_>(m, "State") + .def(py::init<>()) + .def("get_continuous_state", + &State::get_continuous_state, py_iref) + .def("get_mutable_continuous_state", + &State::get_mutable_continuous_state, py_iref) + .def("get_discrete_state", + &State::get_discrete_state, py_iref); + + // - Constituents. + py::class_>(m, "ContinuousState") + .def(py::init<>()) + .def("get_vector", &ContinuousState::get_vector, py_iref) + .def("get_mutable_vector", + &ContinuousState::get_mutable_vector, py_iref); + + py::class_>(m, "DiscreteValues") + .def("num_groups", &DiscreteValues::num_groups) + .def("get_data", &DiscreteValues::get_data, py_iref); + + py::class_(m, "AbstractValues"); +} diff --git a/bindings/pydrake/systems/primitives_py.cc b/bindings/pydrake/systems/primitives_py.cc new file mode 100644 index 000000000000..8c2007fec94b --- /dev/null +++ b/bindings/pydrake/systems/primitives_py.cc @@ -0,0 +1,37 @@ +#include +#include + +#include "drake/systems/primitives/adder.h" +#include "drake/systems/primitives/constant_value_source.h" +#include "drake/systems/primitives/constant_vector_source.h" +#include "drake/systems/primitives/integrator.h" +#include "drake/systems/primitives/zero_order_hold.h" + +namespace py = pybind11; + +PYBIND11_MODULE(primitives, m) { + // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. + using namespace drake; + // NOLINTNEXTLINE(build/namespaces): Emulate placement in namespace. + using namespace drake::systems; + + m.doc() = "Bindings for the primitives portion of the Systems framework."; + + using T = double; + + py::class_, LeafSystem>(m, "ConstantVectorSource") + .def(py::init>()); + + py::class_, LeafSystem>(m, "ConstantValueSource"); + + py::class_, LeafSystem>(m, "Adder") + .def(py::init()); + + py::class_, LeafSystem>(m, "Integrator") + .def(py::init()); + + py::class_, LeafSystem>(m, "ZeroOrderHold") + .def(py::init()); + + // TODO(eric.cousineau): Add more systems as needed. +} diff --git a/bindings/pydrake/systems/test/custom_test.py b/bindings/pydrake/systems/test/custom_test.py new file mode 100644 index 000000000000..8b9f2730aabf --- /dev/null +++ b/bindings/pydrake/systems/test/custom_test.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +from __future__ import print_function + +import copy +import unittest +import numpy as np + +from pydrake.systems.analysis import ( + Simulator, + ) +from pydrake.systems.framework import ( + BasicVector, + DiagramBuilder, + LeafSystem, + PortDataType, + ) +from pydrake.systems.primitives import ( + ZeroOrderHold, + ) + + +class CustomAdder(LeafSystem): + # Reimplements `Adder`. + def __init__(self, num_inputs, size): + LeafSystem.__init__(self) + for i in xrange(num_inputs): + self._DeclareInputPort(PortDataType.kVectorValued, size) + self._DeclareVectorOutputPort(BasicVector(size), self._calc_sum) + + def _calc_sum(self, context, sum_data): + # @note This will NOT work if the scalar type is AutoDiff or symbolic, + # since they are not stored densely. + sum = sum_data.get_mutable_value() + sum[:] = 0 + for i in xrange(context.get_num_input_ports()): + input_vector = self.EvalVectorInput(context, i) + sum += input_vector.get_value() + + +class TestCustom(unittest.TestCase): + def _create_system(self): + system = CustomAdder(2, 3) + return system + + def _fix_inputs(self, context): + self.assertEquals(context.get_num_input_ports(), 2) + context.FixInputPort(0, BasicVector([1, 2, 3])) + context.FixInputPort(1, BasicVector([4, 5, 6])) + + def test_execution(self): + system = self._create_system() + context = system.CreateDefaultContext() + self._fix_inputs(context) + output = system.AllocateOutput(context) + self.assertEquals(output.get_num_ports(), 1) + system.CalcOutput(context, output) + value = output.get_vector_data(0).get_value() + self.assertTrue(np.allclose([5, 7, 9], value)) + + def test_simulation(self): + builder = DiagramBuilder() + adder = builder.AddSystem(self._create_system()) + adder.set_name("custom_adder") + # Add ZOH so we can easily extract state. + zoh = builder.AddSystem(ZeroOrderHold(0.1, 3)) + zoh.set_name("zoh") + + builder.ExportInput(adder.get_input_port(0)) + builder.ExportInput(adder.get_input_port(1)) + builder.Connect(adder.get_output_port(0), zoh.get_input_port(0)) + diagram = builder.Build() + context = diagram.CreateDefaultContext() + self._fix_inputs(context) + + simulator = Simulator(diagram, context) + simulator.Initialize() + simulator.StepTo(1) + # Ensure that we have the outputs we want. + state = diagram.GetMutableSubsystemState(zoh, context) + value = state.get_discrete_state().get_data()[0].get_value() + self.assertTrue(np.allclose([5, 7, 9], value)) + + +if __name__ == '__main__': + unittest.main() diff --git a/bindings/pydrake/systems/test/general_test.py b/bindings/pydrake/systems/test/general_test.py new file mode 100644 index 000000000000..65937ddc79d3 --- /dev/null +++ b/bindings/pydrake/systems/test/general_test.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +from __future__ import print_function + +import copy + +import unittest +import numpy as np + +from pydrake.systems.analysis import ( + Simulator, + ) +from pydrake.systems.framework import ( + BasicVector, + Diagram, + DiagramBuilder, + ) +from pydrake.systems.primitives import ( + Adder, + ConstantVectorSource, + Integrator, + ) + + +class TestGeneral(unittest.TestCase): + def test_simulator_ctor(self): + # Create simple system. + system = ConstantVectorSource([1]) + + def check_output(context): + # Check number of output ports and value for a given context. + output = system.AllocateOutput(context) + self.assertEquals(output.get_num_ports(), 1) + system.CalcOutput(context, output) + value = output.get_vector_data(0).get_value() + self.assertTrue(np.allclose([1], value)) + + # Create simulator with basic constructor. + simulator = Simulator(system) + simulator.Initialize() + self.assertTrue(simulator.get_context() is + simulator.get_mutable_context()) + check_output(simulator.get_context()) + simulator.StepTo(1) + + # Create simulator specifying context. + context = system.CreateDefaultContext() + # @note `simulator` now owns `context`. + simulator = Simulator(system, context) + self.assertTrue(simulator.get_context() is context) + check_output(context) + simulator.StepTo(1) + + def test_copy(self): + # Copy a context using `copy` or `clone`. + system = ConstantVectorSource([1]) + context = system.CreateDefaultContext() + context_2 = copy.copy(context) + self.assertNotEquals(context, context_2) + context_3 = context.Clone() + self.assertNotEquals(context, context_3) + # TODO(eric.cousineau): Check more properties. + + def test_diagram_simulation(self): + # Similar to: //systems/framework:diagram_test, ExampleDiagram + size = 3 + + builder = DiagramBuilder() + adder0 = builder.AddSystem(Adder(2, size)) + adder0.set_name("adder0") + adder1 = builder.AddSystem(Adder(2, size)) + adder1.set_name("adder1") + + integrator = builder.AddSystem(Integrator(size)) + integrator.set_name("integrator") + + builder.Connect(adder0.get_output_port(0), adder1.get_input_port(0)) + builder.Connect(adder1.get_output_port(0), + integrator.get_input_port(0)) + + builder.ExportInput(adder0.get_input_port(0)) + builder.ExportInput(adder0.get_input_port(1)) + builder.ExportInput(adder1.get_input_port(1)) + builder.ExportOutput(integrator.get_output_port(0)) + + diagram = builder.Build() + # TODO(eric.cousineau): Figure out unicode handling if needed. + # See //systems/framework/test/diagram_test.cc:349 (sha: bc84e73) + # for an example name. + diagram.set_name("test_diagram") + + simulator = Simulator(diagram) + context = simulator.get_mutable_context() + + # Create and attach inputs. + # TODO(eric.cousineau): Not seeing any assertions being printed if no + # inputs are connected. Need to check this behavior. + input0 = BasicVector([0.1, 0.2, 0.3]) + context.FixInputPort(0, input0) + input1 = BasicVector([0.02, 0.03, 0.04]) + context.FixInputPort(1, input1) + input2 = BasicVector([0.003, 0.004, 0.005]) + context.FixInputPort(2, input2) + + # Initialize integrator states. + def get_mutable_continuous_state(system): + return (diagram.GetMutableSubsystemState(system, context) + .get_mutable_continuous_state()) + + integrator_xc = get_mutable_continuous_state(integrator) + integrator_xc.get_mutable_vector().SetFromVector([0, 1, 2]) + + simulator.Initialize() + + # Simulate briefly, and take full-context snapshots at intermediate + # points. + n = 6 + times = np.linspace(0, 1, n) + context_log = [] + for t in times: + simulator.StepTo(t) + # Record snapshot of *entire* context. + context_log.append(context.Clone()) + + xc_initial = np.array([0, 1, 2]) + xc_final = np.array([0.123, 1.234, 2.345]) + + for i, context_i in enumerate(context_log): + t = times[i] + self.assertEqual(context_i.get_time(), t) + xc = (context_i.get_state().get_continuous_state() + .get_vector().CopyToVector()) + xc_expected = (float(i) / (n - 1) * (xc_final - xc_initial) + + xc_initial) + print("xc[t = {}] = {}".format(t, xc)) + self.assertTrue(np.allclose(xc, xc_expected)) + + +if __name__ == '__main__': + unittest.main() diff --git a/bindings/pydrake/systems/test/graphviz_example.py b/bindings/pydrake/systems/test/graphviz_example.py new file mode 100644 index 000000000000..8e69e89501b8 --- /dev/null +++ b/bindings/pydrake/systems/test/graphviz_example.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import matplotlib.pyplot as plt + +from pydrake.systems.drawing import plot_system_graphviz +from pydrake.systems.framework import DiagramBuilder +from pydrake.systems.primitives import Adder + +builder = DiagramBuilder() +size = 1 +adders = [ + builder.AddSystem(Adder(1, size)), + builder.AddSystem(Adder(1, size)), +] +for i, adder in enumerate(adders): + adder.set_name("adders[{}]".format(i)) +builder.Connect(adders[0].get_output_port(0), adders[1].get_input_port(0)) +builder.ExportInput(adders[0].get_input_port(0)) +builder.ExportOutput(adders[1].get_output_port(0)) + +diagram = builder.Build() +diagram.set_name("graphviz_example") + +plot_system_graphviz(diagram) +plt.show() diff --git a/bindings/pydrake/systems/test/lifetime_test.py b/bindings/pydrake/systems/test/lifetime_test.py new file mode 100644 index 000000000000..a4c4deb0a81b --- /dev/null +++ b/bindings/pydrake/systems/test/lifetime_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +""" +@file +Captures limitations for the present state of the Python bindings for the +lifetime of objects, eventually lock down capabilities as they are introduced. +""" + +from __future__ import print_function + +import unittest +import numpy as np + +from pydrake.systems.analysis import ( + Simulator, + ) +from pydrake.systems.framework import ( + DiagramBuilder, + ) +from pydrake.systems.primitives import ( + Adder, + ) +from pydrake.systems.test.lifetime_test_util import ( + DeleteListenerSystem, + DeleteListenerVector, + ) + + +class Info(object): + # Tracks if an instance has been deleted. + def __init__(self): + self.deleted = False + + def record_deletion(self): + assert not self.deleted + self.deleted = True + + +class TestLifetime(unittest.TestCase): + def test_basic(self): + info = Info() + system = DeleteListenerSystem(info.record_deletion) + self.assertFalse(info.deleted) + del system + self.assertTrue(info.deleted) + + def test_ownership_diagram(self): + info = Info() + system = DeleteListenerSystem(info.record_deletion) + builder = DiagramBuilder() + # `system` is now owned by `builder`. + builder.AddSystem(system) + # `system` is now owned by `diagram`. + diagram = builder.Build() + # Delete the builder. Should still be alive. + del builder + self.assertFalse(info.deleted) + # Delete the diagram. Should be dead. + del diagram + # WARNING + self.assertTrue(info.deleted) + self.assertTrue(system is not None) + + def test_ownership_multiple_containers(self): + info = Info() + system = DeleteListenerSystem(info.record_deletion) + builder_1 = DiagramBuilder() + builder_2 = DiagramBuilder() + builder_1.AddSystem(system) + # This is tested in our fork of `pybind11`, but echoed here for when + # we decide to switch to use `shared_ptr`. + with self.assertRaises(RuntimeError): + # This should throw an error from `pybind11`, since two containers + # are trying to own a unique_ptr-held object. + builder_2.AddSystem(system) + + def test_ownership_simulator(self): + info = Info() + system = DeleteListenerSystem(info.record_deletion) + simulator = Simulator(system) + self.assertFalse(info.deleted) + del simulator + # Simulator does not own the system. + self.assertFalse(info.deleted) + self.assertTrue(system is not None) + + def test_ownership_vector(self): + system = Adder(1, 1) + context = system.CreateDefaultContext() + info = Info() + vector = DeleteListenerVector(info.record_deletion) + context.FixInputPort(0, vector) + del context + # WARNING + self.assertTrue(info.deleted) + self.assertTrue(vector is not None) + + +assert __name__ == '__main__' +unittest.main() diff --git a/bindings/pydrake/systems/test/lifetime_test_util_py.cc b/bindings/pydrake/systems/test/lifetime_test_util_py.cc new file mode 100644 index 000000000000..2040ba49c17e --- /dev/null +++ b/bindings/pydrake/systems/test/lifetime_test_util_py.cc @@ -0,0 +1,58 @@ +#include +#include + +#include "drake/systems/framework/basic_vector.h" +#include "drake/systems/primitives/constant_vector_source.h" + +namespace py = pybind11; + +using std::unique_ptr; +using drake::VectorX; +using drake::systems::BasicVector; +using drake::systems::ConstantVectorSource; + +using T = double; + +namespace { + +// Informs listener when this class is deleted. +class DeleteListenerSystem : public ConstantVectorSource { + public: + explicit DeleteListenerSystem(std::function delete_callback) + : ConstantVectorSource(VectorX::Constant(1, 0.)), + delete_callback_(delete_callback) {} + + ~DeleteListenerSystem() override { + delete_callback_(); + } + private: + std::function delete_callback_; +}; + +class DeleteListenerVector : public BasicVector { + public: + explicit DeleteListenerVector(std::function delete_callback) + : BasicVector(VectorX::Constant(1, 0.)), + delete_callback_(delete_callback) {} + + ~DeleteListenerVector() override { + delete_callback_(); + } + private: + std::function delete_callback_; +}; + +} // namespace + +PYBIND11_MODULE(lifetime_test_util, m) { + // Import dependencies. + py::module::import("pydrake.systems.framework"); + py::module::import("pydrake.systems.primitives"); + + py::class_>( + m, "DeleteListenerSystem") + .def(py::init>()); + py::class_>( + m, "DeleteListenerVector") + .def(py::init>()); +} diff --git a/bindings/pydrake/systems/test/vector_test.py b/bindings/pydrake/systems/test/vector_test.py new file mode 100644 index 000000000000..93283435c552 --- /dev/null +++ b/bindings/pydrake/systems/test/vector_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +from __future__ import print_function + +import copy +import unittest +import numpy as np + +from pydrake.systems.framework import ( + BasicVector, + ) + +# TODO(eric.cousineau): Add negative test cases for AutoDiffXd and Symbolic +# once they are in the bindings. + + +class TestReference(unittest.TestCase): + def test_basic_vector_double(self): + # Ensure that we can get vectors templated on double by reference. + init = [1., 2, 3] + value_data = BasicVector(init) + value = value_data.get_mutable_value() + # TODO(eric.cousineau): Determine if there is a way to extract the + # pointer referred to by the buffer (e.g. `value.data`). + value[:] += 1 + expected = [2., 3, 4] + self.assertTrue(np.allclose(value, expected)) + self.assertTrue(np.allclose(value_data.get_value(), expected)) + self.assertTrue(np.allclose(value_data.get_mutable_value(), expected)) + expected = [5., 6, 7] + value_data.SetFromVector(expected) + self.assertTrue(np.allclose(value, expected)) + self.assertTrue(np.allclose(value_data.get_value(), expected)) + self.assertTrue(np.allclose(value_data.get_mutable_value(), expected)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/skylark/drake_py.bzl b/tools/skylark/drake_py.bzl index 213bb0da1e3a..3b31d486ada3 100644 --- a/tools/skylark/drake_py.bzl +++ b/tools/skylark/drake_py.bzl @@ -32,14 +32,18 @@ def drake_py_binary( def drake_py_test( name, + srcs = None, deps = None, data = None, **kwargs): """A wrapper to insert Drake-specific customizations.""" + if srcs == None: + srcs = ["test/%s.py" % name] deps = adjust_labels_for_drake_hoist(deps) data = adjust_labels_for_drake_hoist(data) native.py_test( name = name, + srcs = srcs, deps = deps, data = data, **kwargs)