Skip to content

Commit

Permalink
[systems] Add diagram life support (#22132)
Browse files Browse the repository at this point in the history
This patch adds a life support interface that allows arbitrary objects
to track the lifetime of systems as they get transferred from a diagram
builder to a diagram.
  • Loading branch information
rpoyner-tri authored Nov 14, 2024
1 parent 273f884 commit 3613367
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 3 deletions.
5 changes: 5 additions & 0 deletions systems/framework/diagram.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,10 @@ Diagram<T>::ConvertScalarType() const {
// Move the new systems into the blueprint.
blueprint->systems = std::move(new_systems);

// Do nothing about life_support. Since scalar conversion is effectively a
// deep copy, the lifetime extensions provided by life_support are not needed
// here.

return blueprint;
}

Expand Down Expand Up @@ -1551,6 +1555,7 @@ void Diagram<T>::Initialize(std::unique_ptr<Blueprint> blueprint) {
connection_map_ = std::move(blueprint->connection_map);
output_port_ids_ = std::move(blueprint->output_port_ids);
registered_systems_ = std::move(blueprint->systems);
life_support_ = std::move(blueprint->life_support);

// This cache entry just maintains temporary storage. It is only ever used
// by DoCalcNextUpdateTime(). Since this declaration of the cache entry
Expand Down
23 changes: 20 additions & 3 deletions systems/framework/diagram.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <any>
#include <functional>
#include <map>
#include <memory>
Expand All @@ -12,6 +13,7 @@
#include "drake/common/default_scalars.h"
#include "drake/common/drake_copyable.h"
#include "drake/common/pointer_cast.h"
#include "drake/common/string_map.h"
#include "drake/systems/framework/diagram_context.h"
#include "drake/systems/framework/diagram_continuous_state.h"
#include "drake/systems/framework/diagram_discrete_values.h"
Expand All @@ -27,9 +29,9 @@ namespace systems {

namespace internal {

/// Destroys owned systems in the reverse order they were added; this enables
/// Systems to refer to each other during destruction, in the usual "undo"
/// resource order one would expect for C++.
// Destroys owned systems in the reverse order they were added; this enables
// Systems to refer to each other during destruction, in the usual "undo"
// resource order one would expect for C++.
template <typename T>
class OwnedSystems {
public:
Expand Down Expand Up @@ -60,6 +62,17 @@ class OwnedSystems {
std::vector<std::unique_ptr<System<T>>> vec_;
};

// External life support data for the diagram. The data will be moved to the
// diagram at Build() time. Data stored here will have a life-cycle that is the
// union of the builder and the diagram.
//
// This mechanism is particularly useful for the Python FFI. It can be used to
// extend the lifetime of Python-wrapped systems, when the Build() call occurs
// in C++ that is not exposed to the Python bindings.
struct DiagramLifeSupport {
string_map<std::any> attributes;
};

} // namespace internal

template <typename T>
Expand Down Expand Up @@ -528,6 +541,8 @@ class Diagram : public System<T>, internal::SystemParentServiceInterface {
std::map<InputPortLocator, OutputPortLocator> connection_map;
// All of the systems to be included in the diagram.
internal::OwnedSystems<T> systems;

internal::DiagramLifeSupport life_support;
};

// Constructs a Diagram from the Blueprint that a DiagramBuilder produces.
Expand Down Expand Up @@ -603,6 +618,8 @@ class Diagram : public System<T>, internal::SystemParentServiceInterface {
// allocated as a cache entry to avoid heap operations during simulation.
CacheIndex event_times_buffer_cache_index_{};

internal::DiagramLifeSupport life_support_;

// For all T, Diagram<T> considers DiagramBuilder<T> a friend, so that the
// builder can set the internal state correctly.
friend class DiagramBuilder<T>;
Expand Down
1 change: 1 addition & 0 deletions systems/framework/diagram_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ std::unique_ptr<typename Diagram<T>::Blueprint> DiagramBuilder<T>::Compile() {
blueprint->output_port_names = output_port_names_;
blueprint->connection_map = connection_map_;
blueprint->systems = std::move(registered_systems_);
blueprint->life_support = std::move(life_support_);

already_built_ = true;

Expand Down
10 changes: 10 additions & 0 deletions systems/framework/diagram_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,14 @@ class DiagramBuilder {
/// as more ports are exported.
int num_output_ports() const;

/// (Internal use only). Returns a mutable reference to life support data for
/// the diagram. The data will be moved to the diagram at Build() time. Data
/// stored here will have a life-cycle that is the union of the builder and
/// the diagram.
internal::DiagramLifeSupport& get_mutable_life_support() {
return life_support_;
}

private:
// Declares a new input to the entire Diagram, using @p model_input to
// supply the data type. @p name is an optional name for the input port; if
Expand Down Expand Up @@ -533,6 +541,8 @@ class DiagramBuilder {
std::unordered_set<const System<T>*> systems_;
// The Systems in this DiagramBuilder, in the order they were registered.
internal::OwnedSystems<T> registered_systems_;

internal::DiagramLifeSupport life_support_;
};

} // namespace systems
Expand Down
29 changes: 29 additions & 0 deletions systems/framework/test/diagram_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4084,6 +4084,35 @@ GTEST_TEST(ImplicitTimeDerivatives, DiagramProcessing) {
EXPECT_EQ(residual, expected_result);
}

// Life support data has a lifetime that is the union of the builder and the
// resulting diagram.
GTEST_TEST(LifeSupport, Lifetime) {
std::weak_ptr<int> spy;
{
auto do_build = [&]() {
auto thing = std::make_shared<int>(42);
spy = thing;
DiagramBuilder<double> builder;
builder.get_mutable_life_support().attributes.emplace("thing",
std::move(thing));
// Thing is living inside builder.
EXPECT_FALSE(builder.get_mutable_life_support().attributes.empty());
EXPECT_FALSE(spy.expired());
builder.AddSystem<EmptySystem<double>>();
auto result = builder.Build();
// Thing has been transferred to diagram.
EXPECT_TRUE(builder.get_mutable_life_support().attributes.empty());
EXPECT_FALSE(spy.expired());
return result;
};
auto diagram = do_build();
// Builder is gone; thing remains.
EXPECT_FALSE(spy.expired());
}
// Diagram is gone; thing is now also gone.
EXPECT_TRUE(spy.expired());
}

} // namespace
} // namespace systems
} // namespace drake

0 comments on commit 3613367

Please sign in to comment.