diff --git a/.clang-format b/.clang-format index 6c67a5beeaef..cbfbcf0feb0c 100644 --- a/.clang-format +++ b/.clang-format @@ -41,7 +41,7 @@ IncludeCategories: - Regex: '^<' Priority: 40 # Your project's h files. - - Regex: '^"drake' + - Regex: '^"drake/' Priority: 50 # Other libraries' h files (with quotes). - Regex: '^"' diff --git a/bindings/pydrake/.clang-format b/bindings/pydrake/.clang-format index 9cf9b9986c11..f8d9d6101808 100644 --- a/bindings/pydrake/.clang-format +++ b/bindings/pydrake/.clang-format @@ -38,7 +38,7 @@ IncludeCategories: - Regex: '^<' Priority: 40 # Your project's h files. - - Regex: '^"drake' + - Regex: '^"drake/' Priority: 50 # Other libraries' h files (with quotes). - Regex: '^"' diff --git a/bindings/pydrake/BUILD.bazel b/bindings/pydrake/BUILD.bazel index cc94b011ac9e..fca39c96e4d1 100644 --- a/bindings/pydrake/BUILD.bazel +++ b/bindings/pydrake/BUILD.bazel @@ -57,7 +57,10 @@ drake_py_library( drake_cc_library( name = "pydrake_pybind", - hdrs = ["pydrake_pybind.h"], + hdrs = [ + "numpy_object_pybind.h", + "pydrake_pybind.h", + ], declare_installed_headers = 0, defines = select({ "//tools/workspace/nanobind:enabled": [ diff --git a/bindings/pydrake/common/BUILD.bazel b/bindings/pydrake/common/BUILD.bazel index 4a370b64861d..9eef02cfcf72 100644 --- a/bindings/pydrake/common/BUILD.bazel +++ b/bindings/pydrake/common/BUILD.bazel @@ -417,6 +417,8 @@ drake_pybind_library( drake_py_unittest( name = "eigen_pybind_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":eigen_pybind_test_util_py", ], @@ -474,6 +476,8 @@ drake_py_unittest( name = "deprecation_test", # kcov messes with module ref counts. opt_out_conditions = ["//tools/kcov:enabled"], + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":common", ":deprecation_example", @@ -484,6 +488,8 @@ drake_py_unittest( # See source for an explanation why this is separate from `deprecation_test`. drake_py_unittest( name = "deprecation_autocomplete_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":common", ":deprecation_example", @@ -493,6 +499,8 @@ drake_py_unittest( # Provides a unittest for high-level deprecation API. drake_py_unittest( name = "deprecation_utility_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":deprecation_example", "//bindings/pydrake/common/test_utilities:deprecation_py", @@ -534,6 +542,8 @@ drake_pybind_library( drake_py_unittest( name = "ref_cycle_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":common", ":ref_cycle_test_util_py", @@ -650,6 +660,8 @@ drake_pybind_library( drake_py_unittest( name = "sorted_pair_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":common", ":sorted_pair_test_util_py", @@ -758,6 +770,8 @@ drake_pybind_library( drake_py_unittest( name = "wrap_pybind_test", + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":wrap_test_util_py", ], @@ -775,6 +789,8 @@ drake_py_unittest( data = [ "//common/yaml:test/yaml_io_test_input_1.yaml", ], + # TODO(#21572) Remove this opt-out. + test_alt_binder = False, deps = [ ":common", ":serialize_test_util_py", diff --git a/bindings/pydrake/common/cpp_param_pybind.cc b/bindings/pydrake/common/cpp_param_pybind.cc index 446155519ac5..e3d219971f8f 100644 --- a/bindings/pydrake/common/cpp_param_pybind.cc +++ b/bindings/pydrake/common/cpp_param_pybind.cc @@ -106,7 +106,11 @@ py::object GetPyParamScalarImpl(const std::type_info& tinfo) { // This type is not aliased. Get the pybind-registered type, // erroring out if it's not registered. // WARNING: Internal API :( +#ifdef PYDRAKE_USE_PYBIND11 auto* info = py::detail::get_type_info(tinfo); +#else // PYDRAKE_USE_NANOBIND + auto* info = py::detail::nb_type_lookup(&tinfo); +#endif if (!info) { // TODO(eric.cousineau): Use NiceTypeName::Canonicalize(...Demangle(...)) // once simpler dependencies are used (or something else is used to @@ -114,7 +118,11 @@ py::object GetPyParamScalarImpl(const std::type_info& tinfo) { const std::string name = tinfo.name(); throw std::runtime_error("C++ type is not registered in pybind: " + name); } +#ifdef PYDRAKE_USE_PYBIND11 py::handle h(reinterpret_cast(info->type)); +#else // PYDRAKE_USE_NANOBIND + py::handle h(reinterpret_cast(info)); +#endif return py::borrow(h); } } diff --git a/bindings/pydrake/common/cpp_param_pybind.h b/bindings/pydrake/common/cpp_param_pybind.h index 6f05b38faaa6..ddb334d18a4f 100644 --- a/bindings/pydrake/common/cpp_param_pybind.h +++ b/bindings/pydrake/common/cpp_param_pybind.h @@ -100,7 +100,7 @@ py::object GetPyParamScalarImpl(const std::type_info& tinfo); // Gets Python type for a C++ type (base case). template inline py::object GetPyParamScalarImpl(type_pack = {}) { - static_assert(!py::detail::is_pyobject::value, + static_assert(!internal::is_pyobject::value, "You cannot use `pybind11` types (e.g. `py::object`). Use a publicly " "visible replacement type instead (e.g. `drake::pydrake::Object`)."); return GetPyParamScalarImpl(typeid(T)); @@ -114,12 +114,12 @@ inline py::object GetPyParamScalarImpl( } // Gets Python type for a C++ vector that is not registered using -// PYBIND11_MAKE_OPAQUE. +// {PYBIND11,NB}_MAKE_OPAQUE. template inline py::object GetPyParamScalarImpl(type_pack> = {}) { // Get inner type for validation. py::object py_T = GetPyParamScalarImpl(type_pack{}); - if constexpr (!internal::is_generic_pybind_v>) { + if constexpr (!internal::is_generic_caster_v>) { return py::module_::import_("pydrake.common.cpp_param").attr("List")[py_T]; } else { return GetPyParamScalarImpl(typeid(std::vector)); @@ -146,7 +146,7 @@ inline py::tuple GetPyParam(type_pack = {}) { } // namespace pydrake } // namespace drake -namespace pybind11 { +namespace PYDRAKE_BINDER_NAMESPACE { namespace detail { template <> @@ -155,4 +155,4 @@ struct type_caster drake::pydrake::internal::wrapper_pydrake_object> {}; } // namespace detail -} // namespace pybind11 +} // namespace PYDRAKE_BINDER_NAMESPACE diff --git a/bindings/pydrake/common/cpp_template.py b/bindings/pydrake/common/cpp_template.py index cab6be6dbd1f..7c51820b31b6 100644 --- a/bindings/pydrake/common/cpp_template.py +++ b/bindings/pydrake/common/cpp_template.py @@ -439,14 +439,14 @@ def _rename_callable(f, scope, name, cls=None): qualname = cls.__qualname__ + "." + name else: qualname = name - # If Python2, we have to wrap instancemethods + built-in functions to spoof - # the metadata. - type_requires_wrap = ( - types.MethodType, - types.BuiltinMethodType, - types.BuiltinFunctionType, - ) - if isinstance(f, type_requires_wrap): + # Native functions (built-in or nanobind-bound) don't allowing resetting the + # metadata unless we wrap them in Python first. + if ( + isinstance(f, types.BuiltinMethodType) + or isinstance(f, types.BuiltinFunctionType) + or f.__class__.__name__ == "nb_method" + or f.__class__.__name__ == "nb_func" + ): orig = f def f(*args, **kwargs): diff --git a/bindings/pydrake/common/cpp_template_pybind.h b/bindings/pydrake/common/cpp_template_pybind.h index be8a2b88ed45..c3e9dfa98491 100644 --- a/bindings/pydrake/common/cpp_template_pybind.h +++ b/bindings/pydrake/common/cpp_template_pybind.h @@ -153,7 +153,12 @@ py::object AddTemplateMethod( // BR const std::string instantiation_name = internal::GetInstantiationName(py_template, param, mangle); py::object py_func = py::cpp_function(std::forward(method), - py::name(instantiation_name.c_str()), py::is_method(scope), + py::name(instantiation_name.c_str()), +#ifdef PYDRAKE_USE_PYBIND11 + py::is_method(scope), +#else // PYDRAKE_USE_NANOBIND + py::is_method(), +#endif std::forward(extra)...); internal::AddInstantiation(py_template, py_func, param); return py_template; diff --git a/bindings/pydrake/common/eigen_pybind.h b/bindings/pydrake/common/eigen_pybind.h index 788a0304a810..3d401bfa74f3 100644 --- a/bindings/pydrake/common/eigen_pybind.h +++ b/bindings/pydrake/common/eigen_pybind.h @@ -74,19 +74,14 @@ inline py::object WrapToMatchInputShape(py::handle func) { } // namespace pydrake } // namespace drake -namespace pybind11 { +namespace PYDRAKE_BINDER_NAMESPACE { namespace detail { /** -Provides pybind11 `type_caster`s for drake::EigenPtr. - +Provides `type_caster`s for drake::EigenPtr. Uses `type_caster` internally to avoid code duplication. -See http://pybind11.readthedocs.io/en/stable/advanced/cast/custom.html for -more details on custom type casters. - -TODO(eric.cousineau): Place all logic inside of `drake` namespace once our -pybind11 fork includes PYBIND11_TYPE_CASTER macro w/ fully qualified symbols. */ +#ifdef PYDRAKE_USE_PYBIND11 template struct type_caster> { using PtrType = drake::EigenPtr; @@ -126,6 +121,12 @@ struct type_caster> { private: InnerCaster inner_caster; }; +#else // PYDRAKE_USE_NANOBIND +template +struct type_caster> { + // TODO(#21572) Implement me. +}; +#endif // PYDRAKE_USE_PYBIND11 } // namespace detail -} // namespace pybind11 +} // namespace PYDRAKE_BINDER_NAMESPACE diff --git a/bindings/pydrake/common/module_py.cc b/bindings/pydrake/common/module_py.cc index 1b3a4c9ef63e..5961a34ed968 100644 --- a/bindings/pydrake/common/module_py.cc +++ b/bindings/pydrake/common/module_py.cc @@ -42,11 +42,23 @@ void trigger_an_assertion_failure() { // Resolves to a Python handle given a type erased pointer. If the instance or // lowest-level RTTI type are unregistered, returns an empty handle. py::handle ResolvePyObject(const type_erased_ptr& ptr) { +#ifdef PYDRAKE_USE_PYBIND11 + auto py_type_info = py::detail::get_type_info(ptr.info); + return py::detail::get_object_handle(ptr.raw, py_type_info); +#else // TODO(#21572) Use is_polymorphic for nanobind (or remove it from the struct, // if we change our mind and end up not needing it). unused(ptr.is_polymorphic); - auto py_type_info = py::detail::get_type_info(ptr.info); - return py::detail::get_object_handle(ptr.raw, py_type_info); + bool is_new{false}; + PyObject* result{}; + result = py::detail::nb_type_put(&ptr.info, const_cast(ptr.raw), + py_rvp::reference, nullptr, &is_new); + if (is_new) { + py::object delete_me = py::steal(result); + return py::handle(); + } + return py::steal(result); +#endif } // Override for SetNiceTypeNamePtrOverride, to ensure that instances that are @@ -71,7 +83,9 @@ namespace testing { // Registered type. Also a base class for UnregisteredDerivedType. class RegisteredType { public: - virtual ~RegisteredType() {} + DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(RegisteredType); + RegisteredType() = default; + virtual ~RegisteredType() = default; }; // Completely unregistered type. class UnregisteredType {}; @@ -244,7 +258,7 @@ void InitLowLevelModules(py::module_ m) { const std::string_view name_str(name.c_str()); if (name_str == "contents" || name_str == "extension" || name_str == "filename_hint") { - name = py::str(fmt::format("_{}", name_str)); + name = py::str(fmt::format("_{}", name_str).c_str()); } py::eval("object.__setattr__", py::globals())(self, name, value); }); @@ -256,8 +270,8 @@ void InitLowLevelModules(py::module_ m) { return py::bytes(contents.c_str(), contents.size()); }, [](Class& self, const py::bytes& contents) { - self = MemoryFile{ - std::string{contents}, self.extension(), self.filename_hint()}; + self = MemoryFile{std::string{contents.c_str(), contents.size()}, + self.extension(), self.filename_hint()}; }); cls.def_prop_rw( "_extension", [](const Class& self) { return self.extension(); }, @@ -325,15 +339,21 @@ discussion), use e.g. // Admittedly, it's unusual for a python library like pydrake to raise // SystemExit, but for now its better than C++ ::abort() taking down the // whole interpreter with a worse diagnostic message. - py::register_exception_translator([](std::exception_ptr p) { - try { - if (p) { - std::rethrow_exception(p); - } - } catch (const drake::internal::assertion_error& e) { - PyErr_SetString(PyExc_SystemExit, e.what()); - } - }); + py::register_exception_translator( +#ifdef PYDRAKE_USE_PYBIND11 + [](std::exception_ptr p) +#else // PYDRAKE_USE_NANOBIND + [](const std::exception_ptr& p, void*) +#endif + { + try { + if (p) { + std::rethrow_exception(p); + } + } catch (const drake::internal::assertion_error& e) { + PyErr_SetString(PyExc_SystemExit, e.what()); + } + }); // Convenient wrapper for querying FindResource(resource_path). m.def("FindResourceOrThrow", &FindResourceOrThrow, "Attempts to locate a Drake resource named by the given path string. " @@ -357,7 +377,7 @@ discussion), use e.g. []() { py::object result; if (auto optional_result = MaybeGetDrakePath()) { - result = py::str(*optional_result); + result = py::str(optional_result->c_str()); } return result; }, @@ -377,9 +397,10 @@ discussion), use e.g. // ========================================================================= // Define `_testing` submodule. - py::module_ pydrake_top = py::eval("sys.modules['pydrake']", py::globals()); - py::module_ pydrake_common = - py::eval("sys.modules['pydrake.common']", py::globals()); + py::module_ pydrake_top = + py::cast(py::eval("sys.modules['pydrake']", py::globals())); + py::module_ pydrake_common = py::cast( + py::eval("sys.modules['pydrake.common']", py::globals())); py::module_ testing = pydrake_common.def_submodule("_testing"); testing::def_testing(testing); diff --git a/bindings/pydrake/common/serialize_pybind.h b/bindings/pydrake/common/serialize_pybind.h index 2970b662f406..3412a22f4c65 100644 --- a/bindings/pydrake/common/serialize_pybind.h +++ b/bindings/pydrake/common/serialize_pybind.h @@ -57,20 +57,16 @@ class DefAttributesArchive { const int offset = CalcClassOffset(prototype_value); // Define property functions to get and set this particular field. - py::cpp_function getter( - [offset](const CxxClass& self) -> const T& { - const T* const field_in_self = reinterpret_cast( - reinterpret_cast(&self) + offset); - return *field_in_self; - }, - py::is_method(*ppy_class_)); - py::cpp_function setter( - [offset](CxxClass& self, const T& value) { - T* const field_in_self = - reinterpret_cast(reinterpret_cast(&self) + offset); - *field_in_self = value; - }, - py::is_method(*ppy_class_)); + auto getter = [offset](const CxxClass& self) -> const T& { + const T* const field_in_self = reinterpret_cast( + reinterpret_cast(&self) + offset); + return *field_in_self; + }; + auto setter = [offset](CxxClass& self, const T& value) { + T* const field_in_self = + reinterpret_cast(reinterpret_cast(&self) + offset); + *field_in_self = value; + }; // Fetch the docstring (or the empty string, if we aren't using docstrings). const char* doc = ""; @@ -144,24 +140,39 @@ class DefAttributesArchive { static py::object CalcSchemaType(const T*) { // Pybind11 doesn't support type::of<> for primitive types, so we must // match them manually. See https://github.com/pybind/pybind11/issues/2486. + auto type_of = [](const py::object& o) { +#ifdef PYDRAKE_USE_PYBIND11 + return py::type::of(o); +#else // PYDRAKE_USE_NANOBIND + return py::borrow(o.type()); +#endif + }; if constexpr (std::is_same_v) { - return py::type::of(py::bool_()); + return type_of(py::bool_(false)); } else if constexpr (std::is_integral_v) { - return py::type::of(py::int_()); + return type_of(py::int_(0)); } else if constexpr (std::is_floating_point_v) { - return py::type::of(py::float_()); + return type_of(py::float_(0.0)); } else if constexpr (std::is_same_v) { - return py::type::of(py::str()); + return type_of(py::str("")); } else if constexpr (is_eigen_type::value) { // TODO(jwnimmer-tri) Perhaps we can use numpy.typing here some day? return py::module_::import_("numpy").attr("ndarray"); + } else if constexpr (std::is_enum_v) { +#ifdef PYDRAKE_USE_PYBIND11 + return py::type::of(); +#else // PYDRAKE_USE_NANOBIND + return py::borrow(py::type()); +#endif } else { - // Anything that remains should be a registered C++ type. - constexpr bool is_registered_type = - std::is_base_of_v>; + // Anything that remains should be a registered C++ class type. + constexpr bool is_registered_type = is_generic_caster_v; if constexpr (is_registered_type) { +#ifdef PYDRAKE_USE_PYBIND11 return py::type::of(); +#else // PYDRAKE_USE_NANOBIND + return py::borrow(py::type()); +#endif } else { return CannotIdentifySchemaType(); } @@ -296,22 +307,23 @@ void DefReprUsingSerialize(PyClass* ppy_class) { internal::DefReprArchive archive; CxxClass prototype{}; prototype.Serialize(&archive); - ppy_class->def("__repr__", - [names = std::move(archive.property_names())](py::object self) { - std::ostringstream result; - result << py::str(internal::PrettyClassName(self.attr("__class__"))); - result << "("; - bool first = true; - for (const std::string& name : names) { - if (!first) { - result << ", "; - } - result << name << "=" << py::repr(self.attr(name.c_str())); - first = false; - } - result << ")"; - return result.str(); - }); + ppy_class->def("__repr__", [names = std::move(archive.property_names())]( + py::object self) { + std::ostringstream result; + result + << py::str(internal::PrettyClassName(self.attr("__class__"))).c_str(); + result << "("; + bool first = true; + for (const std::string& name : names) { + if (!first) { + result << ", "; + } + result << name << "=" << py::repr(self.attr(name.c_str())).c_str(); + first = false; + } + result << ")"; + return result.str(); + }); } } // namespace pydrake diff --git a/bindings/pydrake/common/test/cpp_template_test.py b/bindings/pydrake/common/test/cpp_template_test.py index 5e2c2adbf373..0a8475d92d30 100644 --- a/bindings/pydrake/common/test/cpp_template_test.py +++ b/bindings/pydrake/common/test/cpp_template_test.py @@ -339,8 +339,8 @@ def test_template_type_mappings(self): # arguments. with self.assertRaisesRegex( TypeError, - r".*incompatible function arguments(?s:.)*\(arg0: " - r".*\.SimpleTemplate𝓣int𝓤\).*", + r".*incompatible function arguments(?s:.)*\(x: " + r".*\.SimpleTemplate𝓣int𝓤\.*", ): simple_func("incorrect_value") diff --git a/bindings/pydrake/common/test/cpp_template_test_util_py.cc b/bindings/pydrake/common/test/cpp_template_test_util_py.cc index 7fcc1de0bddb..29fa5a7a3118 100644 --- a/bindings/pydrake/common/test/cpp_template_test_util_py.cc +++ b/bindings/pydrake/common/test/cpp_template_test_util_py.cc @@ -78,7 +78,7 @@ PYDRAKE_MODULE(cpp_template_test_util, m) { BindTemplateWithDefault(m); BindTemplateWithDefault(m); - m.def("simple_func", [](const SimpleTemplate&) {}); + m.def("simple_func", [](const SimpleTemplate&) {}, py::arg("x")); // Add dummy constructors to check __call__ pseudo-deduction. { diff --git a/bindings/pydrake/common/test/module_test.py b/bindings/pydrake/common/test/module_test.py index 83eeecb24958..32e147d716c4 100644 --- a/bindings/pydrake/common/test/module_test.py +++ b/bindings/pydrake/common/test/module_test.py @@ -54,6 +54,9 @@ def test_sha256(self): copy.copy(not_empty) copy.deepcopy(not_empty) + @unittest.skipIf( + mut._binder == "nanobind", "TODO(#21572) Fix MemoryFile bindings." + ) def test_memory_file(self): content_bytes = b"Some string" hint = "hint" @@ -98,6 +101,9 @@ def string_regex(s): ) self.assertEqual(file.extension(), ".urdf") + @unittest.skipIf( + mut._binder == "nanobind", "TODO(#21572) Fix MemoryFile bindings." + ) def test_memory_file_yaml_serialization(self): """Confirms that this can be serialized appropriately. The serialization work (with all of its nuances) gets tested elsewhere. diff --git a/bindings/pydrake/common/test/serialize_import_failure_test.py b/bindings/pydrake/common/test/serialize_import_failure_test.py index b0fec4dfb6f1..19db5845409a 100644 --- a/bindings/pydrake/common/test/serialize_import_failure_test.py +++ b/bindings/pydrake/common/test/serialize_import_failure_test.py @@ -1,5 +1,7 @@ import unittest +from pydrake.common import _binder + class TestSerializeImportFailure(unittest.TestCase): def test_import_exception(self): @@ -11,5 +13,11 @@ def test_import_exception(self): with self.assertRaises(ImportError) as cm: from pydrake.common.test.serialize_test_bar import Bar # noqa: F401 message = str(cm.exception) - self.assertIn("an import is missing", message) - self.assertIn("drake::pydrake::test::Foo", message) + if _binder == "pybind11": + self.assertIn("an import is missing", message) + self.assertIn("drake::pydrake::test::Foo", message) + else: + # TODO(#21572) Either port DRAKE_PYBIND11_CHECK_SIGNATURE_INFECTION + # or something like it to nanobind, or add custom code to provide a + # better message for this case. + self.assertIn("error while initializing the extension", message) diff --git a/bindings/pydrake/common/test/serialize_pybind_test.py b/bindings/pydrake/common/test/serialize_pybind_test.py index fd5592ba300d..7dd409d67318 100644 --- a/bindings/pydrake/common/test/serialize_pybind_test.py +++ b/bindings/pydrake/common/test/serialize_pybind_test.py @@ -4,6 +4,7 @@ import numpy as np +from pydrake.common import _binder from pydrake.common.test.serialize_test_util import ( MyData1, MyData2, @@ -41,7 +42,11 @@ def test_attributes_using_serialize_no_docs(self): # No docs. self.assertEqual(dut.quux, -1.0) - self.assertEqual(inspect.getdoc(MyData1.quux), "") + if _binder == "pybind11": + self.assertEqual(inspect.getdoc(MyData1.quux), "") + else: + # Nanobind adds doc itself. + self.assertEqual(inspect.getdoc(MyData1.quux), "(self) -> float") # Test fields. fields = getattr(MyData1, "__fields__") diff --git a/bindings/pydrake/common/test/value_test.py b/bindings/pydrake/common/test/value_test.py index 4fe020f8d57f..c1a3c085afa3 100644 --- a/bindings/pydrake/common/test/value_test.py +++ b/bindings/pydrake/common/test/value_test.py @@ -1,7 +1,7 @@ import copy import unittest -from pydrake.common import pretty_class_name +from pydrake.common import _binder, pretty_class_name from pydrake.common.cpp_param import List from pydrake.common.test.value_test_util import ( CustomType, @@ -85,6 +85,9 @@ def test_assert_equal_but_not_aliased(self): with self.assertRaises(AssertionError): self.assert_equal_but_not_aliased(a, {"x": -10}) + @unittest.skipIf( + _binder == "nanobind", "TODO(#21572) Implement Value[object]." + ) def test_abstract_value_py_object(self): # The Value constructor operates by taking a reference, not by copying. initial_dict = {"x": 10} @@ -149,6 +152,9 @@ def test_abstract_value_make(self): self.assertIsInstance(value, Value[float]) value = AbstractValue.Make(MoveOnlyType(10)) self.assertIsInstance(value, Value[MoveOnlyType]) + if _binder == "nanobind": + # TODO(#21572) Implement Value[object]. + return value = AbstractValue.Make({"x": 10}) self.assertIsInstance(value, Value[object]) # N.B. Empty lists cannot have their type inferred, so the type will @@ -179,7 +185,9 @@ def test_abstract_value_cc_type_unregistered(self): def test_value_registration(self): # Existence check. - Value[object] + if _binder != "nanobind": + # TODO(#21572) Implement Value[object]. + Value[object] Value[str] Value[bool] @@ -187,4 +195,6 @@ def test_value_ctor(self): self.assertIsInstance(Value("foo"), Value[str]) self.assertIsInstance(Value(True), Value[bool]) self.assertIsInstance(Value(1.0), Value[float]) - self.assertIsInstance(Value(object()), Value[object]) + if _binder != "nanobind": + # TODO(#21572) Implement Value[object]. + self.assertIsInstance(Value(object()), Value[object]) diff --git a/bindings/pydrake/common/text_logging_pybind.cc b/bindings/pydrake/common/text_logging_pybind.cc index d305fda0ace6..be1b9209ea25 100644 --- a/bindings/pydrake/common/text_logging_pybind.cc +++ b/bindings/pydrake/common/text_logging_pybind.cc @@ -62,8 +62,8 @@ class pylogging_sink final is_configured_.store(true); } - // NOLINTNEXTLINE(build/namespaces_literals) This is how pybind11 wants it. - using namespace pybind11::literals; + // NOLINTNEXTLINE(build/namespaces_literals) This is the typical spelling. + using namespace PYDRAKE_BINDER_NAMESPACE::literals; // Construct the LogRecord. // https://docs.python.org/3/library/logging.html#logrecord-objects diff --git a/bindings/pydrake/common/value_py.cc b/bindings/pydrake/common/value_py.cc index b759770d2b59..459f52b789ec 100644 --- a/bindings/pydrake/common/value_py.cc +++ b/bindings/pydrake/common/value_py.cc @@ -38,10 +38,13 @@ class PyObjectValue : public drake::Value { // Add instantiations of primitive types on an as-needed basis; please be // conservative. void AddPrimitiveValueInstantiations(py::module_ m) { - AddValueInstantiation(m); // Value[str] - AddValueInstantiation(m); // Value[bool] - AddValueInstantiation(m); // Value[float] + AddValueInstantiation(m); // Value[str] + AddValueInstantiation(m); // Value[bool] + AddValueInstantiation(m); // Value[float] +#ifdef PYDRAKE_USE_PYBIND11 + // TODO(#21572) Implement me. AddValueInstantiation(m); // Value[object] +#endif } } // namespace diff --git a/bindings/pydrake/common/value_pybind.h b/bindings/pydrake/common/value_pybind.h index db93e14ce5a4..5b0fdc85c26c 100644 --- a/bindings/pydrake/common/value_pybind.h +++ b/bindings/pydrake/common/value_pybind.h @@ -26,7 +26,7 @@ namespace pydrake { /// @returns Reference to the registered Python type. template > class_ AddValueInstantiation(py::module_ scope) { - static_assert(!py::detail::is_pyobject::value, "See docs for GetPyParam"); + static_assert(!internal::is_pyobject::value, "See docs for GetPyParam"); py::module_ py_common = py::module_::import_("pydrake.common.value"); class_ py_class( scope, TemporaryClassName().c_str()); @@ -40,24 +40,29 @@ class_ AddValueInstantiation(py::module_ scope) { py_class.def(py::init()); // Define emplace constructor. py::object py_T = param[0]; - py_class.def(py::init([py_T](py::args args, py::kwargs kwargs) { - // Use Python constructor for the bound type. - py::object py_v = py_T(*args, **kwargs); - // TODO(eric.cousineau): Use `unique_ptr` for custom types if it's ever a - // performance concern. - // Use `type_caster` so that we are not forced to copy T, which is not - // possible for non-movable types. This can be avoided if we bind a - // `cpp_function` accepting a reference. However, that may cause the Python - // instance to be double-initialized. - py::detail::type_caster caster; - DRAKE_THROW_UNLESS(caster.load(py_v, false)); - const T& v = caster; // Use implicit conversion from `type_caster<>`. - return new Class(v); - })); + py_class.def( + "__init__", [py_T](Class* self, py::args args, py::kwargs kwargs) { + // Use Python constructor for the bound type. + py::object py_v = py_T(*args, **kwargs); + // Use `type_caster` so that we are not forced to copy T, which is not + // possible for non-movable types. This can be avoided if we bind a + // `cpp_function` accepting a reference. However, that may cause the + // Python instance to be double-initialized. + py::detail::type_caster caster; +#ifdef PYDRAKE_USE_PYBIND11 + DRAKE_THROW_UNLESS(caster.load(py_v, false)); + // Use implicit conversion from `type_caster<>`. + const T& v = caster; +#else // PYDRAKE_USE_NANOBIND + DRAKE_THROW_UNLESS(caster.from_python(py_v, 0, nullptr)); + const T& v = caster.operator nanobind::detail::cast_t(); +#endif + new (self) Class(v); + }); // If the type is registered via `class_`, or is of type `Object` // (`py::object`), then we can obtain a mutable view into the value. constexpr bool has_get_mutable_value = - internal::is_generic_pybind_v || std::is_same_v; + internal::is_generic_caster_v || std::is_same_v; if constexpr (has_get_mutable_value) { py::rv_policy return_policy = py_rvp::reference_internal; if (std::is_same_v) { diff --git a/bindings/pydrake/common/wrap_pybind.h b/bindings/pydrake/common/wrap_pybind.h index 5e7aa200a2da..cf0710ff5410 100644 --- a/bindings/pydrake/common/wrap_pybind.h +++ b/bindings/pydrake/common/wrap_pybind.h @@ -23,16 +23,22 @@ namespace internal { // implies that the type has been declared using `class_`, and can have // a reference passed through. Otherwise, the type uses type-conversion: // https://pybind11.readthedocs.io/en/stable/advanced/cast/index.html +#ifdef PYDRAKE_USE_PYBIND11 template -constexpr inline bool is_generic_pybind_v = +constexpr inline bool is_generic_caster_v = std::is_base_of_v>; +#else // PYDRAKE_USE_NANOBIND +template +constexpr inline bool is_generic_caster_v = + py::detail::is_base_caster_v>; +#endif // PYDRAKE_USE_PYBIND11 template struct wrap_ref_ptr : public wrap_arg_default {}; template -struct wrap_ref_ptr>> { +struct wrap_ref_ptr>> { // NOLINTNEXTLINE[runtime/references]: Intentional. static T* wrap(T& arg) { return &arg; } static T& unwrap(T* arg_wrapped) { return *arg_wrapped; } @@ -55,6 +61,7 @@ struct wrap_callback> // Struct which must provide `Type`, `WrappedType`, `unwrap`, `wrap`, // `wrapped_name`, and `original_name`. // Fails-fast at runtime if there is an attempt to pass by reference. +#ifdef PYDRAKE_USE_PYBIND11 template struct type_caster_wrapped { using Type = typename Wrapper::Type; @@ -109,6 +116,12 @@ struct type_caster_wrapped { bool loaded_{false}; Type value_; }; +#else // PYDRAKE_USE_NANOBIND +template +struct type_caster_wrapped { + // TODO(#21572) Implement me. +}; +#endif // PYDRAKE_USE_PYBIND11 } // namespace internal #endif // DRAKE_DOXYGEN_CXX @@ -137,14 +150,21 @@ auto WrapCallbacks(Func&& func) { template void DefReadWriteKeepAlive( PyClass* cls, const char* name, T Class::* member, const char* doc = "") { - auto getter = [member](const Class* obj) { return obj->*member; }; - auto setter = [member](Class* obj, const T& value) { obj->*member = value; }; +#ifdef PYDRAKE_USE_PYBIND11 cls->def_prop_rw(name, // BR - py::cpp_function(getter), - py::cpp_function(setter, + py::cpp_function([member](const Class* obj) { return obj->*member; }), + py::cpp_function( + [member](Class* obj, const T& value) { obj->*member = value; }, // Keep alive, reference: `self` keeps `value` alive. py::keep_alive<1, 2>()), doc); +#else // PYDRAKE_USE_NANOBIND + cls->def_prop_rw( + name, // BR + [member](const Class* obj) { return obj->*member; }, + [member](Class* obj, const T& value) { obj->*member = value; }, + py::for_setter(py::keep_alive<1, 2>()), doc); +#endif } /// Idempotent to pybind11's `def_ro()`, which works for unique_ptr @@ -157,20 +177,18 @@ void DefReadWriteKeepAlive( template void DefReadUniquePtr(PyClass* cls, const char* name, const std::unique_ptr Class::* member, const char* doc = "") { - auto getter = py::cpp_function( - [member](const Class* obj) { return (obj->*member).get(); }, - py_rvp::reference_internal); - cls->def_prop_ro(name, getter, doc); + cls->def_prop_ro( + name, [member](const Class* obj) { return (obj->*member).get(); }, + py_rvp::reference_internal, doc); } // Variant of DefReadUniquePtr() for copyable_unique_ptr. template void DefReadUniquePtr(PyClass* cls, const char* name, const copyable_unique_ptr Class::* member, const char* doc = "") { - auto getter = py::cpp_function( - [member](const Class* obj) { return (obj->*member).get(); }, - py_rvp::reference_internal); - cls->def_prop_ro(name, getter, doc); + cls->def_prop_ro( + name, [member](const Class* obj) { return (obj->*member).get(); }, + py_rvp::reference_internal, doc); } } // namespace pydrake diff --git a/bindings/pydrake/numpy_object_pybind.h b/bindings/pydrake/numpy_object_pybind.h new file mode 100644 index 000000000000..60a1354802cc --- /dev/null +++ b/bindings/pydrake/numpy_object_pybind.h @@ -0,0 +1,223 @@ +#pragma once + +#include "drake_nanobind/eigen/dense.h" + +#ifndef PYDRAKE_USE_NANOBIND +#error "Should only be used when the binder is nanobind!" +#endif + +/* @file +This file provides the PYDRAKE_NUMPY_OBJECT_DTYPE macro. */ + +namespace nanobind { +namespace detail { + +/* Base class for PYDRAKE_NUMPY_OBJECT_DTYPE casters. +@tparam T is the Eigen matrix type (e.g., MatrixX)*/ +template +struct pydrake_numpy_dtype_object_type_caster { + using Scalar = typename T::Scalar; + using PlainScalar = std::remove_cv_t; + using PlainScalarCaster = make_caster; + static constexpr bool kCompileTime1D = + (T::RowsAtCompileTime == 1 || T::ColsAtCompileTime == 1); + static constexpr int kCompileTime1DShape = + (T::RowsAtCompileTime == 1) ? T::ColsAtCompileTime + : (T::ColsAtCompileTime == 1) ? T::RowsAtCompileTime + : Eigen::Dynamic; + + NB_TYPE_CASTER( + T, const_name("numpy.ndarray[") + + concat_maybe(const_name("dtype=") + PlainScalarCaster::Name, + const_name(shape::name, + shape::name), + dtype_const_name::name) + + const_name("]")) + + bool from_python( + handle src, uint8_t flags, cleanup_list* /* cleanup */) noexcept { + auto numpy = module_::import_("numpy"); + + if (src.is_none()) { + return false; + } + + // Avoid converting np.array(dtype=np.float64) to AutoDiffXd prematurely. + // Only accept autodiff conversions as a last resort. + const bool convert = flags & (uint8_t)cast_flags::convert; + if (!convert) { + return false; + } + + // TODO(jwnimmer-tri) This implementation is probably terribly inefficient. + // Using the NumPy C API (like pybind11 does) would probably be faster. + + auto array = numpy.attr("asarray")(src, arg("dtype") = "object"); + auto shape = cast(array.attr("shape")); + + int rows{}; + int cols{}; + bool np_1d{}; + if (shape.size() == 1) { + if constexpr (T::ColsAtCompileTime == 1) { + rows = cast(shape[0]); + cols = 1; + np_1d = true; + } else if constexpr (T::RowsAtCompileTime == 1) { + rows = 1; + cols = cast(shape[0]); + np_1d = true; + } else { + // Promote from 1d array to 2d array (as column vector). + array = array.attr("reshape")(-1, 1); + rows = cast(shape[0]); + cols = 1; + np_1d = false; + } + } else if (shape.size() == 2) { + rows = cast(shape[0]); + cols = cast(shape[1]); + np_1d = false; + } else { + return false; + } + + try { + if constexpr (T::RowsAtCompileTime != Eigen::Dynamic) { + if (rows != T::RowsAtCompileTime) { + return false; + } + } + if constexpr (T::ColsAtCompileTime != Eigen::Dynamic) { + if (cols != T::ColsAtCompileTime) { + return false; + } + } + value.resize(rows, cols); + for (Eigen::Index i = 0; i < rows; ++i) { + for (Eigen::Index j = 0; j < cols; ++j) { + value(i, j) = cast( + array[np_1d ? nanobind::object(nanobind::int_(i + j)) + : nanobind::object(nanobind::make_tuple(i, j))]); + } + } + } catch (const cast_error&) { + return false; + } + + return true; + } + + static handle from_cpp(const T& src, rv_policy /* policy */, + cleanup_list* /* cleanup */) noexcept { + auto numpy = module_::import_("numpy"); + + // Construct an empty numpy.ndarray with the desired shape. + list shape; + if constexpr (kCompileTime1D) { + shape.append(src.size()); + } else { + shape.append(src.rows()); + shape.append(src.cols()); + } + auto result = numpy.attr("empty")(shape, arg("dtype") = "object"); + + // Fill in the array elements, converting to Python objects one at a time. + if constexpr (kCompileTime1D) { + for (Eigen::Index i = 0; i < src.size(); ++i) { + result[i] = src[i]; + } + } else { + for (Eigen::Index i = 0; i < src.rows(); ++i) { + for (Eigen::Index j = 0; j < src.cols(); ++j) { + list ij; + ij.append(i); + ij.append(j); + result[nanobind::tuple(ij)] = src(i, j); + } + } + } + + return result.release(); + } +}; + +} // namespace detail +} // namespace nanobind + +/* The PYDRAKE_NUMPY_OBJECT_DTYPE macro defines a type_caster<> specialization +between C++ Eigen matrices and Python NumPy arrays in cases where array's +dtype=object (i.e., the Eigen::Scalar type is not a primitive type). + +@tparam Type the Eigen::Scalar type to declare casters for (e.g., AutoDiffXd). + +The first type_caster partial template specialization handles plain (dense) +matrices and expression trees that can be evaluated to same. + +The second and third specializations handle Eigen:Ref and Eigen::Map, +respectively. + +TODO(#24749) The Ref and Map casters should disallow mutable matrices. */ +#define PYDRAKE_NUMPY_OBJECT_DTYPE(Type) \ + namespace nanobind { \ + namespace detail { \ + template \ + struct type_caster || is_eigen_xpr_v) && \ + std::is_same_v, \ + Type>>> \ + : public pydrake_numpy_dtype_object_type_caster {}; \ + template \ + struct type_caster, \ + enable_if_t, Type>>> \ + : public pydrake_numpy_dtype_object_type_caster< \ + Eigen::Matrix> { \ + using _matrix = Eigen::Matrix; \ + template \ + using Cast = _matrix; \ + explicit operator _matrix() const { return this->value; } \ + }; \ + template \ + struct type_caster, \ + enable_if_t, Type>>> \ + : public pydrake_numpy_dtype_object_type_caster< \ + Eigen::Matrix> { \ + using _matrix = Eigen::Matrix; \ + template \ + using Cast = _matrix; \ + explicit operator _matrix() const { return this->value; } \ + }; \ + } /* namespace detail */ \ + } /* namespace nanobind */ diff --git a/bindings/pydrake/pydrake_pybind.h b/bindings/pydrake/pydrake_pybind.h index 3f0f45fc9427..1c0edaadaf85 100644 --- a/bindings/pydrake/pydrake_pybind.h +++ b/bindings/pydrake/pydrake_pybind.h @@ -1,14 +1,16 @@ #pragma once #include +#include #include -// Here we include a lot of the pybind11 API, to ensure that all code in pydrake -// sees the same definitions ("One Definition Rule") for template types intended -// for specialization. Any pybind11 headers with `type_caster<>` specializations -// must be included here (e.g., eigen.h, functional.h, numpy.h, stl.h) as well -// as ADL headers (e.g., operators.h). Headers that are unused by pydrake -// (e.g., complex.h) are omitted. +// Here we include a lot of the pybind11 (or nanobind) API, to ensure that all +// code in pydrake sees the same definitions ("One Definition Rule") for +// template types intended for specialization. Any headers with `type_caster<>` +// specializations must be included here (e.g., eigen.h, functional.h, numpy.h, +// stl.h) as well as ADL headers (e.g., operators.h). Headers that are unused by +// pydrake (e.g., complex.h) are omitted. +#ifdef PYDRAKE_USE_PYBIND11 #include "pybind11/eigen.h" #include "pybind11/eval.h" #include "pybind11/functional.h" @@ -18,6 +20,47 @@ #include "pybind11/stl.h" #include "pybind11/stl/filesystem.h" #include "pybind11/typing.h" +#endif // PYDRAKE_USE_PYBIND11 + +#ifdef PYDRAKE_USE_NANOBIND +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wattributes" +#ifdef __clang__ +#pragma GCC diagnostic ignored "-Wc++11-narrowing" +#else +#pragma GCC diagnostic ignored "-Wclass-memaccess" +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif // __clang__ +#include "drake_nanobind/eigen/dense.h" +#include "drake_nanobind/eigen/sparse.h" +#include "nanobind/eval.h" +#include "nanobind/make_iterator.h" +#include "nanobind/nanobind.h" +#include "nanobind/ndarray.h" +#include "nanobind/operators.h" +#include "nanobind/stl/array.h" +#include "nanobind/stl/filesystem.h" +#include "nanobind/stl/function.h" +#include "nanobind/stl/list.h" +#include "nanobind/stl/map.h" +#include "nanobind/stl/optional.h" +#include "nanobind/stl/pair.h" +#include "nanobind/stl/set.h" +#include "nanobind/stl/shared_ptr.h" +#include "nanobind/stl/string.h" +#include "nanobind/stl/string_view.h" +#include "nanobind/stl/tuple.h" +#include "nanobind/stl/unique_ptr.h" +#include "nanobind/stl/unordered_map.h" +#include "nanobind/stl/unordered_set.h" +#include "nanobind/stl/variant.h" +#include "nanobind/stl/vector.h" +#include "nanobind/trampoline.h" +#pragma GCC diagnostic pop + +#include "drake/bindings/pydrake/numpy_object_pybind.h" +#endif // PYDRAKE_USE_NANOBIND namespace drake { @@ -25,10 +68,10 @@ namespace drake { /// "Python Bindings" technical notes. /// /// Drake developers should prefer any aliases defined here over their full -/// spellings in `pybind11`. +/// spellings in `pybind11` or `nanobind`. /// -/// `namespace py` is a shorthand alias to `pybind11` for consistency. (This -/// symbol cannot be exposed directly in Doxygen.) +/// `namespace py` is a shorthand alias to either `pybind11` or `nanobind`, for +/// consistency. (This symbol cannot be exposed directly in Doxygen.) /// /// @note Downstream users should avoid `using namespace drake::pydrake`, as /// this may create ambiguous aliases (especially for GCC). Instead, consider @@ -37,14 +80,41 @@ namespace pydrake { // Note: Doxygen apparently doesn't process comments for namespace aliases. If // you put Doxygen comments here they will apply instead to py_rvp. +#ifdef PYDRAKE_USE_PYBIND11 namespace py = pybind11; +#else // PYDRAKE_USE_NANOBIND +namespace py = nanobind; +#endif /// Shortened alias for py::rv_policy. For more information, see /// the @ref PydrakeReturnValuePolicy "Return Value Policy" section. using py_rvp = py::rv_policy; // This alias helps ease Drake's transition to nanobind. +#ifdef PYDRAKE_USE_PYBIND11 using py::class_; +#else // PYDRAKE_USE_NANOBIND +template +class __attribute__((visibility("hidden"))) class_ + : public py::class_ { + public: + using Base = py::class_; + explicit class_(auto&&... args) + : Base(std::forward(args)..., + py::is_weak_referenceable()) {} +}; +#endif // PYDRAKE_USE_PYBIND11 + +namespace internal { +#ifdef PYDRAKE_USE_PYBIND11 +template +using is_pyobject = py::detail::is_pyobject; +#else // PYDRAKE_USE_NANOBIND +template +using is_pyobject = + std::is_base_of >; +#endif // PYDRAKE_USE_PYBIND11 +} // namespace internal // Implementation for `overload_cast_explicit`. We must use this structure so // that we can constrain what is inferred. Otherwise, the ambiguity confuses @@ -120,12 +190,14 @@ void DefClone(PyClass* ppy_class) { /// /// The set_state functor should take `(Class* self, Pickled pickled)` and /// placement-new construct the object into `self` based on `pickled`, with no -/// return value. (The use of placement new is in anticipation of a nanobind -/// port of this helper function.) +/// return value. template void DefPickle(PyClass* ppy_class, GetState&& get_state, SetState&& set_state) { PyClass& py_class = *ppy_class; - +#ifdef PYDRAKE_USE_NANOBIND + py_class.def("__getstate__", std::forward(get_state)); + py_class.def("__setstate__", std::forward(set_state)); +#else // PYDRAKE_USE_PYBIND11 using Class = typename PyClass::Type; using Pickled = std::invoke_result_t; @@ -143,6 +215,7 @@ void DefPickle(PyClass* ppy_class, GetState&& get_state, SetState&& set_state) { py_class.def(py::pickle( std::forward(get_state), std::move(set_state_with_return))); +#endif // PYDRAKE_USE_NANOBIND } /// Returns a constructor for creating an instance of Class and initializing @@ -157,6 +230,7 @@ void DefPickle(PyClass* ppy_class, GetState&& get_state, SetState&& set_state) { /// @endcode /// /// @tparam Class The C++ class. Must have a default constructor. +#ifdef PYDRAKE_USE_PYBIND11 template auto ParamInit() { return py::init([](py::kwargs kwargs) { @@ -171,6 +245,27 @@ auto ParamInit() { return obj; }); } +#else // PYDRAKE_USE_NANOBIND +template +struct __attribute__((visibility("hidden"))) ParamInit + : py::def_visitor > { + template + void execute(PyClass& cl, const Extra&...) { + cl.def("__init__", [](CppClass* self, py::kwargs kwargs) { + new (self) CppClass(); + py::object py_obj = py::cast(self, py_rvp::reference); + + // Nanobind wouldn't have known the c++ instance is ready yet, but we have + // to mark it ready to allow all of the setattr machinery to work before + // init returns. This also set the object's `destruct` flag to true, so + // that if the _setattr_kwargs raises, `self`'s C++ destructor will run. + py::inst_mark_ready(py_obj); + + py::module_::import_("pydrake").attr("_setattr_kwargs")(py_obj, kwargs); + }); + } +}; +#endif // PYDRAKE_USE_PYBIND11 /// Executes Python code to introduce additional symbols for a given module. /// For a module with local name `{name}` and use_subdir=False, the code @@ -232,17 +327,33 @@ std::shared_ptr make_shared_ptr_from_py_object(py::object py_object) { /// Allow numpy arrays of with dtype=object containing `Type` objects to convert /// to and from Eigen matrices of `Type`. +#ifdef PYDRAKE_USE_PYBIND11 #define PYDRAKE_NUMPY_OBJECT_DTYPE(Type) PYBIND11_NUMPY_OBJECT_DTYPE(Type) +#endif // PYDRAKE_USE_PYBIND11 +// N.B. For PYDRAKE_USE_NANOBIND the numpy_object_pybind.h has already defined +// the PYDRAKE_NUMPY_OBJECT_DTYPE macro. +#ifdef PYDRAKE_USE_PYBIND11 // Legacy synonym for PYDRAKE_NUMPY_OBJECT_DTYPE. Don't use this in new code. #define DRAKE_PYBIND11_NUMPY_OBJECT_DTYPE(Type) \ PYBIND11_NUMPY_OBJECT_DTYPE(Type) +#endif // PYDRAKE_USE_PYBIND11 // These aliases help ease Drake's transition to nanobind. +#ifdef PYDRAKE_USE_PYBIND11 #define PYDRAKE_MODULE PYBIND11_MODULE +#define PYDRAKE_BINDER_NAMESPACE pybind11 #define PYDRAKE_OVERRIDE PYBIND11_OVERRIDE #define PYDRAKE_OVERRIDE_PURE PYBIND11_OVERRIDE_PURE +#else // PYDRAKE_USE_NANOBIND +#define PYDRAKE_MODULE NB_MODULE +#define PYDRAKE_BINDER_NAMESPACE nanobind +#define PYDRAKE_OVERRIDE(unused1, unused2, ...) NB_OVERRIDE(__VA_ARGS__) +#define PYDRAKE_OVERRIDE_PURE(unused1, unused2, ...) \ + NB_OVERRIDE_PURE(__VA_ARGS__) +#endif +#ifdef PYDRAKE_USE_PYBIND11 // This is an implementation of nanobind's NB_TRAMPOLINE macro for pybind11. // https://nanobind.readthedocs.io/en/latest/classes.html#overriding-virtual-functions-in-python // In particular, `size` should match how many PYDRAKE_OVERRIDE{,_PURE} are used @@ -251,3 +362,4 @@ std::shared_ptr make_shared_ptr_from_py_object(py::object py_object) { static_assert(size >= 0); \ using NBBase = base; \ using NBBase::NBBase +#endif // PYDRAKE_USE_PYBIND11 diff --git a/bindings/pydrake/symbolic/symbolic_py_monolith.cc b/bindings/pydrake/symbolic/symbolic_py_monolith.cc index ee96d234aadc..2f4e7947cf35 100644 --- a/bindings/pydrake/symbolic/symbolic_py_monolith.cc +++ b/bindings/pydrake/symbolic/symbolic_py_monolith.cc @@ -291,7 +291,11 @@ void DefineSymbolicMonolith(py::module_ m) { .def( "__iter__", [](const Variables& vars) { - return py::make_iterator(vars.begin(), vars.end()); + return py::make_iterator( +#ifdef PYDRAKE_USE_NANOBIND + py::type(), "iterator", +#endif + vars.begin(), vars.end()); }, // Keep alive, reference: `return` keeps `self` alive py::keep_alive<0, 1>()) diff --git a/bindings/pydrake/symbolic/test/symbolic_test.py b/bindings/pydrake/symbolic/test/symbolic_test.py index 601d24f44edc..1e7b11a83bb0 100644 --- a/bindings/pydrake/symbolic/test/symbolic_test.py +++ b/bindings/pydrake/symbolic/test/symbolic_test.py @@ -2163,7 +2163,7 @@ def test_invalid_variable_id(self): # value, and must be a valid enum in the range [0..7]; adding 8 makes it # invalid. variable_id += 8 << 64 - with self.assertRaisesRegex(ValueError, "Ill-formed Variable::Id"): + with self.assertRaisesRegex(TypeError, "incompatible function arg"): sym.DecomposeAffineExpression(e, {variable_id: 0}) diff --git a/bindings/pydrake/symbolic_types_pybind.h b/bindings/pydrake/symbolic_types_pybind.h index 9235e2e65d78..6982694d0156 100644 --- a/bindings/pydrake/symbolic_types_pybind.h +++ b/bindings/pydrake/symbolic_types_pybind.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "drake/bindings/pydrake/pydrake_pybind.h" #include "drake/common/symbolic/expression.h" #include "drake/common/symbolic/polynomial.h" @@ -29,61 +31,83 @@ class VariableIdPythonAttorney { VariableIdPythonAttorney() = delete; static uint64_t hi(const Variable::Id& id) { return id.hi_; } static uint64_t lo(const Variable::Id& id) { return id.lo_; } - static Variable::Id Construct(uint64_t hi, uint64_t lo) { + static std::optional TryConstruct(uint64_t hi, uint64_t lo) { // We need to maintain Id's invariant that the low byte of hi_ is the // Variable::Type, by rejecting out-of-bounds types. const uint8_t var_type = static_cast(hi); if (var_type > static_cast(Variable::Type::RANDOM_EXPONENTIAL)) { - throw std::domain_error("Ill-formed Variable::Id"); + return std::nullopt; } Variable::Id result; result.hi_ = hi; result.lo_ = lo; - return result; + return {result}; } }; } // namespace symbolic } // namespace drake -namespace pybind11 { +namespace PYDRAKE_BINDER_NAMESPACE { namespace detail { +namespace py = PYDRAKE_BINDER_NAMESPACE; template <> struct type_caster { public: using Attorney = drake::symbolic::VariableIdPythonAttorney; +#ifdef PYDRAKE_USE_PYBIND11 PYBIND11_TYPE_CASTER(drake::symbolic::Variable::Id, _("int")); +#else // PYDRAKE_USE_NANOBIND + NB_TYPE_CASTER(drake::symbolic::Variable::Id, const_name("int")); +#endif - bool load(handle src, bool /* convert */) { +#ifdef PYDRAKE_USE_PYBIND11 + bool load(handle src, bool /* convert */) +#else // PYDRAKE_USE_NANOBIND + bool from_python( + handle src, uint8_t /* flags */, cleanup_list* /* cleanup */) noexcept +#endif + { if (!src) { return false; } - pybind11::int_ concat; + py::int_ concat; try { - concat = pybind11::cast(src); + concat = py::cast(src); } catch (...) { return false; } - const pybind11::object hi_py = concat >> pybind11::int_(64); - const pybind11::object lo_py = concat & pybind11::int_(~uint64_t{}); - const uint64_t hi = pybind11::cast(hi_py); - const uint64_t lo = pybind11::cast(lo_py); - // N.B. "value" is a magic variable declared by pybind11 where we're - // supposed to put the loaded result. - value = Attorney::Construct(hi, lo); + const py::object hi_py = concat >> py::int_(64); + const py::object lo_py = concat & py::int_(~uint64_t{}); + const uint64_t hi = py::cast(hi_py); + const uint64_t lo = py::cast(lo_py); + std::optional maybe_result = + Attorney::TryConstruct(hi, lo); + if (!maybe_result.has_value()) { + return false; + } + // N.B. "value" is a magic variable declared by the ..._TYPE_CASTER macro + // where we're supposed to put the loaded result. + value = *maybe_result; return true; } +#ifdef PYDRAKE_USE_PYBIND11 static handle cast(drake::symbolic::Variable::Id src, rv_policy /* policy */, - handle /* parent */) { - const pybind11::int_ hi_py{Attorney::hi(src)}; - const pybind11::int_ lo_py{Attorney::lo(src)}; - pybind11::object concat = (hi_py << pybind11::int_(64)) + lo_py; + handle /* parent */) +#else // PYDRAKE_USE_NANOBIND + static handle from_cpp(const drake::symbolic::Variable::Id& src, + rv_policy /* policy */, cleanup_list* /* cleanup */) noexcept +#endif + { + const py::int_ hi_py{Attorney::hi(src)}; + const py::int_ lo_py{Attorney::lo(src)}; + py::object concat = (hi_py << py::int_(64)) + lo_py; return concat.release(); } }; } // namespace detail -} // namespace pybind11 +} // namespace PYDRAKE_BINDER_NAMESPACE diff --git a/tools/skylark/README.md b/tools/skylark/README.md index cfb0594f855c..1874b89637c7 100644 --- a/tools/skylark/README.md +++ b/tools/skylark/README.md @@ -109,4 +109,4 @@ to its default value. When "auto", the test's package name is used to choose True vs False. TODO(#21572) Eventually "auto" should imply True in //bindings/pydrake/..., //tutorials/..., and //examples/... and False everywhere else -- but for now -it always means False. +it only implies True in a selected subset of those packages. diff --git a/tools/skylark/drake_py.bzl b/tools/skylark/drake_py.bzl index ad4d2c0b13c6..13c9010df58b 100644 --- a/tools/skylark/drake_py.bzl +++ b/tools/skylark/drake_py.bzl @@ -204,8 +204,19 @@ def drake_py_test( if test_alt_binder not in (True, False, "auto"): fail("test_alt_binder must be set to True, False, or \"auto\"") if test_alt_binder == "auto": - # TODO(#21572) Eventually "auto" should enable relevant tests. - test_alt_binder = False + # TODO(#21572) Eventually "auto" should enable relevant tests (i.e., + # "bindings/pydrake", "examples", and "tutorials"). For now, only a + # selected subset of those will pass. + package_name = native.package_name() + test_alt_binder = any([ + package_name.startswith(x) + for x in [ + "bindings/pydrake/autodiffutils", + "bindings/pydrake/common", + "bindings/pydrake/math", + "bindings/pydrake/symbolic", + ] + ]) if test_alt_binder: alt_target_compatible_with, _ = combine_conditions( name = "alt_binder/" + name,