diff --git a/bindings/generated_docstrings/systems_analysis.h b/bindings/generated_docstrings/systems_analysis.h index 0c5baf580c47..7c604af15faa 100644 --- a/bindings/generated_docstrings/systems_analysis.h +++ b/bindings/generated_docstrings/systems_analysis.h @@ -4340,6 +4340,11 @@ See also: Initialize(), AdvancePendingEvents(), SimulatorStatus, set_monitor())"""; } AdvanceTo; + // Symbol: drake::systems::Simulator::EmplaceWithSharedContext + struct /* EmplaceWithSharedContext */ { + // Source: drake/systems/analysis/simulator.h + const char* doc = R"""()"""; + } EmplaceWithSharedContext; // Symbol: drake::systems::Simulator::GetCurrentWitnessTimeIsolation struct /* GetCurrentWitnessTimeIsolation */ { // Source: drake/systems/analysis/simulator.h @@ -4445,11 +4450,6 @@ Returns ``status``: See also: AdvanceTo(), AdvancePendingEvents(), SimulatorStatus)"""; } Initialize; - // Symbol: drake::systems::Simulator::MakeWithSharedContext - struct /* MakeWithSharedContext */ { - // Source: drake/systems/analysis/simulator.h - const char* doc = R"""()"""; - } MakeWithSharedContext; // Symbol: drake::systems::Simulator::ResetStatistics struct /* ResetStatistics */ { // Source: drake/systems/analysis/simulator.h diff --git a/bindings/pydrake/multibody/inverse_kinematics_py_differential.cc b/bindings/pydrake/multibody/inverse_kinematics_py_differential.cc index cb3544c06f99..a4a83e519bb2 100644 --- a/bindings/pydrake/multibody/inverse_kinematics_py_differential.cc +++ b/bindings/pydrake/multibody/inverse_kinematics_py_differential.cc @@ -334,15 +334,18 @@ void DefineDifferentialIkSystem(py::module_ m) { using Class = DifferentialInverseKinematicsSystem; cls // BR - .def(py::init([](py::object recipe, std::string_view task_frame, - py::object collision_checker, const DofMask& active_dof, - double time_step, double K_VX, - const SpatialVelocity& Vd_TG_limit) { - return std::make_unique( - make_shared_ptr_from_py_object(recipe), task_frame, - make_shared_ptr_from_py_object(collision_checker), - active_dof, time_step, K_VX, Vd_TG_limit); - }), + .def( + "__init__", + [](Class* self, py::object recipe, std::string_view task_frame, + py::object collision_checker, const DofMask& active_dof, + double time_step, double K_VX, + const SpatialVelocity& Vd_TG_limit) { + new (self) Class(make_shared_ptr_from_py_object(recipe), + task_frame, + make_shared_ptr_from_py_object( + collision_checker), + active_dof, time_step, K_VX, Vd_TG_limit); + }, py::arg("recipe"), py::arg("task_frame"), py::arg("collision_checker"), py::arg("active_dof"), py::arg("time_step"), py::arg("K_VX"), py::arg("Vd_TG_limit"), diff --git a/bindings/pydrake/systems/analysis_py.cc b/bindings/pydrake/systems/analysis_py.cc index 580bd545858e..794cafc54330 100644 --- a/bindings/pydrake/systems/analysis_py.cc +++ b/bindings/pydrake/systems/analysis_py.cc @@ -291,24 +291,28 @@ PYDRAKE_MODULE(analysis, m) { auto cls = DefineTemplateClassWithDefault>( m, "Simulator", GetPyParam(), doc.Simulator.doc); cls // BR - .def(py::init([](const System& system, py::object py_context) { - // Handle the two cases for context ownership explicitly: - // 1. If py_context is None, create a new context and take ownership. - // 2. If py_context is provided, use the existing Python wrapper - // directly (it already owns the C++ object). - if (py_context.is_none()) { - std::unique_ptr> context_ptr = - system.CreateDefaultContext(); - // Use take_ownership because we just created this context and need - // Python to own it. The unique_ptr is released, leaving the raw - // pointer with no owner until take_ownership establishes Python - // ownership. - py_context = - py::cast(context_ptr.release(), py_rvp::take_ownership); - } - return Simulator::MakeWithSharedContext( - system, make_shared_ptr_from_py_object>(py_context)); - }), + .def( + "__init__", + [](Simulator* self, const System& system, + py::object py_context) { + // Handle the two cases for context ownership explicitly: + // 1. If py_context is None, create a new context and take + // ownership. + // 2. If py_context is provided, use the existing Python wrapper + // directly (it already owns the C++ object). + if (py_context.is_none()) { + std::unique_ptr> context_ptr = + system.CreateDefaultContext(); + // Use take_ownership because we just created this context and + // need Python to own it. The unique_ptr is released, leaving + // the raw pointer with no owner until take_ownership + // establishes Python ownership. + py_context = + py::cast(context_ptr.release(), py_rvp::take_ownership); + } + Simulator::EmplaceWithSharedContext(self, system, + make_shared_ptr_from_py_object>(py_context)); + }, py::arg("system"), py::arg("context") = py::none(), // Keep alive, reference: `self` keeps `system` alive. py::keep_alive<1, 2>(), diff --git a/systems/analysis/BUILD.bazel b/systems/analysis/BUILD.bazel index 27388a47898b..b21ea728a231 100644 --- a/systems/analysis/BUILD.bazel +++ b/systems/analysis/BUILD.bazel @@ -522,6 +522,7 @@ drake_cc_googletest( ":implicit_euler_integrator", ":runge_kutta3_integrator", ":simulator", + "//common:scope_exit", "//common/test_utilities:expect_throws_message", "//common/test_utilities:is_dynamic_castable", "//systems/analysis/test_utilities:controlled_spring_mass_system", diff --git a/systems/analysis/simulator.cc b/systems/analysis/simulator.cc index cf91d763e494..012237eea516 100644 --- a/systems/analysis/simulator.cc +++ b/systems/analysis/simulator.cc @@ -22,11 +22,10 @@ Simulator::Simulator(std::unique_ptr> owned_system, : Simulator(nullptr, std::move(owned_system), std::move(context), true) {} template -std::unique_ptr> Simulator::MakeWithSharedContext( - const System& system, std::shared_ptr> context) { - // This slightly odd spelling allows access to the private constructor. - return std::unique_ptr>( - new Simulator(&system, nullptr, std::move(context), false)); +void Simulator::EmplaceWithSharedContext( + Simulator* self, const System& system, + std::shared_ptr> context) { + new (self) Simulator(&system, nullptr, std::move(context), false); } template diff --git a/systems/analysis/simulator.h b/systems/analysis/simulator.h index 4e9d854507ca..252adfcb1fb2 100644 --- a/systems/analysis/simulator.h +++ b/systems/analysis/simulator.h @@ -275,14 +275,20 @@ class Simulator { std::unique_ptr> context = nullptr); #ifndef DRAKE_DOXYGEN_CXX - // (Internal use only) Makes a Simulator which accepts a context via shared - // pointer. + // (Internal use only) Emplaces a Simulator which accepts a context via + // shared pointer. + // + // The `self` parameter should point to uninitialized storage of at least + // sizeof(Simulator) bytes. It is the calling code's responsibility to + // eventually destroy the Simulator instance, using (for example) + // std::destroy_at. // // The shared pointer signature is useful for implementing pydrake memory // management, because it permits supplying a custom deleter. The context is // not *actually* shared. The simulator will modify it at will. - static std::unique_ptr> MakeWithSharedContext( - const System& system, std::shared_ptr> context); + static void EmplaceWithSharedContext(Simulator* self, + const System& system, + std::shared_ptr> context); #endif // TODO(sherm1) Make Initialize() attempt to satisfy constraints. diff --git a/systems/analysis/test/simulator_test.cc b/systems/analysis/test/simulator_test.cc index 5c972100fe37..a254c22253e0 100644 --- a/systems/analysis/test/simulator_test.cc +++ b/systems/analysis/test/simulator_test.cc @@ -18,6 +18,7 @@ #include "drake/common/autodiff.h" #include "drake/common/drake_assert.h" #include "drake/common/drake_copyable.h" +#include "drake/common/scope_exit.h" #include "drake/common/test_utilities/expect_throws_message.h" #include "drake/common/test_utilities/is_dynamic_castable.h" #include "drake/common/text_logging.h" @@ -814,9 +815,9 @@ GTEST_TEST(SimulatorTest, SecondConstructor) { EXPECT_EQ(simulator.get_context().get_time(), 3.0); } -// Tests the internal use (for Python) factory method that takes the context via -// shared pointer. -GTEST_TEST(SimulatorTest, SharedContextFactoryMethod) { +// Tests in-place construction factory method that takes the context via shared +// pointer. (This method is internal use only for Python.) +GTEST_TEST(SimulatorTest, SharedContextEmplacementFactoryMethod) { // Create the spring-mass system and context. analysis_test::MySpringMassSystem spring_mass(1.0, 1.0, 0.0); auto context = spring_mass.CreateDefaultContext(); @@ -825,9 +826,15 @@ GTEST_TEST(SimulatorTest, SharedContextFactoryMethod) { context->SetTime(7.0); std::shared_ptr> shared_context(std::move(context)); - // Construct the simulator with the created context. - auto simulator = Simulator::MakeWithSharedContext( - spring_mass, std::move(shared_context)); + // Construct the simulator via placement new with the created context. + std::array)> storage; + Simulator* simulator = + reinterpret_cast*>(storage.data()); + ScopeExit guard([&]() { + std::destroy_at(simulator); + }); + Simulator::EmplaceWithSharedContext(simulator, spring_mass, + std::move(shared_context)); // Verify that context values are equivalent. EXPECT_EQ(simulator->get_context().get_time(), 7.0);