From e499a20ac82d0e035beb495f012b8ce3af32ce44 Mon Sep 17 00:00:00 2001 From: castor639 <316019788+castor639@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:23:38 +0000 Subject: [PATCH] [primitives] Add input_required option to PassThrough --- .../generated_docstrings/systems_primitives.h | 38 ++++++++++++-- bindings/pydrake/systems/primitives_py.cc | 14 +++-- .../pydrake/systems/test/primitives_test.py | 31 +++++++++++ systems/primitives/pass_through.cc | 15 ++++-- systems/primitives/pass_through.h | 51 ++++++++++++++----- systems/primitives/test/pass_through_test.cc | 51 +++++++++++++++++++ 6 files changed, 174 insertions(+), 26 deletions(-) diff --git a/bindings/generated_docstrings/systems_primitives.h b/bindings/generated_docstrings/systems_primitives.h index 35f1c21b44c5..764ac87b89d1 100644 --- a/bindings/generated_docstrings/systems_primitives.h +++ b/bindings/generated_docstrings/systems_primitives.h @@ -1847,8 +1847,11 @@ However this system incurs no computational cost. The input to this system directly feeds through to its output. The system can also be used to provide default values for a port in -any diagram. If the input port does not have a value, then the default -value passed in the constructor is passed to the output. +any diagram. If the input port does not have a value (and the input is +not required), then the default value passed in the constructor is +passed to the output. Alternatively, the input can be declared as +required, in which case evaluating the output with an unconnected +input will throw. This system is used, for instance, in PidController which is a Diagram composed of simple framework primitives. In this case a PassThrough is @@ -1876,7 +1879,13 @@ R"""(Constructs a pass-through system. Parameter ``vector_size``: number of elements in the signal to be processed. When no input is - connected, the output will be a vector of all zeros.)"""; + connected and ``input_required`` is false, the output will be a + vector of all zeros. + +Parameter ``input_required``: + If true, then evaluating the output with no input connected will + throw. If false (the default), the zero default value is used + instead.)"""; // Source: drake/systems/primitives/pass_through.h const char* doc_1args_value = R"""(Constructs a pass-through system with vector-valued input/output @@ -1884,7 +1893,13 @@ ports. Parameter ``value``: The model value, which defines the size of the ports and serves as - the default when no input is connected.)"""; + the default when no input is connected (unless ``input_required`` + is true). + +Parameter ``input_required``: + If true, then evaluating the output with no input connected will + throw. If false (the default), ``value`` is used as the output + instead.)"""; // Source: drake/systems/primitives/pass_through.h const char* doc_1args_abstract_model_value = R"""(Constructs a pass-through system with abstract-valued input/output @@ -1892,7 +1907,13 @@ ports. Parameter ``abstract_model_value``: A model value, which defines the type of the ports and serves as - the default when no input is connected.)"""; + the default when no input is connected (unless ``input_required`` + is true). + +Parameter ``input_required``: + If true, then evaluating the output with no input connected will + throw. If false (the default), ``abstract_model_value`` is used as + the output instead.)"""; // Source: drake/systems/primitives/pass_through.h const char* doc_copyconvert = R"""(Scalar-type converting copy constructor. See system_scalar_conversion.)"""; @@ -1902,6 +1923,13 @@ R"""(Scalar-type converting copy constructor. See system_scalar_conversion.)"""; // Source: drake/systems/primitives/pass_through.h const char* doc = R"""(Returns the sole input port.)"""; } get_input_port; + // Symbol: drake::systems::PassThrough::input_required + struct /* input_required */ { + // Source: drake/systems/primitives/pass_through.h + const char* doc = +R"""(Returns true iff the input port must be connected before evaluating +the output.)"""; + } input_required; } PassThrough; // Symbol: drake::systems::PerceptronActivationType struct /* PerceptronActivationType */ { diff --git a/bindings/pydrake/systems/primitives_py.cc b/bindings/pydrake/systems/primitives_py.cc index 8e6d5951511b..cdf445b70b22 100644 --- a/bindings/pydrake/systems/primitives_py.cc +++ b/bindings/pydrake/systems/primitives_py.cc @@ -469,12 +469,18 @@ PYDRAKE_MODULE(primitives, m) { DefineTemplateClassWithDefault, LeafSystem>( m, "PassThrough", GetPyParam(), doc.PassThrough.doc) - .def(py::init(), py::arg("vector_size"), + .def(py::init(), py::arg("vector_size"), + py::arg("input_required") = false, doc.PassThrough.ctor.doc_1args_vector_size) - .def(py::init&>(), py::arg("value"), + .def(py::init&, bool>(), + py::arg("value"), py::arg("input_required") = false, doc.PassThrough.ctor.doc_1args_value) - .def(py::init(), py::arg("abstract_model_value"), - doc.PassThrough.ctor.doc_1args_abstract_model_value); + .def(py::init(), + py::arg("abstract_model_value"), + py::arg("input_required") = false, + doc.PassThrough.ctor.doc_1args_abstract_model_value) + .def("input_required", &PassThrough::input_required, + doc.PassThrough.input_required.doc); DefineTemplateClassWithDefault, LeafSystem>( m, "PortSwitch", GetPyParam(), doc.PortSwitch.doc) diff --git a/bindings/pydrake/systems/test/primitives_test.py b/bindings/pydrake/systems/test/primitives_test.py index f35eb22301ec..8b654a4c47ee 100644 --- a/bindings/pydrake/systems/test/primitives_test.py +++ b/bindings/pydrake/systems/test/primitives_test.py @@ -433,6 +433,7 @@ def test_bus_selector(self, T): def test_vector_pass_through(self): model_value = BasicVector([1.0, 2, 3]) system = PassThrough(vector_size=model_value.size()) + self.assertFalse(system.input_required()) context = system.CreateDefaultContext() system.get_input_port(0).FixValue(context, model_value) output = system.AllocateOutput() @@ -445,6 +446,7 @@ def test_vector_pass_through(self): def test_default_vector_pass_through(self): model_value = [1.0, 2, 3] system = PassThrough(value=model_value) + self.assertFalse(system.input_required()) context = system.CreateDefaultContext() np.testing.assert_array_equal( model_value, system.get_output_port().Eval(context) @@ -453,6 +455,7 @@ def test_default_vector_pass_through(self): def test_abstract_pass_through(self): model_value = Value("Hello world") system = PassThrough(abstract_model_value=model_value) + self.assertFalse(system.input_required()) context = system.CreateDefaultContext() system.get_input_port(0).FixValue(context, model_value) output = system.AllocateOutput() @@ -462,6 +465,34 @@ def test_abstract_pass_through(self): output_value = output.get_data(0) compare_value(self, output_value, model_value) + def test_pass_through_required_input(self): + model_value = [1.0, 2, 3] + input_value = [4.0, 5, 6] + system = PassThrough(value=model_value, input_required=True) + self.assertTrue(system.input_required()) + context = system.CreateDefaultContext() + with self.assertRaisesRegex(RuntimeError, "not connected"): + system.get_output_port().Eval(context) + system.get_input_port().FixValue(context, input_value) + np.testing.assert_array_equal( + input_value, system.get_output_port().Eval(context) + ) + + abstract_system = PassThrough( + abstract_model_value=Value("default"), input_required=True + ) + self.assertTrue(abstract_system.input_required()) + abstract_context = abstract_system.CreateDefaultContext() + with self.assertRaisesRegex(RuntimeError, "not connected"): + abstract_system.get_output_port().Eval(abstract_context) + abstract_system.get_input_port().FixValue( + abstract_context, Value("connected") + ) + self.assertEqual( + abstract_system.get_output_port().Eval(abstract_context), + "connected", + ) + def test_port_switch(self): system = PortSwitch(vector_size=2) a = system.DeclareInputPort(name="a") diff --git a/systems/primitives/pass_through.cc b/systems/primitives/pass_through.cc index dd96e262760b..e2640a49ee74 100644 --- a/systems/primitives/pass_through.cc +++ b/systems/primitives/pass_through.cc @@ -8,10 +8,12 @@ namespace systems { template PassThrough::PassThrough( const Eigen::Ref& model_vector, - std::unique_ptr abstract_model_value) + std::unique_ptr abstract_model_value, + bool input_required) : LeafSystem(SystemTypeTag()), model_vector_(model_vector), - abstract_model_value_(std::move(abstract_model_value)) { + abstract_model_value_(std::move(abstract_model_value)), + input_required_(input_required) { if (!is_abstract()) { input_port_ = &this->DeclareVectorInputPort("u", BasicVector(model_vector)); @@ -38,7 +40,8 @@ template PassThrough::PassThrough(const PassThrough& other) : PassThrough(other.model_vector_, other.is_abstract() ? other.abstract_model_value_->Clone() - : nullptr) {} + : nullptr, + other.input_required_) {} template PassThrough::~PassThrough() = default; @@ -47,7 +50,8 @@ template void PassThrough::DoCalcVectorOutput(const Context& context, BasicVector* output) const { DRAKE_ASSERT(!is_abstract()); - if (this->get_input_port().HasValue(context)) { + if (input_required_ || this->get_input_port().HasValue(context)) { + // When input_required_ is true and the port is unconnected, Eval throws. const auto& input = this->get_input_port().Eval(context); DRAKE_ASSERT(input.size() == output->size()); output->get_mutable_value() = input; @@ -60,7 +64,8 @@ template void PassThrough::DoCalcAbstractOutput(const Context& context, AbstractValue* output) const { DRAKE_ASSERT(is_abstract()); - if (this->get_input_port().HasValue(context)) { + if (input_required_ || this->get_input_port().HasValue(context)) { + // When input_required_ is true and the port is unconnected, Eval throws. output->SetFrom( this->get_input_port().template Eval(context)); } else { diff --git a/systems/primitives/pass_through.h b/systems/primitives/pass_through.h index 412061b045f1..b3fe5f0f5b49 100644 --- a/systems/primitives/pass_through.h +++ b/systems/primitives/pass_through.h @@ -15,8 +15,10 @@ namespace systems { /// directly feeds through to its output. /// /// The system can also be used to provide default values for a port in any -/// diagram. If the input port does not have a value, then the default value -/// passed in the constructor is passed to the output. +/// diagram. If the input port does not have a value (and the input is not +/// required), then the default value passed in the constructor is passed to +/// the output. Alternatively, the input can be declared as required, in which +/// case evaluating the output with an unconnected input will throw. /// /// This system is used, for instance, in PidController which is a Diagram /// composed of simple framework primitives. In this case a PassThrough is used @@ -46,21 +48,40 @@ class PassThrough final : public LeafSystem { /// Constructs a pass-through system. /// @param vector_size number of elements in the signal to be processed. - /// When no input is connected, the output will be a vector of all zeros. - explicit PassThrough(int vector_size) - : PassThrough(Eigen::VectorXd::Zero(vector_size), nullptr) {} + /// When no input is connected and `input_required` is false, the output will + /// be a vector of all zeros. + /// @param input_required If true, then evaluating the output with no input + /// connected will throw. If false (the default), the zero default value is + /// used instead. + /// @pydrake_mkdoc_identifier{1args_vector_size} + explicit PassThrough(int vector_size, bool input_required = false) + : PassThrough(Eigen::VectorXd::Zero(vector_size), nullptr, + input_required) {} /// Constructs a pass-through system with vector-valued input/output ports. /// @param value The model value, which defines the size of the ports and - /// serves as the default when no input is connected. - explicit PassThrough(const Eigen::Ref& value) - : PassThrough(value, nullptr) {} + /// serves as the default when no input is connected (unless `input_required` + /// is true). + /// @param input_required If true, then evaluating the output with no input + /// connected will throw. If false (the default), `value` is used as the + /// output instead. + /// @pydrake_mkdoc_identifier{1args_value} + explicit PassThrough(const Eigen::Ref& value, + bool input_required = false) + : PassThrough(value, nullptr, input_required) {} /// Constructs a pass-through system with abstract-valued input/output ports. /// @param abstract_model_value A model value, which defines the type of the - /// ports and serves as the default when no input is connected. - explicit PassThrough(const AbstractValue& abstract_model_value) - : PassThrough(Vector0(), abstract_model_value.Clone()) {} + /// ports and serves as the default when no input is connected (unless + /// `input_required` is true). + /// @param input_required If true, then evaluating the output with no input + /// connected will throw. If false (the default), `abstract_model_value` is + /// used as the output instead. + /// @pydrake_mkdoc_identifier{1args_abstract_model_value} + explicit PassThrough(const AbstractValue& abstract_model_value, + bool input_required = false) + : PassThrough(Vector0(), abstract_model_value.Clone(), + input_required) {} /// Scalar-type converting copy constructor. /// See @ref system_scalar_conversion. @@ -78,6 +99,10 @@ class PassThrough final : public LeafSystem { return *input_port_; } + /// Returns true iff the input port must be connected before evaluating the + /// output. + bool input_required() const { return input_required_; } + private: // Allow different specializations to access each other's private data. template @@ -85,7 +110,8 @@ class PassThrough final : public LeafSystem { // All of the other constructors delegate here. PassThrough(const Eigen::Ref& model_vector, - std::unique_ptr abstract_model_value); + std::unique_ptr abstract_model_value, + bool input_required); /// Sets the output port to equal the input port. void DoCalcVectorOutput(const Context& context, @@ -99,6 +125,7 @@ class PassThrough final : public LeafSystem { const Eigen::VectorXd model_vector_; const std::unique_ptr abstract_model_value_; + const bool input_required_; // We store our port pointer so that DoCalcVectorOutput's access to the // input_port_->Eval is inlined (without any port-count bounds checking). diff --git a/systems/primitives/test/pass_through_test.cc b/systems/primitives/test/pass_through_test.cc index 8d245578c8be..dad27166754f 100644 --- a/systems/primitives/test/pass_through_test.cc +++ b/systems/primitives/test/pass_through_test.cc @@ -6,6 +6,7 @@ #include #include "drake/common/autodiff.h" +#include "drake/common/test_utilities/expect_throws_message.h" #include "drake/math/autodiff_gradient.h" #include "drake/systems/framework/basic_vector.h" #include "drake/systems/framework/fixed_input_port_value.h" @@ -60,6 +61,9 @@ class PassThroughTest make_unique>(Value(size)); } } + EXPECT_FALSE( + dynamic_cast&>(*pass_through_) + .input_required()); context_ = pass_through_->CreateDefaultContext(); } @@ -152,6 +156,53 @@ GTEST_TEST(PassThroughTest, AutoDiffFromDouble) { math::DiscardZeroGradient(pass.get_output_port().Eval(*context))); } +GTEST_TEST(PassThroughRequiredInputTest, VectorRequiredConnected) { + const Eigen::Vector3d value(1.0, 2.0, 3.0); + const Eigen::Vector3d input(4.0, 5.0, 6.0); + PassThrough dut(value, true); + EXPECT_TRUE(dut.input_required()); + auto context = dut.CreateDefaultContext(); + dut.get_input_port().FixValue(context.get(), input); + EXPECT_EQ(dut.get_output_port().Eval(*context), input); +} + +GTEST_TEST(PassThroughRequiredInputTest, VectorRequiredUnconnectedThrows) { + PassThrough dut(3, true); + EXPECT_TRUE(dut.input_required()); + auto context = dut.CreateDefaultContext(); + DRAKE_EXPECT_THROWS_MESSAGE(dut.get_output_port().Eval(*context), + ".*required InputPort.*\\(u\\).*is not connected.*"); +} + +GTEST_TEST(PassThroughRequiredInputTest, AbstractRequiredConnected) { + const Eigen::Vector3d value(1.0, 2.0, 3.0); + const Eigen::Vector3d input(4.0, 5.0, 6.0); + PassThrough dut(Value(value), true); + EXPECT_TRUE(dut.input_required()); + auto context = dut.CreateDefaultContext(); + dut.get_input_port().FixValue(context.get(), SimpleAbstractType(input)); + EXPECT_EQ(dut.get_output_port().Eval(*context).value(), + input); +} + +GTEST_TEST(PassThroughRequiredInputTest, AbstractRequiredUnconnectedThrows) { + PassThrough dut(Value(3), true); + EXPECT_TRUE(dut.input_required()); + auto context = dut.CreateDefaultContext(); + DRAKE_EXPECT_THROWS_MESSAGE(dut.get_output_port().Eval(*context), + ".*required InputPort.*\\(u\\).*is not connected.*"); +} + +GTEST_TEST(PassThroughRequiredInputTest, ScalarConversionPreservesFlag) { + PassThrough dut(2, true); + EXPECT_TRUE(is_autodiffxd_convertible(dut, [](const auto& converted) { + EXPECT_TRUE(converted.input_required()); + })); + EXPECT_TRUE(is_symbolic_convertible(dut, [](const auto& converted) { + EXPECT_TRUE(converted.input_required()); + })); +} + } // namespace } // namespace systems } // namespace drake