From ee0b1b1df64fe798b8580b9214a83a737067bedb Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Wed, 26 Aug 2026 07:36:54 +0000 Subject: [PATCH 01/31] Add BusPartitionInterface to define partition boundaries --- .../BusPartitionInterface.cpp | 365 ++++++++++++++++++ .../BusPartitionInterface.hpp | 164 ++++++++ .../PartitionInterface/CMakeLists.txt | 9 + .../PartitionInterface/PartitionInterface.hpp | 29 ++ 4 files changed, 567 insertions(+) create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp new file mode 100644 index 0000000000..d4619f3f16 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp @@ -0,0 +1,365 @@ + +#include "BusPartitionInterface.hpp" + +#include +#include +#include + +#include +#include + +namespace GridKit +{ + + /** + * @brief Construct a partition interface between a bus and a circuit component. + * + * The interface wraps a copy of a component located across a partition + * boundary and evaluates the contributions that the component makes to the + * connected bus. All interface variables are treated as external variables + * and have the same size as the wrapped component. + * + * + * @param bus Bus associated with the partition interface. + * @param component Copy of the component across the partition boundary. + * @param id Unique identifier for the interface. + */ + template + BusPartitionInterface::BusPartitionInterface(node_type* bus, component_type* component, IdxT id) + : component_(component->clone()), + bus_(bus) + { + size_ = component_->size(); + n_intern_ = 0; + n_extern_ = static_cast(component_->size()); + idc_ = id; + + // All variables of the bus interface are external to the interface. + for (IdxT i = 0; i < size_; i++) + { + extern_indices_.insert(i); + } + + // Map each global bus connection index to its position in the bus. + std::unordered_map bus_connections; + + for (size_t i = 0; i < static_cast(bus_->size()); ++i) + { + bus_connections[bus_->getNodeConnection(i).idx_] = i; + } + + // The interface Jacobian contains only rows corresponding to variables + // owned by the bus. + const IdxT* coo_rows = component_->jacobianCooRows(); + + nnz_ = 0; + + for (IdxT k = 0; k < component_->nnz(); ++k) + { + const IdxT row_node = component_->getNodeConnection(static_cast(coo_rows[k])); + + if (bus_connections.contains(row_node)) + { + ++nnz_; + jac_map_.push_back(k); // Keep track of the entries so they can be easily extracted + } + } + } + + template + BusPartitionInterface::~BusPartitionInterface() + { + delete component_; + } + + /** + * @brief Allocate storage and initialize the interface mappings. + * + * Identifies the external variables of the wrapped component that are + * connected to the bus and builds the mappings used to transfer residual + * contributions between the component and the interface. Private storage is + * also allocated for the internal variables of the wrapped component. + * + * @return 0 on success and a nonzero value if the component does not contain + * all variables required by the bus interface. + */ + template + int BusPartitionInterface::allocate() + { + + CircuitComponent::allocate(); + + // Build a lookup of the global indices belonging to the bus. + std::unordered_map bus_connections; + + for (size_t i = 0; i < static_cast(bus_->size()); ++i) + { + bus_connections[bus_->getNodeConnection(i).idx_] = i; + } + + const auto& external_indices = component_->getExternIndices(); + + is_external_.assign(static_cast(size_), false); + + bus_input_ports_.clear(); + bus_output_ports_.clear(); + + size_t external_index = 0; + + // Identify which component variables are external and which of those external + // variables are connected to the bus. bus_input_ports_ stores the corresponding + // variable position in this interface, while bus_output_ports_ stores its position + // in the wrapped component's external residual vector. + for (size_t i = 0; i < static_cast(size_); ++i) + { + const IdxT connection_index = component_->getNodeConnection(i); + + this->setConnectionNodes(i, connection_index); + + if (!external_indices.contains(static_cast(i))) + { + continue; + } + + is_external_[i] = true; + + if (bus_connections.contains(connection_index)) + { + bus_input_ports_.push_back(i); + bus_output_ports_.push_back(external_index); + } + + ++external_index; + } + + // A valid bus interface must contain every variable belonging to the bus. + // If fewer bus variables are found, the wrapped component does not provide + // the complete coupling required by this interface. + const size_t bus_size = static_cast(bus_->size()); + + if (bus_input_ports_.size() != bus_size) + { + GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " + << "Bus(ID=" << bus_->busID() + << "), Component(ID=" << component_->getIDcomponent() + << "). Expected " << bus_size + << " bus connections, but found " + << bus_input_ports_.size() << "." + << std::endl; + + return 1; + } + + // The wrapped component is evaluated independently by the interface. + // Allocate storage for its internal variables and residuals and redirect + // its internal pointers to this private storage. + const size_t internal_size = static_cast(component_->getInternalSize()); + + component_y_int_ = std::make_unique(internal_size); + component_yp_int_ = std::make_unique(internal_size); + component_f_int_ = std::make_unique(internal_size); + + component_->setInternalPointer(component_y_int_.get()); + component_->setInternalDerivativePointer(component_yp_int_.get()); + component_->setInternalResidualPointer(component_f_int_.get()); + + component_f_ext_ = std::make_unique(component_->getExternSize()); + + return 0; + } + + template + int BusPartitionInterface::setAbsoluteTolerance(RealT rel_tol) + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + /** + * @brief Initialize the partition interface. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::initialize() + { + return 0; + } + + /** + * @brief Identify differential variables + */ + template + int BusPartitionInterface::tagDifferentiable() + { + return 0; + } + + /** + * @brief Eval Internal Residual + */ + template + int BusPartitionInterface::evaluateInternalResidual() + { + return 0; + } + + /** + * @brief Evaluate the wrapped component's contributions to the bus residual. + * + * The wrapped component is evaluated using state information supplied through + * the interface. Residual contributions associated with bus variables are + * extracted from the component's external residual and accumulated into the + * corresponding interface residual entries. + * + * @return 0 on success, or the error code returned by the wrapped component. + */ + template + int BusPartitionInterface::evaluateExternalResidual() + { + std::fill_n(component_f_ext_.get(), component_->getExternSize(), ScalarT{}); + + updateComponentPointers(); + + if (int err_code = component_->evaluateExternalResidual()) + { + return err_code; + } + + // Only contributions associated with the bus are accumulated + // into the interface residual below. + for (size_t i = 0; i < bus_input_ports_.size(); ++i) + { + *f_ext_[bus_input_ports_[i]] += component_f_ext_[bus_output_ports_[i]]; + } + + return 0; + } + + /** + * @brief Evaluate the Jacobian contributions associated with the bus. + * + * Evaluates the Jacobian of the wrapped component and extracts the entries + * whose residual rows correspond to variables belonging to the connected bus. + * The selected entries are then used to assemble the interface Jacobian. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::evaluateJacobian() + { + + this->zeroJacMatrix(); + + updateComponentPointers(); + + component_->evaluateJacobian(); + + const IdxT* cooRows = component_->jacobianCooRows(); + const IdxT* cooCols = component_->jacobianCooCols(); + const RealT* cooVals = component_->jacobianCooValues(); + + std::vector rows; + std::vector cols; + std::vector vals; + + rows.reserve(jac_map_.size()); + cols.reserve(jac_map_.size()); + vals.reserve(jac_map_.size()); + + // Extract only the Jacobian entries whose residual rows belong to the bus. + for (const IdxT index : jac_map_) + { + rows.push_back(cooRows[index]); + cols.push_back(cooCols[index]); + vals.push_back(cooVals[index]); + } + + this->setJacValues(rows, cols, vals); + + return 0; + } + + /** + * @brief Update the wrapped component with state and output residual pointers. + * + * Reconstructs the state required to evaluate the wrapped component using + * data supplied through the interface. External component variables are + * connected directly to the interface data, while internal component + * variables are copied into the interface's private storage, which the wrapped + * component already keeps track of for its internal state. + * + * @pre The interface must be allocated and its external state pointers must + * reference valid state and derivative data. + * + * @post The wrapped component is configured with the state, derivative, and + * residual pointers required for evaluation. + * + * @param residual Storage for the component's external residual contributions, + * or nullptr when residual output is not required. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::updateComponentPointers() + { + size_t internal_index = 0; + size_t external_index = 0; + + // Reconstruct the state expected by the wrapped component from the + // interface's external data. Route the external residual to component_f_ext_. + for (size_t i = 0; i < static_cast(component_->size()); ++i) + { + if (is_external_[i]) + { + ExternalConnection connection{ + .y_ = y_ext_[i], + .yp_ = yp_ext_[i], + .f_ = &component_f_ext_[external_index], + .idx_ = component_->getNodeConnection(i)}; + + component_->setExternalConnectionNodes(i, connection); + ++external_index; + } + else + { + component_y_int_[internal_index] = *y_ext_[i]; + component_yp_int_[internal_index] = *yp_ext_[i]; + ++internal_index; + } + } + + return 0; + } + + template + int BusPartitionInterface::evaluateIntegrand() + { + return 0; + } + + template + int BusPartitionInterface::initializeAdjoint() + { + return 0; + } + + template + int BusPartitionInterface::evaluateAdjointResidual() + { + return 0; + } + + template + int BusPartitionInterface::evaluateAdjointIntegrand() + { + return 0; + } + + // Available template instantiations + template class BusPartitionInterface; + template class BusPartitionInterface; + template class BusPartitionInterface; + template class BusPartitionInterface; + +} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp new file mode 100644 index 0000000000..aa832095b7 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp @@ -0,0 +1,164 @@ + + +#pragma once + +#include + +#include +#include +#include + +namespace GridKit +{ + /** + * @brief Partition interface that provides a wrapped component's current + * injection contributions to a bus. + * + * A BusPartitionInterface is used when a bus and one of its connected + * components belong to different partitions. The interface represents the + * component within the partition containing the bus and provides the current + * injection contributions that the wrapped component makes to that bus. + * + * The interface has the same number of variables as the wrapped component, + * with a one-to-one correspondence between interface variables and wrapped + * component variables. However, all variables are external from the + * interface's point of view, regardless of whether the corresponding variable + * is internal or external to the wrapped component: + * + * @code + * Interface variables: [ext_, ext_, ext_, ext_, ext_, ext_, ext_] + * | | | | | | | + * Wrapped component: [int_, int_, ext_, int_, ext_, ext_, ext_] + * @endcode + * + * This allows the partition containing the wrapped component to provide all + * state information required to reconstruct and evaluate the component on the + * bus side of the partition boundary. Variables that are internal to the + * wrapped component are stored in private interface storage during evaluation, + * while variables that are external to the wrapped component are connected + * directly to the corresponding interface data. + * + * After evaluating the wrapped component, the interface extracts the residual + * and Jacobian contributions associated with the bus and exposes them to the + * partition containing the bus. + * + * * @note The interface residual contributions are zero for all variables except + * those connected to the bus. Only variables connected to the bus expose + * the wrapped component's contribution to the partition. + * + * @note This interface must be added to the partition containing the bus + * to ensure that the system is properly partitioned. + * + * @tparam ScalarT Scalar type used by the model. + * @tparam IdxT Index type used for variables and connections. + */ + template + class BusPartitionInterface : public PartitionInterface + { + + using component_type = CircuitComponent; + using node_type = typename PowerElectronics::NodeBase; + using RealT = typename CircuitComponent::RealT; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::time_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::tag_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::g_; + using CircuitComponent::yB_; + using CircuitComponent::ypB_; + using CircuitComponent::fB_; + using CircuitComponent::gB_; + using CircuitComponent::param_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + using CircuitComponent::connection_nodes_; + + public: + BusPartitionInterface(node_type* bus, component_type* component, IdxT id); + virtual ~BusPartitionInterface(); + + int allocate() final; + int initialize() final; + int tagDifferentiable() final; + int setAbsoluteTolerance(RealT) final; + int evaluateInternalResidual() final; + int evaluateExternalResidual() final; + int evaluateJacobian() final; + int evaluateIntegrand() final; + + int initializeAdjoint() final; + int evaluateAdjointResidual() final; + // int evaluateAdjointJacobian(); + int evaluateAdjointIntegrand() final; + + private: + int updateComponentPointers(); + + /** + * @brief Circuit component participating in the partition split. + */ + component_type* component_; + + /** + * @brief Bus associated with the partition boundary. + */ + node_type* bus_; + + /** + * @brief Storage for the wrapped component's internal state variables. + */ + std::unique_ptr component_y_int_; + + /** + * @brief Storage for the wrapped component's internal state derivatives. + */ + std::unique_ptr component_yp_int_; + + /** + * @brief Storage for the wrapped component's internal residual values. + */ + std::unique_ptr component_f_int_; + + /** + * @brief Storage for the wrapped component's external residual values. + */ + std::unique_ptr component_f_ext_; + + /** + * @brief Wrapped component ports that receive variables from the bus. + */ + std::vector bus_input_ports_; + + /** + * @brief Wrapped component ports whose residual contributions are exposed + * to the bus. + */ + std::vector bus_output_ports_; + + /** + * @brief Mapping from interface Jacobian entries to wrapped component + * Jacobian entries. + */ + std::vector jac_map_; + + /** + * @brief Cached lookup indicating whether each wrapped component variable is external. + * + * Avoids repeatedly searching `extern_indices_` when determining whether + * a wrapped component variable is internal or external. + */ + std::vector is_external_; + }; +} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt new file mode 100644 index 0000000000..8d833c6dd6 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -0,0 +1,9 @@ +gridkit_add_library( + power_elec_partition_interfaces + SOURCES BusPartitionInterface.cpp + HEADERS BusPartitionInterface.hpp PartitionInterface.hpp + LINK_LIBRARIES + PUBLIC + GridKit::dense_vector + PUBLIC + GridKit::utilities_logger) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp new file mode 100644 index 0000000000..e76930e649 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp @@ -0,0 +1,29 @@ + + +#pragma once + +#include +#include +#include + +namespace GridKit +{ + /*! + * @brief Base class for partition interface components. + * + * A partition interface is simply a component that is designed to aid in + * marking partition boundary to facilitate partion evaluation and + * Jacobian assembly. All partition interface that will be implemented + * in the future should inherit from this class and all common features + * should be factored here. + */ + template + class PartitionInterface : public CircuitComponent + { + public: + PartitionInterface() = default; + + ~PartitionInterface() = default; + }; + +} // namespace GridKit From ca6fe4451015e582463bebc491bdabee928b1cd9 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Wed, 26 Aug 2026 07:44:41 +0000 Subject: [PATCH 02/31] Add SubsystemModel for partition evaluation --- GridKit/Model/PowerElectronics/CMakeLists.txt | 2 + .../Model/PowerElectronics/SubsystemModel.hpp | 844 ++++++++++++++++++ .../SystemModelPowerElectronics.hpp | 527 +++++++++++ 3 files changed, 1373 insertions(+) create mode 100644 GridKit/Model/PowerElectronics/SubsystemModel.hpp create mode 100644 GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp diff --git a/GridKit/Model/PowerElectronics/CMakeLists.txt b/GridKit/Model/PowerElectronics/CMakeLists.txt index 66c058cfa5..9cb8c946ab 100644 --- a/GridKit/Model/PowerElectronics/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(TransmissionLine) add_subdirectory(MicrogridLoad) add_subdirectory(MicrogridLine) add_subdirectory(MicrogridBusDQ) +add_subdirectory(PartitionInterface) install( FILES Component.hpp @@ -28,4 +29,5 @@ install( SystemModel.hpp NodeBase.hpp ExternalConnection.hpp + SubsystemModel.hpp DESTINATION include/GridKit/Model/PowerElectronics) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp new file mode 100644 index 0000000000..9ee1eaee18 --- /dev/null +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -0,0 +1,844 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + + /** + * @brief Represents a subset of a PowerElectronicsModel that can be evaluated + * independently. + * + * A SubsystemModel contains a collection of existing GridKit components and + * nodes taken from a larger system. Variables owned by those components and + * nodes become internal variables of the subsystem. Variables needed by those + * components but owned outside the subsystem become external coupling + * variables. + * + * Components normally store connection indices in the global system indexing. + * During subsystem allocation, these indices are temporarily replaced with a + * contiguous local subsystem indexing so that the subsystem can be evaluated + * like an independent PowerElectronicsModel. + * + * External coupling values must be supplied before residual or Jacobian + * evaluation, either directly through the external-data vectors or through a + * forcing function. + * + * @tparam ScalarT Scalar type used by the model. + * @tparam IdxT Index type used for variable and connection indices. + */ + template + class SubsystemModel : public PowerElectronicsModel + { + public: + struct ForcingData + { + std::vector y; + std::vector yp; + }; + + using TimeFunction = std::function; + + protected: + using SystemModel = PowerElectronicsModel; + using RealT = typename CircuitComponent::RealT; + using CsrMatrixT = typename CircuitComponent::CsrMatrixT; + using component_type = CircuitComponent; + using node_type = PowerElectronics::NodeBase; + using interface_type = PartitionInterface; + + using SystemModel::abs_tol_; + using SystemModel::allocated_; + using SystemModel::allocateVectors; + using SystemModel::alpha_; + using SystemModel::connection_nodes_; + using SystemModel::f_int_; + using SystemModel::n_extern_; + using SystemModel::n_intern_; + using SystemModel::nnz_; + using SystemModel::size_; + using SystemModel::tag_; + using SystemModel::time_; + using SystemModel::y_int_; + using SystemModel::yp_int_; + + using SystemModel::components_; + using SystemModel::csr_jac_; + using SystemModel::jac_call_count_; + using SystemModel::map_to_csr_; + using SystemModel::neg1_; + using SystemModel::nodes_; + using SystemModel::use_jac_; + + public: + /** + * @brief Default constructor for the system model + * + * @post System model parameters set as default + */ + + SubsystemModel() + : SystemModel(false) + { + } + + /** + * @brief Default constructor for the system model + * + * @post System model parameters set as default + */ + + SubsystemModel(bool use_jac) + : SystemModel(use_jac) + { + } + + ~SubsystemModel() override + { + for (auto* comp : interfaces_) + { + delete comp; + } + // SubsystemModel does not own components_/nodes_ — they belong to (and are + // deleted by) the parent system model. + components_.clear(); + nodes_.clear(); + } + + /** + * @brief Allocate the subsystem for independent evaluation. + * + * Converts the selected components and nodes from global system indexing to + * local subsystem indexing, allocates storage for the subsystem's internal + * variables, and creates storage for coupling variables that lie outside the + * subsystem. + * + * Component pointers are then redirected to either the subsystem's internal + * vectors or its external coupling-data vectors. Finally, the subsystem + * Jacobian structure is assembled using only entries whose row and column + * belong to internal subsystem variables. + * + * @pre Components and nodes added to the subsystem must already be allocated + * by the parent system. + * + * @post Component and node connection indices use local subsystem indexing, + * and the subsystem is ready for residual and Jacobian evaluation. + * + * @return 0 on success, otherwise an error code returned during allocation. + */ + int allocate() override + { + if (int err_code = buildIndexMappings()) + { + return err_code; + } + + if (int err_code = mapGlobalToLocal()) + { + return err_code; + } + + n_intern_ = internal_map_.size(); + n_extern_ = external_map_.size(); + size_ = n_intern_; + + // Allocate subsystem vectors. + y_ext_data_.resize(n_extern_); + yp_ext_data_.resize(n_extern_); + f_ext_data_.resize(n_extern_); + + connection_nodes_ = std::make_unique(size_); + if (!allocated_) + { + allocateVectors(static_cast(size_), true); + abs_tol_.setToZero(memory::HOST); + } + + tag_.resize(size_); + + external_data_indices_.resize(n_extern_); + + // Store the mapping from local subsystem indices back to their global system indices + for (const auto [global_idx, local_idx] : internal_map_) + { + this->setConnectionNodes(local_idx, global_idx); + } + + // Store the global indices of all external coupling variables. + for (const auto [global_idx, local_idx] : external_map_) + { + external_data_indices_[local_idx - n_intern_] = global_idx; + } + + { // Start node internal indexing after all component internals for proper KLU ordering + size_t node_internal_idx; + for (node_type* node : nodes_) + { + for (size_t i = 0; i < node->getInternalSize(); i++) + { + node_internal_idx = node->getNodeConnection(i).idx_; + + ExternalConnection node_connection{ + .y_ = y_int_ + node_internal_idx, + .yp_ = yp_int_ + node_internal_idx, + .f_ = f_int_ + node_internal_idx, + .idx_ = static_cast(node_internal_idx)}; + + node->setExternalConnectionNodes(i, node_connection); + } + } + } + + { + // The offset for each component's internal variables in the system vector. + // They start at 0, and are stacked on top of each other. + size_t component_internal_idx = 0; + for (component_type* comp : components_) + { + // Update component internal pointers to their correct offsets + comp->setInternalPointer(&y_int_[component_internal_idx]); + comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); + comp->setInternalResidualPointer(&f_int_[component_internal_idx]); + + component_internal_idx += comp->getInternalSize(); + + const auto& external_indices = comp->getExternIndices(); + + for (IdxT local_index : external_indices) + { + const IdxT connection_index = comp->getNodeConnection(local_index); + + // A variable can be external from the component's point of view while still + // being owned by this subsystem. In that case, connect the component directly + // to the corresponding entry in the subsystem's internal vectors. + if (connection_index < n_intern_) + { + ExternalConnection connection{ + .y_ = y_int_ + connection_index, + .yp_ = yp_int_ + connection_index, + .f_ = f_int_ + connection_index, + .idx_ = connection_index}; + + comp->setExternalConnectionNodes(local_index, connection); + + continue; + } + + // Otherwise the variable is owned outside this subsystem. Connect the + // component to the subsystem's external coupling-data storage instead. + const IdxT external_offset = connection_index - static_cast(n_intern_); + + ExternalConnection connection{ + .y_ = &y_ext_data_[external_offset], + .yp_ = &yp_ext_data_[external_offset], + .f_ = &f_ext_data_[external_offset], + .idx_ = connection_index}; + + comp->setExternalConnectionNodes(local_index, connection); + } + } + } + + // Allocation always rebuilds the system Jacobian and its COO-to-CSR map. + delete csr_jac_; + csr_jac_ = nullptr; + + delete[] map_to_csr_; + map_to_csr_ = nullptr; + + // Evaluate component Jacobians to get sparsity + for (component_type* component : components_) + { + component->evaluateJacobian(); + } + + // Check whether a Jacobian entry belongs to the subsystem Jacobian. + // Only entries whose row and column are both internal subsystem + // variables are retained. + auto isValidEntry = [this](IdxT row, IdxT col) + { + if (row == neg1_ || col == neg1_) + { + return false; + } + + const bool row_is_internal = row < this->getInternalSize(); + const bool col_is_internal = col < this->getInternalSize(); + + return (row_is_internal && col_is_internal); + }; + + IdxT nnz_dup = 0; + + for (const component_type* component : components_) + { + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const IdxT nnz = component->nnz(); + + for (IdxT i = 0; i < nnz; ++i) + { + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + if (isValidEntry(row, col)) + { + ++nnz_dup; + } + } + } + + // Allocate COO triplet arrays (we own these until we hand off to CsrMatrix) + IdxT* rows_dup = new IdxT[nnz_dup]; + IdxT* cols_dup = new IdxT[nnz_dup]; + RealT* vals_dup = new RealT[nnz_dup]; + + IdxT counter = 0; + + for (const component_type* component : components_) + { + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const RealT* v = component->jacobianCooValues(); + const IdxT nnz = component->nnz(); + + for (IdxT i = 0; i < nnz; ++i) + { + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + if (!isValidEntry(row, col)) + { + continue; + } + + rows_dup[counter] = row; + cols_dup[counter] = col; + vals_dup[counter] = v[i]; + + ++counter; + } + } + + // Build the system COO Jacobian + LinearAlgebra::CooMatrix jac(size_, size_, nnz_dup, &rows_dup, &cols_dup, &vals_dup); + + // Populate CSR data with sort and deduplicate + IdxT* row_ptrs = jac.getCsrRowData(); + + // Deduplicated nnz + nnz_ = jac.getNnz(); + + // Allocate cols/vals with deduplicated nnz + IdxT* cols = new IdxT[nnz_]; + RealT* vals = new RealT[nnz_]; + + std::copy(jac.getColData(), jac.getColData() + nnz_, cols); + std::copy(jac.getValues(), jac.getValues() + nnz_, vals); + + // Create the CSR Jacobian + csr_jac_ = new CsrMatrixT(size_, size_, nnz_, &row_ptrs, &cols, &vals); + + const IdxT* map_to_sorted = jac.getMapToSorted(); + const IdxT* map_to_dedup = jac.getMapToDeduplicated(); + + // Build a mappping from original COO index to CSR index + map_to_csr_ = new IdxT[nnz_dup]; + for (IdxT i = 0; i < nnz_dup; ++i) + { + map_to_csr_[map_to_sorted[i]] = map_to_dedup[i]; + } + + allocated_ = true; + return 0; + } + + /** + * @brief Update the subsystem external state and derivative data. + * + * If a forcing function is provided, evaluate it at the current subsystem + * time and copy the returned coupling values into the external state vectors. + * + * @post y_ext_data_ and yp_ext_data_ contain the external coupling values + * returned by the forcing function, if one is set. + * + * @throws std::runtime_error If the forcing function returns vectors whose + * sizes do not match the subsystem external-data vectors. + * + * @return 0 on success. + */ + int distributeExternalVectors() + { + + if (forcing_function_) + { + const auto forcing = (*forcing_function_)(time_); + + if (forcing.y.size() != y_ext_data_.size() || forcing.yp.size() != yp_ext_data_.size()) + { + throw std::runtime_error( + "SubsystemModel::distributeExternalVectors: forcing function " + "returned vectors with incorrect sizes."); + } + + std::copy(forcing.y.begin(), forcing.y.end(), y_ext_data_.begin()); + std::copy(forcing.yp.begin(), forcing.yp.end(), yp_ext_data_.begin()); + } + + return 0; + } + + /** + * @brief Evaluate Residuals at each component then collect them + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateInternalResidual() override + { + if (int err_code = distributeExternalVectors()) + { + return err_code; + } + + return SystemModel::evaluateInternalResidual(); + } + + /** + * @brief Creates the system Jacobian representing \f$\alpha dF/dy' + dF/dy\f$ + * + * Updates the CSR Jacobian values using the per-component mappings + * computed during allocate(). + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateJacobian() override + { + if (int err_code = distributeExternalVectors()) + { + return err_code; + } + + return SystemModel::evaluateJacobian(); + } + + /** + * @brief Add a component to the subsystem. + * + * Rejected while connections are in the local-indexed state, since the + * component's stored connection indices would otherwise be interpreted + * inconsistently with the rest of the subsystem. Call release() first. + * + * @param[in] component Component to add. + */ + void addComponent(component_type* component) + { + if (!component->isAllocated()) + { + throw std::logic_error( + "SubsystemModel::addComponent: cannot add an unallocated component."); + } + + if (connections_are_local_) + { + throw std::logic_error( + "SubsystemModel::addComponent: cannot add component while " + "in local-indexed state. Call release() first."); + } + + SystemModel::addComponent(component); + } + + /** + * @brief Add a node to the subsystem. + * + * Rejected while connections are in the local-indexed state, since the + * node's stored connection indices would otherwise be interpreted + * inconsistently with the rest of the subsystem. Call release() first. + * + * @param[in] node Node to add. + */ + void addNode(node_type* node) + { + if (!node->isAllocated()) + { + throw std::logic_error( + "SubsystemModel::addNode: cannot add an unallocated node."); + } + + if (connections_are_local_) + { + throw std::logic_error( + "SubsystemModel::addNode: cannot add node while in " + "local-indexed state. Call release() first."); + } + + SystemModel::addNode(node); + } + + /** + * @brief Add a partition interface to the subsystem. + * + * Adds the partition interface to the subsystem's component list and keeps a + * separate reference to it in the interface list. + * + * @param component Pointer to the interface component to add. + */ + void addInterface(interface_type* component) + { + addComponent(component); + interfaces_.push_back(component); + } + + /** + * @brief Restore the subsystem topology to global system indexing. + * + * Reverses local subsystem connection indices with the original + * global system indices. The local internal/external mappings are + * then cleared so that components or nodes can safely be added or removed. + * + * @pre Component and node connections may use local subsystem indices. + * + * @post Component and node connections use their original global indices, + * the subsystem mappings are cleared, and the subsystem is marked + * unallocated. + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int release() + { + if (int err_code = mapLocalToGlobal()) + { + return err_code; + } + + internal_map_.clear(); + external_map_.clear(); + + allocated_ = false; + + return 0; + } + + const std::vector& getExternalDataIndices() const + { + return external_data_indices_; + } + + std::vector& getExternalDataY() + { + return y_ext_data_; + } + + std::vector& getExternalDataYP() + { + return yp_ext_data_; + } + + std::vector& getExternalDataF() + { + return f_ext_data_; + } + + void setForcingFunction(TimeFunction function) + { + forcing_function_ = std::move(function); + } + + const std::unordered_map& getInternalMap() const + { + return internal_map_; + } + + const std::unordered_map& getExternalMap() const + { + return external_map_; + } + + private: + /** + * @brief Restore local subsystem connection indices to global system indices. + * + * Reverses \ref mapGlobalToLocal(). Internal subsystem indices are translated + * through the subsystem connection-node table, while external subsystem + * indices are translated through external_data_indices_. + * + * The component/node connectivity is unchanged; only the index representation + * is restored. + * + * @pre Component and node connections use local subsystem indices. + * + * @post Component and node connections use their original global indices. + * + * @return 0 on success. + */ + int mapLocalToGlobal() + { + if (!connections_are_local_) + { + return 0; + } + + for (component_type* component : components_) + { + + for (IdxT i = 0; i < component->size(); i++) + { + const IdxT index = component->getNodeConnection(i); + + if (index == neg1_) + { + continue; + } + else if (index < this->getInternalSize()) + { + component->setConnectionNodes(i, this->getNodeConnection(index)); + } + else + { + component->setConnectionNodes(i, external_data_indices_[index - this->getInternalSize()]); + } + } + } + + for (node_type* node : nodes_) + { + + for (IdxT i = 0; i < node->size(); i++) + { + const IdxT index = node->getNodeConnection(i).idx_; + + if (index == neg1_) + { + continue; + } + node->setConnectionNodes(i, this->getNodeConnection(index)); + } + } + + connections_are_local_ = false; + + return 0; + } + + /** + * @brief Replace global connection indices with subsystem-local indices. + * + * Uses the mappings created by \ref buildIndexMappings() to rewrite the connection + * indices stored by every component and node. Variables owned by the subsystem + * use `internal_map_`; variables owned outside the subsystem use `external_map_`. + * + * This changes only the indexing used to identify connections; it does not + * change the physical component/node connectivity. + * + * @pre `internal_map_` and `external_map_` have been constructed from global + * connection indices. + * + * @post All valid component and node connections use local subsystem indices. + * + * @return 0 on success. + */ + int mapGlobalToLocal() + { + if (connections_are_local_) + { + return 0; + } + + for (component_type* component : components_) + { + for (IdxT i = 0; i < component->size(); ++i) + { + const IdxT index = component->getNodeConnection(i); + + if (index == neg1_) + { + continue; + } + + if (internal_map_.contains(index)) + { + component->setConnectionNodes(i, internal_map_.at(index)); + } + else + { + component->setConnectionNodes(i, external_map_.at(index)); + } + } + } + + for (node_type* node : nodes_) + { + for (IdxT i = 0; i < node->size(); ++i) + { + const IdxT index = node->getNodeConnection(i).idx_; + + if (index == neg1_) + { + continue; + } + + node->setConnectionNodes(i, internal_map_.at(index)); + } + } + + connections_are_local_ = true; + + return 0; + } + + /** + * @brief Build the global-to-local variable mappings for the subsystem. + * + * Examines the connection indices of all components and nodes in the + * subsystem and divides the referenced variables into two groups: + * + * - Internal variables are owned by a component or node in this subsystem. + * - External variables are required by a subsystem component but are owned + * outside the subsystem. + * + * Internal variables receive local indices first. External coupling variables + * are then assigned indices immediately after the internal range. This gives + * every variable referenced by the subsystem a unique local index while + * preserving its original global index in the corresponding map. + * + * @pre Component and node connections use global system indices. + * + * @post internal_map_ and external_map_ contain the global-to-local mappings + * needed to convert the subsystem topology to local indexing. + * + * @return 0 on success. + */ + int buildIndexMappings() + { + + if (connections_are_local_) + { + return 0; + } + + internal_map_.clear(); + external_map_.clear(); + + size_t component_internal_idx = 0; + // Pass 1: Add variables owned internally by subsystem components. + for (component_type* comp : components_) + { + const auto& extern_indices = comp->getExternIndices(); + + for (IdxT i = 0; i < comp->size(); i++) + { + const IdxT index = comp->getNodeConnection(i); + + if (index != neg1_ && !extern_indices.contains(i)) + { + internal_map_[index] = component_internal_idx++; + } + } + } + + // Pass 2: Add variables owned by subsystem nodes. + for (node_type* node : nodes_) + { + + for (IdxT i = 0; i < node->size(); i++) + { + const IdxT index = node->getNodeConnection(i).idx_; + + if (index != neg1_) + { + internal_map_[index] = component_internal_idx++; + } + } + } + + // Pass 3: Add component dependencies that are owned outside the subsystem. + size_t component_external_idx = component_internal_idx; + for (component_type* comp : components_) + { + auto extern_indices = comp->getExternIndices(); + for (IdxT j = 0; j < comp->size(); j++) + { + if (!extern_indices.contains(j)) + { + continue; + } + + const IdxT index = comp->getNodeConnection(j); + + if (index != neg1_ && !internal_map_.contains(index) && !external_map_.contains(index)) + { + external_map_[index] = component_external_idx++; + } + } + } + + return 0; + } + + /** + *@brief Maps global system indices to local subsystem indices for internal variables. + */ + std::unordered_map internal_map_; + + /** + * @brief Maps global system indices to local subsystem indices for external variables. + */ + std::unordered_map external_map_; + + /** + * @brief Global system index corresponding to each entry in the external subsystem vectors. + */ + std::vector external_data_indices_; + + /** + * @brief subsystem external State, derivative, and residual vectors. + */ + std::vector y_ext_data_; + + /** + * @brief subsystem external derivative + */ + std::vector yp_ext_data_; + + /** + * @brief subsystem external derivative + */ + std::vector f_ext_data_; + + /** + * @brief Optional forcing function used to provide external subsystem data. + * + * It is continuous function that can be sampled at different times + */ + std::optional forcing_function_; + + /** + * @brief Partition interfaces owned by the subsystem. + * + * These interfaces expose the contributions of components participating + * in the partition split. + */ + std::vector interfaces_; + + /** + * @brief Keeps track of whether the components are in local state or global state + */ + bool connections_are_local_{false}; + + }; // class SubsystemModel + +} // namespace GridKit \ No newline at end of file diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp new file mode 100644 index 0000000000..20111ace90 --- /dev/null +++ b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp @@ -0,0 +1,527 @@ + + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace GridKit +{ + template + class PowerElectronicsModel : public CircuitComponent + { + using RealT = typename CircuitComponent::RealT; + using CsrMatrixT = typename CircuitComponent::CsrMatrixT; + using component_type = CircuitComponent; + using node_type = PowerElectronics::NodeBase; + + using CircuitComponent::abs_tol_; + using CircuitComponent::allocated_; + using CircuitComponent::allocateVectors; + using CircuitComponent::alpha_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + using CircuitComponent::nnz_; + using CircuitComponent::size_; + using CircuitComponent::tag_; + using CircuitComponent::time_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + + public: + /** + * @brief Constructor for the system model + * + * @param[in] use_jac Boolean to choose if to use jacobian + * + * @post System model parameters set as input + */ + PowerElectronicsModel(bool use_jac = false) + { + // Can choose if to use jacobian + use_jac_ = use_jac; + } + + /** + * @brief Destructor for the system model + * + * @pre System components are allocated + * + * @post System components are deallocated + * + */ + virtual ~PowerElectronicsModel() + { + for (auto comp : this->components_) + { + delete comp; + } + delete csr_jac_; + delete[] map_to_csr_; + } + + /** + * @brief Will check if each component has jacobian avalible. If one doesn't have it, return false. + * + * + * @return true if all components have jacobian + * @return false otherwise + */ + bool hasJacobian() final + { + if (!this->use_jac_) + return false; + + for (const auto& component : components_) + { + if (!component->hasJacobian()) + { + return false; + } + } + return true; + } + + /** + * @brief Allocate system vectors and construct the system CSR Jacobian + * + * @post System model vectors allocated with the computed total number of unknowns + * @post CSR Jacobian sparsity pattern is computed + * @post COO->CSR mapping is computed + * @post Every component's \ref CircuitComponent::y_int_, \ref CircuitComponent::yp_int_, and \ref CircuitComponent::f_int_ pointers + * are set to their appropriate offsets in the system vector, allowing them to directly access their internal variables, derivatives, + * and residuals. + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int allocate() override + { + size_t component_internal_size = 0; + for (component_type* comp : components_) + { + component_internal_size += comp->getInternalSize(); + } + + size_t node_internal_size = 0; + for (node_type* node : nodes_) + { + node_internal_size += node->getInternalSize(); + } + + n_intern_ = component_internal_size + node_internal_size; + n_extern_ = 0; + size_ = n_intern_ + n_extern_; + + // Allocation always rebuilds the system Jacobian and its COO-to-CSR map. + delete csr_jac_; + csr_jac_ = nullptr; + + delete[] map_to_csr_; + map_to_csr_ = nullptr; + + if (!allocated_) + { + allocateVectors(static_cast(size_), true); + // Component and node offsets can change when topology is modified. + abs_tol_.setToZero(memory::HOST); + } + + tag_.resize(size_); + + { // Start node internal indexing after all component internals for proper KLU ordering + size_t node_internal_idx = component_internal_size; + for (node_type* node : nodes_) + { + node->allocate(); + + for (size_t i = 0; i < node->getInternalSize(); i++) + { + ExternalConnection node_connection{ + .y_ = y_int_ + node_internal_idx, + .yp_ = yp_int_ + node_internal_idx, + .f_ = f_int_ + node_internal_idx, + .idx_ = static_cast(node_internal_idx)}; + + node->setExternalConnectionNodes(i, node_connection); + node_internal_idx++; + } + } + } + + { + // The offset for each component's internal variables in the system vector. + // They start at 0, and are stacked on top of each other. + size_t component_internal_idx = 0; + for (component_type* comp : components_) + { + comp->allocate(); + + // Update component internal pointers to their correct offsets + comp->setInternalPointer(&y_int_[component_internal_idx]); + comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); + comp->setInternalResidualPointer(&f_int_[component_internal_idx]); + + const auto& external_indices = comp->getExternIndices(); + for (IdxT i = 0; i < comp->size(); i++) + { + if (!external_indices.contains(i)) + { + comp->setInternalConnectionNodes(i, component_internal_idx); + component_internal_idx++; + } + } + } + } + + // Evaluate component Jacobians to get sparsity + for (component_type* component : components_) + { + component->evaluateJacobian(); + } + + // Count the number of non-zeros + IdxT nnz_dup = 0; + for (const component_type* component : components_) + { + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + IdxT nnz = component->nnz(); + + for (IdxT i = 0; i < nnz; ++i) + { + if (component->getNodeConnection(r[i]) != neg1_ && component->getNodeConnection(c[i]) != neg1_) + { + ++nnz_dup; + } + } + } + + // Allocate COO triplet arrays (we own these until we hand off to CsrMatrix) + IdxT* rows_dup = new IdxT[nnz_dup]; + IdxT* cols_dup = new IdxT[nnz_dup]; + RealT* vals_dup = new RealT[nnz_dup]; + + IdxT counter = 0; + for (const component_type* component : components_) + { + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const RealT* v = component->jacobianCooValues(); + IdxT nnz = component->nnz(); + + for (IdxT i = 0; i < nnz; ++i) + { + if (component->getNodeConnection(r[i]) != neg1_ && component->getNodeConnection(c[i]) != neg1_) + { + rows_dup[counter] = component->getNodeConnection(r[i]); + cols_dup[counter] = component->getNodeConnection(c[i]); + vals_dup[counter] = v[i]; + counter++; + } + } + } + + // Build the system COO Jacobian + LinearAlgebra::CooMatrix jac(size_, size_, nnz_dup, &rows_dup, &cols_dup, &vals_dup); + + // Populate CSR data with sort and deduplicate + IdxT* row_ptrs = jac.getCsrRowData(); + + // Deduplicated nnz + nnz_ = jac.getNnz(); + + // Allocate cols/vals with deduplicated nnz + IdxT* cols = new IdxT[nnz_]; + RealT* vals = new RealT[nnz_]; + + std::copy(jac.getColData(), jac.getColData() + nnz_, cols); + std::copy(jac.getValues(), jac.getValues() + nnz_, vals); + + // Create the CSR Jacobian + csr_jac_ = new CsrMatrixT(size_, size_, nnz_, &row_ptrs, &cols, &vals); + + const IdxT* map_to_sorted = jac.getMapToSorted(); + const IdxT* map_to_dedup = jac.getMapToDeduplicated(); + + // Build a mappping from original COO index to CSR index + map_to_csr_ = new IdxT[nnz_dup]; + for (IdxT i = 0; i < nnz_dup; ++i) + { + map_to_csr_[map_to_sorted[i]] = map_to_dedup[i]; + } + + allocated_ = true; + return 0; + } + + /** + * @brief Set intial y and y' of each component + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int initialize() + { + // Initialize components + for (const auto& component : components_) + { + component->initialize(); + } + + return CircuitComponent::initialize(); + } + + /** + * @brief Tags all system variables as differentiable, based on what the + * components that own those variables tag them as. + * + * Starts by asking all components to tag their differentiables. This implementation + * assumes all node variables are algebraic, and will not ask nodes to tag their differentiables. + * Sets all variables to algebraic (`false`) to start, then loops over all component internal variables. + * Re-creates the same internal variable to system variables mapping as in \ref allocate() - all + * internal variables from the same component are stored contiguously in a block, and blocks are + * stored contiguously in the same order as \ref components_, with node variables at the end. + * Each internal variable's tag in the system is set to its tag in the component. + */ + int tagDifferentiable() + { + // Ask all component to tag their differentiables + for (size_t i = 0; i < components_.size(); i++) + { + component_type* component = components_[i]; + + // Bubble up errors if necessary + if (int err = component->tagDifferentiable()) + { + return err; + } + } + + // Fill tags with a default value (false) for node variables. Assumed to be algebraic here. + std::fill(tag_.begin(), tag_.end(), false); + + // Copy tags for internal variables from their components - going in the order as described above + size_t idx = 0; + for (component_type* comp : components_) + { + const auto& external_indices = comp->getExternIndices(); + + // Loop over all component variables - including externals + for (IdxT i = 0; i < comp->size(); i++) + { + // Discard externals + if (!external_indices.contains(i)) + { + tag_[idx] = comp->tag()[i]; + + // Ensures internal variables are contiguous, and in the same order as the component + idx++; + } + } + } + + return 0; + } + + /** + * @brief Compute the absolute tolerance for each variable in the model + * + * @param rel_tol The relative tolerance which can be used to pick the + * absolute tolerance. + * @tparam ScalarT Scalar data type + * @tparam IdxT Index data type + * @return int 0 if successful, non-zero otherwise. + * + * This represents a "noise" level close to zero for which pure relative + * error cannot be used. + */ + int setAbsoluteTolerance(RealT rel_tol) final + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + /** + * @brief Evaluate Residuals at each component then collect them + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateInternalResidual() override + { + for (IdxT i = 0; i < size_; i++) + { + f_int_[i] = 0.0; + } + + // Update system residual vector + + // Evaluate component internal residuals - this is embarassingly parallel + for (component_type* component : components_) + { + if (int err_code = component->evaluateInternalResidual()) + return err_code; + } + + for (component_type* component : components_) + { + if (int err_code = component->evaluateExternalResidual()) + return err_code; + } + + return 0; + } + + /** + * @todo implement this for nested systems + */ + int evaluateExternalResidual() final + { + return 0; + } + + /** + * @brief Creates the system Jacobian representing \f$\alpha dF/dy' + dF/dy\f$ + * + * Updates the CSR Jacobian values using the per-component mappings + * computed during allocate(). + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateJacobian() override + { + // Zero out values + RealT* vals = csr_jac_->getValues(); + for (IdxT i = 0; i < csr_jac_->getNnz(); ++i) + { + vals[i] = 0.0; + } + + // Update CSR values from component Jacobians + IdxT counter = 0; + for (const auto& component : components_) + { + component->evaluateJacobian(); + + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const RealT* v = component->jacobianCooValues(); + IdxT nnz = component->nnz(); + + for (IdxT i = 0; i < nnz; ++i) + { + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + const bool is_internal_entry = row != neg1_ && col != neg1_ && row < n_intern_ && col < n_intern_; + + if (!is_internal_entry) + { + continue; + } + + vals[map_to_csr_[counter]] += v[i]; + ++counter; + } + } + + jac_call_count_++; + return 0; + } + + /** + * @brief Evaluate integrands for the system quadratures. + */ + int evaluateIntegrand() final + { + return 0; + } + + /** + * @brief Initialize system adjoint. + * + * Updates variables and optimization parameters, then initializes + * adjoints locally and copies them to the system adjoint vector. + */ + int initializeAdjoint() final + { + return 0; + } + + /** + * @brief Compute adjoint residual for the system model. + * + * + */ + int evaluateAdjointResidual() final + { + return 0; + } + + /** + * @brief Evaluate adjoint integrand for the system model. + * + * + */ + int evaluateAdjointIntegrand() final + { + return 0; + } + + /** + * @brief Distribute time and time scaling for each component + * + * @param t + * @param a + */ + void updateTime(RealT t, RealT a) final + { + for (const auto& component : components_) + { + component->updateTime(t, a); + } + time_ = t; + alpha_ = a; + } + + CsrMatrixT* getCsrJacobian() const override + { + return csr_jac_; + } + + void addComponent(component_type* component) + { + components_.push_back(component); + allocated_ = false; + } + + void addNode(node_type* node) + { + nodes_.push_back(node); + allocated_ = false; + } + + protected: + static constexpr IdxT neg1_ = INVALID_INDEX; + + std::vector components_; + std::vector nodes_; + + IdxT* map_to_csr_{nullptr}; + CsrMatrixT* csr_jac_{nullptr}; + + int jac_call_count_{0}; + bool use_jac_; + + }; // class PowerElectronicsModel + +} // namespace GridKit \ No newline at end of file From c31504023c8749140a305b1fdff0e586832b08a6 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Wed, 26 Aug 2026 07:46:16 +0000 Subject: [PATCH 03/31] Add HIRES test for SubsystemModel --- .../UnitTests/PowerElectronics/CMakeLists.txt | 12 +- tests/UnitTests/PowerElectronics/README.md | 139 ++++ .../SubsystemModelWithHiresTest.hpp | 775 ++++++++++++++++++ .../runSubsystemModelWithHiresTest.cpp | 12 + 4 files changed, 936 insertions(+), 2 deletions(-) create mode 100644 tests/UnitTests/PowerElectronics/README.md create mode 100644 tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp create mode 100644 tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index dc3c167371..0af2401d1e 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -3,6 +3,13 @@ target_link_libraries( test_power_electronics_node PRIVATE GridKit::power_electronics_circuit_node GridKit::testing) +add_executable(test_subsystem_model_with_hires runSubsystemModelWithHiresTest.cpp) +target_link_libraries( + test_subsystem_model_with_hires + PRIVATE GridKit::power_elec_partition_interfaces + GridKit::testing + GridKit::sparse_matrix) + add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( test_power_electronics_component_clone @@ -13,8 +20,9 @@ target_link_libraries( GridKit::testing) add_test(NAME PowerElectronicsNodeTest COMMAND $) -add_test(NAME PowerElectronicsComponentCloneTest - COMMAND $) +add_test(NAME SubsystemModelWithHires COMMAND $) +add_test(NAME PowerElectronicsComponentCloneTest COMMAND $) install(TARGETS test_power_electronics_node RUNTIME DESTINATION bin) +install(TARGETS test_subsystem_model_with_hires RUNTIME DESTINATION bin) install(TARGETS test_power_electronics_component_clone RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/PowerElectronics/README.md b/tests/UnitTests/PowerElectronics/README.md new file mode 100644 index 0000000000..be5c30248b --- /dev/null +++ b/tests/UnitTests/PowerElectronics/README.md @@ -0,0 +1,139 @@ +## HIRES Partitioning Test Problem + +The HIRES test problem is a simple ODE system with eight variables. To +demonstrate the partitioning machinery in GridKit, the system is divided into +three components. Equations 1--3 belong to **Component 1**, equations 4--5 +belong to **Component 2**, which is modeled as a bus, and equations 6--8 belong +to **Component 3**. + +> **Note:** HIRES is not a circuit problem. In this example, it is modeled to +> resemble GridKit circuit components so that the existing partitioning +> machinery can be used. The example also provides a simple test problem for +> verifying the order of co-simulation methods. + +The full HIRES system is + +$$ +\begin{aligned} +f_1 &= \frac{dy_1}{dt} +1.71y_1 -0.43y_2 -8.32y_3 -0.0007, \\ +f_2 &= \frac{dy_2}{dt} -1.71y_1 +8.75y_2, \\ +f_3 &= \frac{dy_3}{dt} +10.03y_3 -0.43y_4 -0.035y_5, \\ +f_4 &= \frac{dy_4}{dt} -8.32y_2 -1.71y_3 +1.12y_4, \\ +f_5 &= \frac{dy_5}{dt} +1.745y_5 -0.43y_6 -0.43y_7, \\ +f_6 &= \frac{dy_6}{dt} +280y_6y_8 -0.69y_4 -1.71y_5 + +0.43y_6 -0.69y_7, \\ +f_7 &= \frac{dy_7}{dt} -280y_6y_8 +1.81y_7, \\ +f_8 &= \frac{dy_8}{dt} +280y_6y_8 -1.81y_7. +\end{aligned} +$$ + +For the component representation, equations $f_4$ and $f_5$ are decomposed +to expose the contributions from each component: + +$$ +\begin{aligned} +f_4 +&= \frac{dy_4}{dt} -8.32y_2 -1.71y_3 +1.12y_4 \qquad +&\longrightarrow +\left(\frac{dy_4}{dt}+y_4\right) ++\left(0.1y_4-8.32y_2-1.71y_3\right) ++0.02y_4, +\\[6pt] +f_5 +&= \frac{dy_5}{dt}+1.745y_5-0.43y_6-0.43y_7 \qquad +&\longrightarrow +\left(\frac{dy_5}{dt}+y_5\right) ++0.7y_5 ++\left[0.045y_5-0.43y_6-0.43y_7\right]. +\end{aligned} +$$ + +The decomposition does not change the HIRES system. It separates the terms in +the bus equations (conviniently choosen to be equation 4 and 5) according to the component responsible for each contribution. + +### Component 1 + +Component 1 has three internal equations: + +$$ +\begin{aligned} +f_1 &= \frac{dy_1}{dt} +1.71y_1 -0.43y_2 -8.32y_3 -0.0007, \\ +f_2 &= \frac{dy_2}{dt} -1.71y_1 +8.75y_2, \\ +f_3 &= \frac{dy_3}{dt} +10.03y_3 -0.43y_4 -0.035y_5. +\end{aligned} +$$ + +It also contributes the following terms to the bus equations as its external contribution: + +$$ +\begin{aligned} +f_4^{(1)} &= 0.1y_4 -8.32y_2 -1.71y_3, \\ +f_5^{(1)} &= 0.7y_5. +\end{aligned} +$$ + +### Component 2 (HiresBus) + +Component 2 represents the bus and owns the following contributions to +equations 4 and 5: + +$$ +\begin{aligned} +f_4^{(2)} &= \frac{dy_4}{dt} + y_4, \\ +f_5^{(2)} &= \frac{dy_5}{dt} + y_5. +\end{aligned} +$$ + +The remaining terms in these equations are supplied by the components +connected to the bus. + +### Component 3 + +Component 3 has three internal equations: + +$$ +\begin{aligned} +f_6 &= \frac{dy_6}{dt} -280y_6y_8 +0.69y_4 +1.71y_5 + -0.43y_6 +0.69y_7, \\ +f_7 &= \frac{dy_7}{dt} +280y_6y_8 -1.81y_7, \\ +f_8 &= \frac{dy_8}{dt} -280y_6y_8 +1.81y_7. +\end{aligned} +$$ + +It also contributes the following terms to the bus equations as its external contribution: + +$$ +\begin{aligned} +f_4^{(3)} &= 0.02y_4, \\ +f_5^{(3)} &= 0.045y_5 -0.43y_6 -0.43y_7. +\end{aligned} +$$ + +### Bus Residual Assembly + +The complete bus residuals are obtained by adding the contributions from +Components 1, 2, and 3: + +$$ +f_4 = f_4^{(1)} + f_4^{(2)} + f_4^{(3)}, +$$ + +$$ +f_5 = f_5^{(1)} + f_5^{(2)} + f_5^{(3)}. +$$ + +This reconstruction gives exactly the corresponding equations in the original +HIRES system. + +### Partitioning + +The full HIRES system in component form looks like this: + +```text +Component 1 -------- Component 2 (HiresBus) -------- Component 3 +``` + +The system is divided between **Component 2 (HiresBus)** and **Component 3**, and +a `BusPartitionInterface` is added to the first partition. The resulting +partition residuals are then evaluated independently and are then compared with the full-system residual to verify +that the partitioned evaluation reproduces the original system. diff --git a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp new file mode 100644 index 0000000000..b0c98b8048 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp @@ -0,0 +1,775 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + + /*! + * @brief Hires Component 1 class. + * + */ + template + class HiresComponent1 : public CircuitComponent + { + using RealT = typename CircuitComponent::RealT; + using NodeT = typename PowerElectronics::NodeBase; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + + public: + HiresComponent1(NodeT* bus, IdxT id) + : node_ref_(bus) + { + size_ = 5; + n_intern_ = 3; + n_extern_ = 2; + extern_indices_ = {0, 1}; + idc_ = id; + nnz_ = 12; + } + + ~HiresComponent1() + { + } + + int allocate() final + { + CircuitComponent::allocate(); + + this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); + this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); + + return 0; + } + + int initialize() final + { + return 0; + } + + int tagDifferentiable() final + { + return 0; + } + + int evaluateInternalResidual() final + { + // Internals + f_int_[0] = -yp_int_[0] - 1.71 * y_int_[0] + 0.43 * y_int_[1] + 8.32 * y_int_[2] + 0.0007; + f_int_[1] = -yp_int_[1] + 1.71 * y_int_[0] - 8.75 * y_int_[1]; + f_int_[2] = -yp_int_[2] - 10.03 * y_int_[2] + 0.43 * *y_ext_[0] + 0.035 * *y_ext_[1]; + + return 0; + } + + int evaluateExternalResidual() + { + // outputs + *f_ext_[0] += 8.32 * y_int_[1] + 1.71 * y_int_[2] - 0.1 * *y_ext_[0]; + *f_ext_[1] += -0.7 * *y_ext_[1]; + + return 0; + } + + int evaluateJacobian() final + { + + this->zeroJacMatrix(); + + // Internal Jacobian Entries + std::vector row = {2, 2, 2, 3, 3, 4, 4, 4}; + std::vector col = {2, 3, 4, 2, 3, 4, 0, 1}; + std::vector val = {-1.71 - alpha_, 0.43, 8.32, 1.71, -8.75 - alpha_, -10.03 - alpha_, 0.43, 0.035}; + + this->setJacValues(row, col, val); + + // External Jacobian Entries + row = {0, 0, 0, 1}; + col = {3, 4, 0, 1}; + val = {8.32, 1.71, -0.1, -0.7}; + + this->setJacValues(row, col, val); + + return 0; + } + + int evaluateIntegrand() final + { + return 0; + } + + int initializeAdjoint() final + { + return 0; + } + + int evaluateAdjointResidual() final + { + return 0; + } + + int evaluateAdjointIntegrand() final + { + return 0; + } + + /** + * @brief Compute the absolute tolerance for each variable in the model + */ + int setAbsoluteTolerance(RealT rel_tol) final + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + CircuitComponent* clone() const + { + return new HiresComponent1(*this); + } + + private: + NodeT* node_ref_; + }; + + /*! + * @brief Hires Bus Component (Component 2). + * + */ + template + class HiresBus : public CircuitComponent + { + + using RealT = typename CircuitComponent::RealT; + using NodeT = typename PowerElectronics::NodeBase; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + + public: + HiresBus(NodeT* bus, IdxT id) + : node_ref_(bus) + { + size_ = 2; + n_intern_ = 0; + n_extern_ = 2; + extern_indices_ = {0, 1}; + idc_ = id; + nnz_ = 2; + } + + ~HiresBus() + { + } + + int allocate() final + { + CircuitComponent::allocate(); + + this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); + this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); + + return 0; + } + + int initialize() final + { + return 0; + } + + int tagDifferentiable() final + { + return 0; + } + + int evaluateInternalResidual() final + { + return 0; + } + + int evaluateExternalResidual() final + { + *f_ext_[0] += -*yp_ext_[0] - *y_ext_[0]; + *f_ext_[1] += -*yp_ext_[1] - *y_ext_[1]; + + return 0; + } + + int evaluateJacobian() final + { + this->zeroJacMatrix(); + + std::vector row = {0, 1}; + std::vector col = {0, 1}; + std::vector val = {-alpha_ - 1.0, -alpha_ - 1.0}; + + this->setJacValues(row, col, val); + + return 0; + } + + int evaluateIntegrand() final + { + return 0; + } + + int initializeAdjoint() final + { + return 0; + } + + int evaluateAdjointResidual() final + { + return 0; + } + + int evaluateAdjointIntegrand() final + { + return 0; + } + + /** + * @brief Compute the absolute tolerance for each variable in the model + */ + int setAbsoluteTolerance(RealT rel_tol) final + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + CircuitComponent* clone() const + { + return new HiresBus(*this); + } + + private: + NodeT* node_ref_; + }; + + /*! + * @brief Hires Component 3 class. + * + */ + template + class HiresComponent3 : public CircuitComponent + { + using RealT = typename CircuitComponent::RealT; + using NodeT = typename PowerElectronics::NodeBase; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + + public: + HiresComponent3(NodeT* bus, IdxT id) + : node_ref_(bus) + { + size_ = 5; + n_intern_ = 3; + n_extern_ = 2; + extern_indices_ = {0, 1}; + idc_ = id; + nnz_ = 15; + } + + ~HiresComponent3() + { + } + + int allocate() + { + CircuitComponent::allocate(); + + this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); + this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); + + return 0; + } + + int initialize() + { + return 0; + } + + int tagDifferentiable() + { + return 0; + } + + int evaluateInternalResidual() + { + + // Internals + f_int_[0] = -yp_int_[0] - 280 * y_int_[0] * y_int_[2] + 0.69 * *y_ext_[0] + 1.71 * *y_ext_[1] - 0.43 * y_int_[0] + 0.69 * y_int_[1]; + f_int_[1] = -yp_int_[1] + 280 * y_int_[0] * y_int_[2] - 1.81 * y_int_[1]; + f_int_[2] = -yp_int_[2] - 280 * y_int_[0] * y_int_[2] + 1.81 * y_int_[1]; + + return 0; + } + + int evaluateExternalResidual() + { + // Externals + *f_ext_[0] += -0.02 * *y_ext_[0]; + *f_ext_[1] += -0.045 * *y_ext_[1] + 0.43 * y_int_[0] + 0.43 * y_int_[1]; + + return 0; + } + + int evaluateJacobian() + { + this->zeroJacMatrix(); + + // Internal Jacobian Entries [row 1] + std::vector row = {2, 2, 2, 2, 2}; + std::vector col = {2, 3, 4, 0, 1}; + std::vector val = {-280 * y_int_[2] - 0.43 - alpha_, 0.69, -280 * y_int_[0], 0.69, 1.71}; + + this->setJacValues(row, col, val); + + // Internal Jacobian Entries [row 2] + row = {3, 3, 3}; + col = {2, 3, 4}; + val = {280 * y_int_[2], -1.81 - alpha_, 280 * y_int_[0]}; + + this->setJacValues(row, col, val); + + // Internal Jacobian Entries [row 3] + row = {4, 4, 4}; + col = {2, 3, 4}; + val = {-280 * y_int_[2], 1.81, -280 * y_int_[0] - alpha_}; + + this->setJacValues(row, col, val); + + // External Jacobian Entries + row = {0, 1, 1, 1}; + col = {0, 2, 3, 1}; + val = {-0.02, 0.43, 0.43, -0.045}; + + this->setJacValues(row, col, val); + + return 0; + } + + int evaluateIntegrand() + { + return 0; + } + + int initializeAdjoint() + { + return 0; + } + + int evaluateAdjointResidual() + { + return 0; + } + + int evaluateAdjointIntegrand() + { + return 0; + } + + /** + * @brief Compute the absolute tolerance for each variable in the model + * + */ + int setAbsoluteTolerance(RealT rel_tol) + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + CircuitComponent* clone() const + { + return new HiresComponent3(*this); + } + + private: + NodeT* node_ref_; + }; + + namespace Testing + { + + /** + * This example assembles the HIRES problem and partitions it to demonstrate + * that the partitioned system can reconstruct the full system. + * + * The HIRES problem consists of eight ODE equations and is divided into three + * components. HiresComponent1 contains equations 1, 2, and 3, + * HiresComponent3 contains equations 6, 7, and 8, and HiresBus contains + * equations 4 and 5. HiresBus plays a role similar to MicrogridBusDQ, where + * other components connected to the bus contribute terms to its equations. + * + * In the equations below, terms enclosed in parentheses ( ) are contributions + * from HiresComponent1, terms enclosed in square brackets [ ] are contributions + * from HiresComponent3, and the remaining terms in equations 4 and 5 belong + * to HiresBus. + *\f[ + * f_1 = dy_1/dt + 1.71y_1 - 0.43y_2 - 8.32y_3 - 0.0007 + * + * f_2 = dy_2/dt - 1.71y_1 + 8.75y_2 + * + * f_3 = dy_3/dt + 10.03y_3 - 0.43y_4 - 0.035y_5 + * + * f_4 = dy_4/dt + y_4 + (0.1y_4 - 8.32y_2 - 1.71y_3) + [0.02y_4] + * + * f_5 = dy_5/dt + y_5 + \left(0.7y_5 \right) + \left[0.045y_5 - 0.43y_6 - 0.43y_7 \right] + * + * f_6 = dy_6/dt - 280y_6y_8 + 0.69y_4 + 1.71y_5 - 0.43y_6 + 0.69y_7 + * + * f_7 = dy_7/dt + 280y_6y_8 - 1.81y_7 + * + * f_8 = dy_8/dt - 280y_6y_8 + 1.81y_7 + *\f] + * + * The assembled system has the following structure: + * + * (HiresComponent1) -------- (HiresBus) -------- (HiresComponent3) + * + * The system is partitioned between HiresBus and HiresComponent3. A bus + * partition interface is introduced in the partition containing HiresBus to + * preserve the contribution of HiresComponent3 to the bus equations. The + * residuals evaluated independently by the partitions are then printed with + * the residual of the full system for eye-ball comparision. + * + * This example will also be useful later for testing the order of accuracy of co-simulation methods. + */ + template + class SubsystemModelWithHiresTests + { + using RealT = typename CircuitComponent::RealT; + using Bus = PowerElectronics::MicrogridBus; + using Subsystem = SubsystemModel; + using System = PowerElectronicsModel; + + public: + /** + * @brief Construct the HIRES reference system and subsystem partitions. + */ + SubsystemModelWithHiresTests() + : system_(new System()), + partition1_(new Subsystem()), + partition2_(new Subsystem()), + comp1_(new HiresComponent1(&bus_, 1)), + bus1_(new HiresBus(&bus_, 2)), + comp3_(new HiresComponent3(&bus_, 3)) + { + + y_ = {1, 2, 3, 4, 5, 6, 7, 8}; + yp_ = {1, 2, 3, 4, 5, 6, 7, 8}; + // --------------------------------------------------------------------- + // Assemble and allocate the monolithic reference system + // --------------------------------------------------------------------- + + system_->addComponent(comp1_); + system_->addComponent(comp3_); + system_->addComponent(bus1_); + system_->addNode(&bus_); + + system_->allocate(); + + auto* system_y = system_->y().getData(); + auto* system_yp = system_->yp().getData(); + + for (size_t i = 0; i < system_->size(); ++i) + { + system_y[i] = y_[i]; + system_yp[i] = yp_[i]; + } + + system_->y().setDataUpdated(); + system_->yp().setDataUpdated(); + + system_->updateTime(1.0, 2.0); + system_->evaluateResidual(); + + // --------------------------------------------------------------------- + // Construct the partition interface + // --------------------------------------------------------------------- + + auto* comp3_copy = new HiresComponent3(*comp3_); + bus_interface_ = new BusPartitionInterface(&bus_, comp3_copy, 4); + + bus_interface_->allocate(); + + // --------------------------------------------------------------------- + // Assemble the subsystem partitions + // --------------------------------------------------------------------- + + partition1_->addComponent(comp1_); + partition1_->addComponent(bus1_); + partition1_->addInterface(bus_interface_); + partition1_->addNode(&bus_); + + partition2_->addComponent(comp3_); + + partition1_->allocate(); + partition2_->allocate(); + partitions_ = {partition1_, partition2_}; + + // Distribute variables to all partitions + distributeVariables(y_, yp_); + } + + ~SubsystemModelWithHiresTests() + { + delete partition1_; + delete partition2_; + delete system_; + } + + void distributeVariables(const std::vector& y, const std::vector& yp) + { + for (auto* partition : partitions_) + { + + // Test the forcing function mechanism. + auto forcing_function = [partition, &y, &yp]([[maybe_unused]] ScalarT t) + { + typename Subsystem::ForcingData forcing_data; + + forcing_data.y.resize(partition->getExternSize()); + forcing_data.yp.resize(partition->getExternSize()); + + for (size_t i = 0; i < partition->getExternSize(); ++i) + { + const auto global_index = partition->getExternalDataIndices()[i]; + + forcing_data.y[i] = y[global_index]; + forcing_data.yp[i] = yp[global_index]; + } + + return forcing_data; + }; + + partition->setForcingFunction(std::move(forcing_function)); + + auto* partition_y = partition->y().getData(); + auto* partition_yp = partition->yp().getData(); + + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + const auto global_index = partition->getNodeConnection(i); + + partition_y[i] = y[global_index]; + partition_yp[i] = yp[global_index]; + } + + partition->y().setDataUpdated(); + partition->yp().setDataUpdated(); + } + } + + /** + * @brief Verify that the partitioned HIRES residual matches the + * monolithic residual. + */ + TestOutcome residual() + { + TestStatus success = true; + + std::vector partition_residual(system_->size(), 0.0); + + for (auto* partition : partitions_) + { + partition->updateTime(1.0, 2.0); + partition->evaluateResidual(); + + auto* residual = partition->getResidual().getData(); + + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + partition_residual[partition->getNodeConnection(i)] = residual[i]; + } + + partition->getResidual().setDataUpdated(); + } + + auto* reference_residual = system_->getResidual().getData(); + + RealT max_error = 0.0; + + for (size_t i = 0; i < system_->size(); ++i) + { + double error = std::abs(partition_residual[i] - reference_residual[i]) / std::abs(reference_residual[i] + 1); + max_error = std::max(max_error, error); + } + + std::cout << "max error " << max_error << std::endl; + + success *= max_error <= std::numeric_limits::epsilon(); + + return success.report(__func__); + } + + /** + * @brief Verify that each subsystem Jacobian matches the corresponding + * entries of the monolithic HIRES Jacobian. + */ + TestOutcome jacobian() + { + TestStatus success = true; + + RealT alpha = 2.0; + + /* + * GridKit stores the HIRES variables in component assembly order: + * + * System index: 0 1 2 3 4 5 6 7 + * HIRES index: 0 1 2 5 6 7 3 4 + * Variable: y1 y2 y3 y6 y7 y8 y4 y5 + */ + const std::array sysmodel_to_hires = { + 0, 1, 2, 5, 6, 7, 3, 4}; + + const std::array hires_to_sysmodel = { + 0, 1, 2, 6, 7, 3, 4, 5}; + + const RealT y5 = y_[hires_to_sysmodel[5]]; + const RealT y7 = y_[hires_to_sysmodel[7]]; + + std::array, 8> reference_jac = + {{{-alpha - 1.71, 0.43, 8.32, 0.0, 0.0, 0.0, 0.0, 0.0}, + {1.71, -alpha - 8.75, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {0.0, 0.0, -alpha - 10.03, 0.43, 0.035, 0.0, 0.0, 0.0}, + {0.0, 8.32, 1.71, -alpha - 1.12, 0.0, 0.0, 0.0, 0.0}, + {0.0, 0.0, 0.0, 0.0, -alpha - 1.745, 0.43, 0.43, 0.0}, + {0.0, 0.0, 0.0, 0.69, 1.71, -alpha - 280.0 * y7 - 0.43, 0.69, -280.0 * y5}, + {0.0, 0.0, 0.0, 0.0, 0.0, 280.0 * y7, -alpha - 1.81, 280.0 * y5}, + {0.0, 0.0, 0.0, 0.0, 0.0, -280.0 * y7, 1.81, -alpha - 280.0 * y5}}}; + + for (auto* partition : partitions_) + { + partition->updateTime(1.0, alpha); + partition->evaluateJacobian(); + + auto* partition_jac = partition->getCsrJacobian(); + + const auto* row_ptr = partition_jac->getRowData(); + const auto* cols = partition_jac->getColData(); + const auto* vals = partition_jac->getValues(); + + const size_t n = partition->getInternalSize(); + + std::vector> dense_jac(n, std::vector(n, 0.0)); + + // Convert the partition CSR Jacobian to a dense matrix. + for (size_t row = 0; row < n; ++row) + { + for (IdxT k = row_ptr[row]; k < row_ptr[row + 1]; ++k) + { + dense_jac[row][cols[k]] = vals[k]; + } + } + + // Compare with the corresponding entries of the full Jacobian. + for (size_t row = 0; row < n; ++row) + { + const IdxT global_row = partition->getNodeConnection(row); + const IdxT ref_row = sysmodel_to_hires[global_row]; + + for (size_t col = 0; col < n; ++col) + { + const IdxT global_col = partition->getNodeConnection(col); + const IdxT ref_col = sysmodel_to_hires[global_col]; + + success *= std::abs(dense_jac[row][col] - reference_jac[ref_row][ref_col]) <= std::numeric_limits::epsilon(); + } + } + } + + return success.report(__func__); + } + + private: + /// @brief Bus used to define the connection between the HIRES components. + Bus bus_; + + /// @brief Complete monolithic HIRES system. + System* system_; + + /// @brief First subsystem of the partitioned HIRES system. + Subsystem* partition1_; + + /// @brief Second subsystem of the partitioned HIRES system. + Subsystem* partition2_; + + /// @brief First component of the HIRES system. + HiresComponent1* comp1_; + + /// @brief Bus component connecting the two parts of the HIRES system. + HiresBus* bus1_; + + /// @brief Third component of the HIRES system. + HiresComponent3* comp3_; + + /// @brief Partition interface representing the bus connection across the subsystem boundary. + BusPartitionInterface* bus_interface_; + + /// @brief Collection of subsystems comprising the partitioned HIRES system. + std::vector partitions_; + + /// @brief State vector used to initialize and evaluate the HIRES system. + std::vector y_; + + /// @brief State derivative vector used to initialize and evaluate the HIRES system. + std::vector yp_; + }; + + } // namespace Testing + +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp b/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp new file mode 100644 index 0000000000..2b8ef11b08 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp @@ -0,0 +1,12 @@ +#include "SubsystemModelWithHiresTest.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::Testing::SubsystemModelWithHiresTests test; + + result += test.residual(); + result += test.jacobian(); + + return result.summary(); +} From ac8769ea7a0158cf348477f087deda64b33d220f Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Wed, 26 Aug 2026 08:00:00 +0000 Subject: [PATCH 04/31] Add protected access to SystemModelPowerElectronics --- .../Model/PowerElectronics/SystemModelPowerElectronics.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp index 20111ace90..1c07ebf5b0 100644 --- a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp +++ b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp @@ -22,6 +22,7 @@ namespace GridKit using component_type = CircuitComponent; using node_type = PowerElectronics::NodeBase; + protected: using CircuitComponent::abs_tol_; using CircuitComponent::allocated_; using CircuitComponent::allocateVectors; @@ -270,7 +271,7 @@ namespace GridKit * * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable */ - int initialize() + int initialize() override { // Initialize components for (const auto& component : components_) @@ -293,7 +294,7 @@ namespace GridKit * stored contiguously in the same order as \ref components_, with node variables at the end. * Each internal variable's tag in the system is set to its tag in the component. */ - int tagDifferentiable() + int tagDifferentiable() override { // Ask all component to tag their differentiables for (size_t i = 0; i < components_.size(); i++) From 6c76011b985179b67b0eb1d17fcb2f4e4dbe0fae Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 28 Aug 2026 01:52:32 +0000 Subject: [PATCH 05/31] Change external data vector to use GridKit vectors --- .../Model/PowerElectronics/SubsystemModel.hpp | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 9ee1eaee18..479551c404 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -1,3 +1,8 @@ +/** + * @file BusPartitionInterface.hpp + * @author Abdourahman Barry (abdourahman@vt.edu) + * + */ #pragma once #include @@ -38,6 +43,8 @@ namespace GridKit * evaluation, either directly through the external-data vectors or through a * forcing function. * + * @todo Find a better name for this class and its base class. + * * @tparam ScalarT Scalar type used by the model. * @tparam IdxT Index type used for variable and connection indices. */ @@ -55,6 +62,7 @@ namespace GridKit protected: using SystemModel = PowerElectronicsModel; + using VectorT = typename SystemModel::VectorT; using RealT = typename CircuitComponent::RealT; using CsrMatrixT = typename CircuitComponent::CsrMatrixT; using component_type = CircuitComponent; @@ -204,6 +212,10 @@ namespace GridKit } { + ScalarT* y_ext_data_ptr = y_ext_data_.getData(); + ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); + ScalarT* f_ext_data_ptr = f_ext_data_.getData(); + // The offset for each component's internal variables in the system vector. // They start at 0, and are stacked on top of each other. size_t component_internal_idx = 0; @@ -243,9 +255,9 @@ namespace GridKit const IdxT external_offset = connection_index - static_cast(n_intern_); ExternalConnection connection{ - .y_ = &y_ext_data_[external_offset], - .yp_ = &yp_ext_data_[external_offset], - .f_ = &f_ext_data_[external_offset], + .y_ = y_ext_data_ptr + external_offset, + .yp_ = yp_ext_data_ptr + external_offset, + .f_ = f_ext_data_ptr + external_offset, .idx_ = connection_index}; comp->setExternalConnectionNodes(local_index, connection); @@ -388,15 +400,15 @@ namespace GridKit { const auto forcing = (*forcing_function_)(time_); - if (forcing.y.size() != y_ext_data_.size() || forcing.yp.size() != yp_ext_data_.size()) + if (forcing.y.size() != y_ext_data_.getSize() || forcing.yp.size() != yp_ext_data_.getSize()) { throw std::runtime_error( "SubsystemModel::distributeExternalVectors: forcing function " "returned vectors with incorrect sizes."); } - std::copy(forcing.y.begin(), forcing.y.end(), y_ext_data_.begin()); - std::copy(forcing.yp.begin(), forcing.yp.end(), yp_ext_data_.begin()); + std::copy(forcing.y.begin(), forcing.y.end(), y_ext_data_.getData()); + std::copy(forcing.yp.begin(), forcing.yp.end(), yp_ext_data_.getData()); } return 0; @@ -538,17 +550,17 @@ namespace GridKit return external_data_indices_; } - std::vector& getExternalDataY() + VectorT& getExternalDataY() { return y_ext_data_; } - std::vector& getExternalDataYP() + VectorT& getExternalDataYP() { return yp_ext_data_; } - std::vector& getExternalDataF() + VectorT& getExternalDataF() { return f_ext_data_; } @@ -805,19 +817,19 @@ namespace GridKit std::vector external_data_indices_; /** - * @brief subsystem external State, derivative, and residual vectors. + * @brief subsystem external state data. */ - std::vector y_ext_data_; + VectorT y_ext_data_; /** - * @brief subsystem external derivative + * @brief subsystem external state derivative */ - std::vector yp_ext_data_; + VectorT yp_ext_data_; /** - * @brief subsystem external derivative + * @brief subsystem external residual data */ - std::vector f_ext_data_; + VectorT f_ext_data_; /** * @brief Optional forcing function used to provide external subsystem data. From a67eef2f64495cc882cd2020d33d4b39eb4b4c5e Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 28 Aug 2026 01:53:58 +0000 Subject: [PATCH 06/31] Updated SystemModelPowerElectronics.hpp --- .../PowerElectronics/SystemModelPowerElectronics.hpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp index 1c07ebf5b0..9764010d6a 100644 --- a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp +++ b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp @@ -14,6 +14,10 @@ namespace GridKit { + /** + * @todo Move all PowerElectronics models into the PowerElectronics namespace + * for consistency. + */ template class PowerElectronicsModel : public CircuitComponent { @@ -425,13 +429,11 @@ namespace GridKit const bool is_internal_entry = row != neg1_ && col != neg1_ && row < n_intern_ && col < n_intern_; - if (!is_internal_entry) + if (is_internal_entry) { - continue; + vals[map_to_csr_[counter]] += v[i]; + ++counter; } - - vals[map_to_csr_[counter]] += v[i]; - ++counter; } } From 3d36c47b46d55bc679d41e770a1a5f270228f30f Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 28 Aug 2026 01:58:41 +0000 Subject: [PATCH 07/31] Update README.md for Hires problem --- tests/UnitTests/PowerElectronics/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/UnitTests/PowerElectronics/README.md b/tests/UnitTests/PowerElectronics/README.md index be5c30248b..e5b4368b3b 100644 --- a/tests/UnitTests/PowerElectronics/README.md +++ b/tests/UnitTests/PowerElectronics/README.md @@ -37,13 +37,13 @@ f_4 &\longrightarrow \left(\frac{dy_4}{dt}+y_4\right) +\left(0.1y_4-8.32y_2-1.71y_3\right) -+0.02y_4, -\\[6pt] ++\left[0.02y_4\right], +\\ \\ f_5 &= \frac{dy_5}{dt}+1.745y_5-0.43y_6-0.43y_7 \qquad &\longrightarrow \left(\frac{dy_5}{dt}+y_5\right) -+0.7y_5 ++\left(0.7y_5\right) +\left[0.045y_5-0.43y_6-0.43y_7\right]. \end{aligned} $$ From 35af9628d5c40a27b73ecf9bb18e1da4d6e514c4 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 28 Aug 2026 02:01:22 +0000 Subject: [PATCH 08/31] Minor update --- .../PartitionInterface/BusPartitionInterface.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp index aa832095b7..1f1109720d 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp @@ -1,4 +1,8 @@ - +/** + * @file BusPartitionInterface.hpp + * @author Abdourahman Barry (abdourahman@vt.edu) + * + */ #pragma once From 239fbddca4e1afc6fe2576058307f28048f83eb5 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Thu, 3 Sep 2026 18:57:17 +0000 Subject: [PATCH 09/31] Apply pre-commit fixes --- .../PowerElectronics/PartitionInterface/CMakeLists.txt | 10 +++++----- GridKit/Model/PowerElectronics/SubsystemModel.hpp | 2 +- .../PowerElectronics/SystemModelPowerElectronics.hpp | 2 +- tests/UnitTests/PowerElectronics/CMakeLists.txt | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt index 8d833c6dd6..9f013ad673 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -2,8 +2,8 @@ gridkit_add_library( power_elec_partition_interfaces SOURCES BusPartitionInterface.cpp HEADERS BusPartitionInterface.hpp PartitionInterface.hpp - LINK_LIBRARIES - PUBLIC - GridKit::dense_vector - PUBLIC - GridKit::utilities_logger) + LINK_LIBRARIES + PUBLIC + GridKit::dense_vector + PUBLIC + GridKit::utilities_logger) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 479551c404..9ff9e41fd3 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -853,4 +853,4 @@ namespace GridKit }; // class SubsystemModel -} // namespace GridKit \ No newline at end of file +} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp index 9764010d6a..9796ee9f16 100644 --- a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp +++ b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp @@ -527,4 +527,4 @@ namespace GridKit }; // class PowerElectronicsModel -} // namespace GridKit \ No newline at end of file +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index 0af2401d1e..9dd7db2ea0 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -6,9 +6,7 @@ target_link_libraries( add_executable(test_subsystem_model_with_hires runSubsystemModelWithHiresTest.cpp) target_link_libraries( test_subsystem_model_with_hires - PRIVATE GridKit::power_elec_partition_interfaces - GridKit::testing - GridKit::sparse_matrix) + PRIVATE GridKit::power_elec_partition_interfaces GridKit::testing GridKit::sparse_matrix) add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( @@ -21,7 +19,8 @@ target_link_libraries( add_test(NAME PowerElectronicsNodeTest COMMAND $) add_test(NAME SubsystemModelWithHires COMMAND $) -add_test(NAME PowerElectronicsComponentCloneTest COMMAND $) +add_test(NAME PowerElectronicsComponentCloneTest + COMMAND $) install(TARGETS test_power_electronics_node RUNTIME DESTINATION bin) install(TARGETS test_subsystem_model_with_hires RUNTIME DESTINATION bin) From 4b4c8b6c5d950f11af012e83d3fa8b2e7d46993d Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Tue, 8 Sep 2026 19:24:08 +0000 Subject: [PATCH 10/31] Address leak in test --- .../UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp index b0c98b8048..71a1aaaefd 100644 --- a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp +++ b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp @@ -541,8 +541,7 @@ namespace GridKit // Construct the partition interface // --------------------------------------------------------------------- - auto* comp3_copy = new HiresComponent3(*comp3_); - bus_interface_ = new BusPartitionInterface(&bus_, comp3_copy, 4); + bus_interface_ = new BusPartitionInterface(&bus_, comp3_, 4); bus_interface_->allocate(); From 50c8f2633b64977e644cfe1c8707dfc2c9e7ee16 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sat, 12 Sep 2026 21:57:21 +0000 Subject: [PATCH 11/31] Change neg1 to INVALID_INDEX --- GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp index 9796ee9f16..61c3a1933b 100644 --- a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp +++ b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp @@ -427,7 +427,8 @@ namespace GridKit const IdxT row = component->getNodeConnection(r[i]); const IdxT col = component->getNodeConnection(c[i]); - const bool is_internal_entry = row != neg1_ && col != neg1_ && row < n_intern_ && col < n_intern_; + const bool is_internal_entry = row != INVALID_INDEX && col != INVALID_INDEX + && row < this->getInternalSize() && col < this->getInternalSize(); if (is_internal_entry) { From ac21a74b9635d0102ff94988d9a63e8909ce2c77 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sat, 19 Sep 2026 00:09:11 +0000 Subject: [PATCH 12/31] Simplified SubsystemModel code --- .../Model/PowerElectronics/SubsystemModel.hpp | 380 +++++++----------- 1 file changed, 137 insertions(+), 243 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 9ff9e41fd3..a28439d688 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -5,7 +5,9 @@ */ #pragma once +#include #include +#include #include #include #include @@ -88,7 +90,6 @@ namespace GridKit using SystemModel::csr_jac_; using SystemModel::jac_call_count_; using SystemModel::map_to_csr_; - using SystemModel::neg1_; using SystemModel::nodes_; using SystemModel::use_jac_; @@ -150,27 +151,23 @@ namespace GridKit */ int allocate() override { - if (int err_code = buildIndexMappings()) + if (this->isAllocated()) { - return err_code; + restoreGlobalConnection(); } - if (int err_code = mapGlobalToLocal()) + if (int err_code = buildConnectionMap()) { return err_code; } - n_intern_ = internal_map_.size(); - n_extern_ = external_map_.size(); - size_ = n_intern_; - // Allocate subsystem vectors. y_ext_data_.resize(n_extern_); yp_ext_data_.resize(n_extern_); f_ext_data_.resize(n_extern_); - connection_nodes_ = std::make_unique(size_); - if (!allocated_) + connection_nodes_ = std::make_unique(n_intern_ + n_extern_); + if (!this->isAllocated()) { allocateVectors(static_cast(size_), true); abs_tol_.setToZero(memory::HOST); @@ -181,86 +178,91 @@ namespace GridKit external_data_indices_.resize(n_extern_); // Store the mapping from local subsystem indices back to their global system indices - for (const auto [global_idx, local_idx] : internal_map_) + for (const auto [global_idx, local_idx] : global_to_local_map_) { this->setConnectionNodes(local_idx, global_idx); } // Store the global indices of all external coupling variables. - for (const auto [global_idx, local_idx] : external_map_) + size_t counter = 0; + for (size_t i = n_intern_; i < (n_intern_ + n_extern_); i++) { - external_data_indices_[local_idx - n_intern_] = global_idx; + external_data_indices_[counter] = this->getNodeConnection(i); + counter++; } - { // Start node internal indexing after all component internals for proper KLU ordering - size_t node_internal_idx; - for (node_type* node : nodes_) + // Start node internal indexing after all component internals for proper KLU ordering + for (node_type* node : nodes_) + { + for (size_t i = 0; i < node->getInternalSize(); i++) { - for (size_t i = 0; i < node->getInternalSize(); i++) - { - node_internal_idx = node->getNodeConnection(i).idx_; + const IdxT node_global_connection = node->getNodeConnection(i).idx_; + const IdxT node_local_connection = global_to_local_map_.at(node_global_connection); - ExternalConnection node_connection{ - .y_ = y_int_ + node_internal_idx, - .yp_ = yp_int_ + node_internal_idx, - .f_ = f_int_ + node_internal_idx, - .idx_ = static_cast(node_internal_idx)}; + ExternalConnection node_connection{ + .y_ = y_int_ + node_local_connection, + .yp_ = yp_int_ + node_local_connection, + .f_ = f_int_ + node_local_connection, + .idx_ = static_cast(node_local_connection)}; - node->setExternalConnectionNodes(i, node_connection); - } + node->setExternalConnectionNodes(i, node_connection); } } + ScalarT* y_ext_data_ptr = y_ext_data_.getData(); + ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); + ScalarT* f_ext_data_ptr = f_ext_data_.getData(); + + size_t component_internal_idx = 0; + for (component_type* comp : components_) { - ScalarT* y_ext_data_ptr = y_ext_data_.getData(); - ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); - ScalarT* f_ext_data_ptr = f_ext_data_.getData(); - - // The offset for each component's internal variables in the system vector. - // They start at 0, and are stacked on top of each other. - size_t component_internal_idx = 0; - for (component_type* comp : components_) - { - // Update component internal pointers to their correct offsets - comp->setInternalPointer(&y_int_[component_internal_idx]); - comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); - comp->setInternalResidualPointer(&f_int_[component_internal_idx]); + // Update component internal pointers to their correct offsets + comp->setInternalPointer(&y_int_[component_internal_idx]); + comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); + comp->setInternalResidualPointer(&f_int_[component_internal_idx]); - component_internal_idx += comp->getInternalSize(); + component_internal_idx += comp->getInternalSize(); - const auto& external_indices = comp->getExternIndices(); + const auto& external_indices = comp->getExternIndices(); - for (IdxT local_index : external_indices) - { - const IdxT connection_index = comp->getNodeConnection(local_index); + for (IdxT i = 0; i < comp->size(); i++) + { + const IdxT comp_global_connection = comp->getNodeConnection(i); + const IdxT comp_local_connection = global_to_local_map_.at(comp_global_connection); - // A variable can be external from the component's point of view while still - // being owned by this subsystem. In that case, connect the component directly - // to the corresponding entry in the subsystem's internal vectors. - if (connection_index < n_intern_) - { - ExternalConnection connection{ - .y_ = y_int_ + connection_index, - .yp_ = yp_int_ + connection_index, - .f_ = f_int_ + connection_index, - .idx_ = connection_index}; + // Internal component variables use their subsystem-local connection indices. + if (!external_indices.contains(i)) + { + comp->setConnectionNodes(i, comp_local_connection); + continue; + } - comp->setExternalConnectionNodes(local_index, connection); + if (comp_local_connection < this->getInternalSize()) + { + // This variable is external to the component but internal to the subsystem, + // since it is owned by another component within the subsystem. - continue; - } + ExternalConnection connection{ + .y_ = y_int_ + comp_local_connection, + .yp_ = yp_int_ + comp_local_connection, + .f_ = f_int_ + comp_local_connection, + .idx_ = comp_local_connection}; + comp->setExternalConnectionNodes(i, connection); + } + else + { // Otherwise the variable is owned outside this subsystem. Connect the // component to the subsystem's external coupling-data storage instead. - const IdxT external_offset = connection_index - static_cast(n_intern_); + const IdxT external_offset = comp_local_connection - static_cast(this->getInternalSize()); ExternalConnection connection{ .y_ = y_ext_data_ptr + external_offset, .yp_ = yp_ext_data_ptr + external_offset, .f_ = f_ext_data_ptr + external_offset, - .idx_ = connection_index}; + .idx_ = comp_local_connection}; - comp->setExternalConnectionNodes(local_index, connection); + comp->setExternalConnectionNodes(i, connection); } } } @@ -283,7 +285,7 @@ namespace GridKit // variables are retained. auto isValidEntry = [this](IdxT row, IdxT col) { - if (row == neg1_ || col == neg1_) + if (row == INVALID_INDEX || col == INVALID_INDEX) { return false; } @@ -319,8 +321,7 @@ namespace GridKit IdxT* cols_dup = new IdxT[nnz_dup]; RealT* vals_dup = new RealT[nnz_dup]; - IdxT counter = 0; - + counter = 0; for (const component_type* component : components_) { const IdxT* r = component->jacobianCooRows(); @@ -464,13 +465,6 @@ namespace GridKit "SubsystemModel::addComponent: cannot add an unallocated component."); } - if (connections_are_local_) - { - throw std::logic_error( - "SubsystemModel::addComponent: cannot add component while " - "in local-indexed state. Call release() first."); - } - SystemModel::addComponent(component); } @@ -491,13 +485,6 @@ namespace GridKit "SubsystemModel::addNode: cannot add an unallocated node."); } - if (connections_are_local_) - { - throw std::logic_error( - "SubsystemModel::addNode: cannot add node while in " - "local-indexed state. Call release() first."); - } - SystemModel::addNode(node); } @@ -516,29 +503,29 @@ namespace GridKit } /** - * @brief Restore the subsystem topology to global system indexing. + * @brief Restores the subsystem to global system indexing. * - * Reverses local subsystem connection indices with the original - * global system indices. The local internal/external mappings are - * then cleared so that components or nodes can safely be added or removed. + * Restores the original global connection indices of all components and nodes, + * then clears the subsystem's global-to-local connection map. The subsystem is + * marked unallocated so that its local indexing can be rebuilt when it is + * allocated again. * - * @pre Component and node connections may use local subsystem indices. + * @pre Component and node connections may use subsystem-local indices. * - * @post Component and node connections use their original global indices, - * the subsystem mappings are cleared, and the subsystem is marked - * unallocated. + * @post Component and node connections use their original global system indices, + * `global_to_local_map_` is empty, and the subsystem is unallocated. * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + * @return 0 on success, otherwise an error code returned while restoring the + * global connections. */ - int release() + int restoreGlobalConnection() { if (int err_code = mapLocalToGlobal()) { return err_code; } - internal_map_.clear(); - external_map_.clear(); + global_to_local_map_.clear(); allocated_ = false; @@ -572,244 +559,156 @@ namespace GridKit const std::unordered_map& getInternalMap() const { - return internal_map_; - } - - const std::unordered_map& getExternalMap() const - { - return external_map_; + return global_to_local_map_; } private: /** - * @brief Restore local subsystem connection indices to global system indices. + * @brief Restores subsystem-local connection indices to global system indices. * - * Reverses \ref mapGlobalToLocal(). Internal subsystem indices are translated - * through the subsystem connection-node table, while external subsystem - * indices are translated through external_data_indices_. + * Replaces the local connection indices stored by each component and node with + * their corresponding global system indices. Each local index is mapped back to + * its original global connection using the subsystem connection table. * - * The component/node connectivity is unchanged; only the index representation - * is restored. + * This reverses the local indexing established during subsystem allocation + * without changing the underlying component or node connectivity. * - * @pre Component and node connections use local subsystem indices. + * @pre Component and node connections use subsystem-local indices. * - * @post Component and node connections use their original global indices. + * @post Component and node connections use their original global system indices. * * @return 0 on success. */ int mapLocalToGlobal() { - if (!connections_are_local_) + if (!this->isAllocated()) { return 0; } + IdxT local_connection; + for (component_type* component : components_) { - for (IdxT i = 0; i < component->size(); i++) { - const IdxT index = component->getNodeConnection(i); + local_connection = component->getNodeConnection(i); - if (index == neg1_) - { - continue; - } - else if (index < this->getInternalSize()) - { - component->setConnectionNodes(i, this->getNodeConnection(index)); - } - else + if (local_connection != INVALID_INDEX) { - component->setConnectionNodes(i, external_data_indices_[index - this->getInternalSize()]); + component->setConnectionNodes(i, this->getNodeConnection(local_connection)); } } } for (node_type* node : nodes_) { - for (IdxT i = 0; i < node->size(); i++) { - const IdxT index = node->getNodeConnection(i).idx_; + local_connection = node->getNodeConnection(i).idx_; - if (index == neg1_) + if (local_connection != INVALID_INDEX) { - continue; + node->setConnectionNodes(i, this->getNodeConnection(local_connection)); } - node->setConnectionNodes(i, this->getNodeConnection(index)); } } - connections_are_local_ = false; - return 0; } /** - * @brief Replace global connection indices with subsystem-local indices. + * @brief Builds the global-to-local connection map for the subsystem. * - * Uses the mappings created by \ref buildIndexMappings() to rewrite the connection - * indices stored by every component and node. Variables owned by the subsystem - * use `internal_map_`; variables owned outside the subsystem use `external_map_`. + * Assigns a local connection index to every variable referenced by the subsystem. + * Variables owned by components and nodes in the subsystem form the internal + * variables of the subsystem and are assigned the first N local indices [0, N). + * Variables required by the subsystem but owned outside it are then assigned + * local indices starting at N. * - * This changes only the indexing used to identify connections; it does not - * change the physical component/node connectivity. - * - * @pre `internal_map_` and `external_map_` have been constructed from global - * connection indices. - * - * @post All valid component and node connections use local subsystem indices. - * - * @return 0 on success. - */ - int mapGlobalToLocal() - { - if (connections_are_local_) - { - return 0; - } - - for (component_type* component : components_) - { - for (IdxT i = 0; i < component->size(); ++i) - { - const IdxT index = component->getNodeConnection(i); - - if (index == neg1_) - { - continue; - } - - if (internal_map_.contains(index)) - { - component->setConnectionNodes(i, internal_map_.at(index)); - } - else - { - component->setConnectionNodes(i, external_map_.at(index)); - } - } - } - - for (node_type* node : nodes_) - { - for (IdxT i = 0; i < node->size(); ++i) - { - const IdxT index = node->getNodeConnection(i).idx_; - - if (index == neg1_) - { - continue; - } - - node->setConnectionNodes(i, internal_map_.at(index)); - } - } - - connections_are_local_ = true; - - return 0; - } - - /** - * @brief Build the global-to-local variable mappings for the subsystem. - * - * Examines the connection indices of all components and nodes in the - * subsystem and divides the referenced variables into two groups: - * - * - Internal variables are owned by a component or node in this subsystem. - * - External variables are required by a subsystem component but are owned - * outside the subsystem. - * - * Internal variables receive local indices first. External coupling variables - * are then assigned indices immediately after the internal range. This gives - * every variable referenced by the subsystem a unique local index while - * preserving its original global index in the corresponding map. + * A variable that is external to a component may still be internal to the + * subsystem if it is owned by another component or node in the same subsystem. + * Such variables retain the local indices assigned during the internal passes. * * @pre Component and node connections use global system indices. * - * @post internal_map_ and external_map_ contain the global-to-local mappings - * needed to convert the subsystem topology to local indexing. + * @post `global_to_local_map_` maps each referenced global connection to its + * subsystem-local index, with internal variables followed by external + * variables. * * @return 0 on success. */ - int buildIndexMappings() + int buildConnectionMap() { - if (connections_are_local_) - { - return 0; - } - - internal_map_.clear(); - external_map_.clear(); + global_to_local_map_.clear(); - size_t component_internal_idx = 0; - // Pass 1: Add variables owned internally by subsystem components. + // Collect each component's internal variables and assign them local indices, + // since they make up the internal variables of the subsystem. + IdxT local_connection = 0; for (component_type* comp : components_) { const auto& extern_indices = comp->getExternIndices(); for (IdxT i = 0; i < comp->size(); i++) { - const IdxT index = comp->getNodeConnection(i); + const IdxT global_connection = comp->getNodeConnection(i); - if (index != neg1_ && !extern_indices.contains(i)) + if (global_connection != INVALID_INDEX && !extern_indices.contains(i)) { - internal_map_[index] = component_internal_idx++; + global_to_local_map_[global_connection] = local_connection++; } } } - // Pass 2: Add variables owned by subsystem nodes. + // Node variables are also owned by the subsystem and therefore complete + // the set of subsystem-internal variables. for (node_type* node : nodes_) { - for (IdxT i = 0; i < node->size(); i++) { - const IdxT index = node->getNodeConnection(i).idx_; + const IdxT global_connection = node->getNodeConnection(i).idx_; - if (index != neg1_) + if (global_connection != INVALID_INDEX) { - internal_map_[index] = component_internal_idx++; + global_to_local_map_[global_connection] = local_connection++; } } } - // Pass 3: Add component dependencies that are owned outside the subsystem. - size_t component_external_idx = component_internal_idx; + // Everything assigned so far is internal to the subsystem. + const IdxT internal_size = local_connection; + + // Finally assign local indices to variables owned outside the subsystem. for (component_type* comp : components_) { auto extern_indices = comp->getExternIndices(); + for (IdxT j = 0; j < comp->size(); j++) { - if (!extern_indices.contains(j)) + if (extern_indices.contains(j)) { - continue; - } - - const IdxT index = comp->getNodeConnection(j); + const IdxT global_connection = comp->getNodeConnection(j); - if (index != neg1_ && !internal_map_.contains(index) && !external_map_.contains(index)) - { - external_map_[index] = component_external_idx++; + if (global_connection != INVALID_INDEX && !global_to_local_map_.contains(global_connection)) + { + global_to_local_map_[global_connection] = local_connection++; + } } } } + n_intern_ = static_cast(internal_size); + n_extern_ = static_cast(local_connection - internal_size); + size_ = local_connection; + return 0; } /** - *@brief Maps global system indices to local subsystem indices for internal variables. - */ - std::unordered_map internal_map_; - - /** - * @brief Maps global system indices to local subsystem indices for external variables. + *@brief Maps global system connection indices to local subsystem connection indices. */ - std::unordered_map external_map_; + std::unordered_map global_to_local_map_; /** * @brief Global system index corresponding to each entry in the external subsystem vectors. @@ -846,11 +745,6 @@ namespace GridKit */ std::vector interfaces_; - /** - * @brief Keeps track of whether the components are in local state or global state - */ - bool connections_are_local_{false}; - }; // class SubsystemModel -} // namespace GridKit +} // namespace GridKit \ No newline at end of file From e37d476e5667ad530804a620500dd039886a10cd Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sat, 19 Sep 2026 00:14:55 +0000 Subject: [PATCH 13/31] Update BusPartitionInterface constructor and documentation --- .../BusPartitionInterface.cpp | 581 +++++++++--------- .../BusPartitionInterface.hpp | 297 ++++----- 2 files changed, 443 insertions(+), 435 deletions(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp index d4619f3f16..c84e35855a 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp @@ -11,355 +11,360 @@ namespace GridKit { - /** - * @brief Construct a partition interface between a bus and a circuit component. - * - * The interface wraps a copy of a component located across a partition - * boundary and evaluates the contributions that the component makes to the - * connected bus. All interface variables are treated as external variables - * and have the same size as the wrapped component. - * - * - * @param bus Bus associated with the partition interface. - * @param component Copy of the component across the partition boundary. - * @param id Unique identifier for the interface. - */ - template - BusPartitionInterface::BusPartitionInterface(node_type* bus, component_type* component, IdxT id) - : component_(component->clone()), - bus_(bus) + namespace PowerElectronics { - size_ = component_->size(); - n_intern_ = 0; - n_extern_ = static_cast(component_->size()); - idc_ = id; - // All variables of the bus interface are external to the interface. - for (IdxT i = 0; i < size_; i++) + /** + * @brief Constructs a partition interface between a bus and a circuit component. + * + * Initializes the interface with a clone of the component across the partition + * boundary and marks all interface variables as external. The constructor also + * identifies the component Jacobian rows associated with the bus and stores the + * corresponding Jacobian entries for use by the interface. + * + * @param bus Bus associated with the partition interface. + * @param component Component across the partition boundary to clone. + * @param id Unique identifier for the interface. + */ + template + BusPartitionInterface::BusPartitionInterface(node_type* bus, component_type* component, IdxT id) + : component_(component->clone()), + bus_(bus) { - extern_indices_.insert(i); - } + size_ = component_->size(); + n_intern_ = 0; + n_extern_ = static_cast(component_->size()); + idc_ = id; - // Map each global bus connection index to its position in the bus. - std::unordered_map bus_connections; + // Mark all interface variables as external. + for (IdxT i = 0; i < size_; i++) + { + extern_indices_.insert(i); + } - for (size_t i = 0; i < static_cast(bus_->size()); ++i) - { - bus_connections[bus_->getNodeConnection(i).idx_] = i; - } + // Collect the global indices of variables associated with the bus. + std::vector bus_connections(static_cast(bus_->size())); - // The interface Jacobian contains only rows corresponding to variables - // owned by the bus. - const IdxT* coo_rows = component_->jacobianCooRows(); + for (size_t i = 0; i < bus_connections.size(); ++i) + { + bus_connections[i] = bus_->getNodeConnection(i).idx_; + } - nnz_ = 0; + // Identify bus-associated Jacobian rows and collect all entries in each row. + const IdxT* coo_rows = component_->jacobianCooRows(); - for (IdxT k = 0; k < component_->nnz(); ++k) - { - const IdxT row_node = component_->getNodeConnection(static_cast(coo_rows[k])); + nnz_ = 0; - if (bus_connections.contains(row_node)) + for (IdxT k = 0; k < component_->nnz(); ++k) { - ++nnz_; - jac_map_.push_back(k); // Keep track of the entries so they can be easily extracted - } - } - } + const IdxT row_node = component_->getNodeConnection(static_cast(coo_rows[k])); - template - BusPartitionInterface::~BusPartitionInterface() - { - delete component_; - } - - /** - * @brief Allocate storage and initialize the interface mappings. - * - * Identifies the external variables of the wrapped component that are - * connected to the bus and builds the mappings used to transfer residual - * contributions between the component and the interface. Private storage is - * also allocated for the internal variables of the wrapped component. - * - * @return 0 on success and a nonzero value if the component does not contain - * all variables required by the bus interface. - */ - template - int BusPartitionInterface::allocate() - { - - CircuitComponent::allocate(); + bool is_bus_row = std::find(bus_connections.begin(), bus_connections.end(), row_node) != bus_connections.end(); - // Build a lookup of the global indices belonging to the bus. - std::unordered_map bus_connections; + if (is_bus_row) + { + ++nnz_; + jac_map_.push_back(k); // Keep track of the entries so they can be easily extracted + } + } + } - for (size_t i = 0; i < static_cast(bus_->size()); ++i) + template + BusPartitionInterface::~BusPartitionInterface() { - bus_connections[bus_->getNodeConnection(i).idx_] = i; } - const auto& external_indices = component_->getExternIndices(); + /** + * @brief Allocate storage and initialize the interface mappings. + * + * Identifies the external variables of the wrapped component that are + * connected to the bus and builds the mappings used to transfer residual + * contributions between the component and the interface. Private storage is + * also allocated for the internal variables of the wrapped component. + * + * @return 0 on success and a nonzero value if the component does not contain + * all variables required by the bus interface. + */ + template + int BusPartitionInterface::allocate() + { - is_external_.assign(static_cast(size_), false); + CircuitComponent::allocate(); - bus_input_ports_.clear(); - bus_output_ports_.clear(); + // Build a lookup of the global indices belonging to the bus. + std::unordered_map bus_connections; - size_t external_index = 0; + for (size_t i = 0; i < static_cast(bus_->size()); ++i) + { + bus_connections[bus_->getNodeConnection(i).idx_] = i; + } - // Identify which component variables are external and which of those external - // variables are connected to the bus. bus_input_ports_ stores the corresponding - // variable position in this interface, while bus_output_ports_ stores its position - // in the wrapped component's external residual vector. - for (size_t i = 0; i < static_cast(size_); ++i) - { - const IdxT connection_index = component_->getNodeConnection(i); + const auto& external_indices = component_->getExternIndices(); - this->setConnectionNodes(i, connection_index); + is_external_.assign(static_cast(size_), false); - if (!external_indices.contains(static_cast(i))) + bus_input_ports_.clear(); + bus_output_ports_.clear(); + + size_t external_index = 0; + + // Identify which component variables are external and which of those external + // variables are connected to the bus. bus_input_ports_ stores the corresponding + // variable position in this interface, while bus_output_ports_ stores its position + // in the wrapped component's external residual vector. + for (size_t i = 0; i < static_cast(size_); ++i) { - continue; + const IdxT connection_index = component_->getNodeConnection(i); + + this->setConnectionNodes(i, connection_index); + + if (external_indices.contains(static_cast(i))) + { + is_external_[i] = true; + + if (bus_connections.contains(connection_index)) + { + bus_input_ports_.push_back(i); + bus_output_ports_.push_back(external_index); + } + + ++external_index; + } } - is_external_[i] = true; + // A valid bus interface must contain every variable belonging to the bus. + // If fewer bus variables are found, the wrapped component does not provide + // the complete coupling required by this interface. + const size_t bus_size = static_cast(bus_->size()); - if (bus_connections.contains(connection_index)) + if (bus_input_ports_.size() != bus_size) { - bus_input_ports_.push_back(i); - bus_output_ports_.push_back(external_index); + GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " + << "Bus(ID=" << bus_->busID() + << "), Component(ID=" << component_->getIDcomponent() + << "). Expected " << bus_size + << " bus connections, but found " + << bus_input_ports_.size() << "." + << std::endl; + + return 1; } - ++external_index; - } + // The wrapped component is evaluated independently by the interface. + // Allocate storage for its internal variables and residuals and redirect + // its internal pointers to this private storage. + const size_t internal_size = static_cast(component_->getInternalSize()); + + component_y_int_ = std::make_unique(internal_size); + component_yp_int_ = std::make_unique(internal_size); + component_f_int_ = std::make_unique(internal_size); + + component_->setInternalPointer(component_y_int_.get()); + component_->setInternalDerivativePointer(component_yp_int_.get()); + component_->setInternalResidualPointer(component_f_int_.get()); - // A valid bus interface must contain every variable belonging to the bus. - // If fewer bus variables are found, the wrapped component does not provide - // the complete coupling required by this interface. - const size_t bus_size = static_cast(bus_->size()); + component_f_ext_ = std::make_unique(component_->getExternSize()); - if (bus_input_ports_.size() != bus_size) + return 0; + } + + template + int BusPartitionInterface::setAbsoluteTolerance(RealT rel_tol) { - GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " - << "Bus(ID=" << bus_->busID() - << "), Component(ID=" << component_->getIDcomponent() - << "). Expected " << bus_size - << " bus connections, but found " - << bus_input_ports_.size() << "." - << std::endl; - - return 1; + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; } - // The wrapped component is evaluated independently by the interface. - // Allocate storage for its internal variables and residuals and redirect - // its internal pointers to this private storage. - const size_t internal_size = static_cast(component_->getInternalSize()); + /** + * @brief Initialize the partition interface. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::initialize() + { + return 0; + } - component_y_int_ = std::make_unique(internal_size); - component_yp_int_ = std::make_unique(internal_size); - component_f_int_ = std::make_unique(internal_size); + /** + * @brief Identify differential variables + */ + template + int BusPartitionInterface::tagDifferentiable() + { + return 0; + } - component_->setInternalPointer(component_y_int_.get()); - component_->setInternalDerivativePointer(component_yp_int_.get()); - component_->setInternalResidualPointer(component_f_int_.get()); + /** + * @brief Evaluates the internal residual. + * + * All variables are external from the interface's point of view, so there + * are no internal residual contributions to evaluate. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::evaluateInternalResidual() + { + return 0; + } - component_f_ext_ = std::make_unique(component_->getExternSize()); + /** + * @brief Evaluate the wrapped component's contributions to the bus residual. + * + * The wrapped component is evaluated using state information supplied through + * the interface. Residual contributions associated with bus variables are + * extracted from the component's external residual and accumulated into the + * corresponding interface residual entries. + * + * @return 0 on success, or the error code returned by the wrapped component. + */ + template + int BusPartitionInterface::evaluateExternalResidual() + { + std::fill_n(component_f_ext_.get(), component_->getExternSize(), ScalarT{}); - return 0; - } + updateComponentPointers(); - template - int BusPartitionInterface::setAbsoluteTolerance(RealT rel_tol) - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - /** - * @brief Initialize the partition interface. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::initialize() - { - return 0; - } - - /** - * @brief Identify differential variables - */ - template - int BusPartitionInterface::tagDifferentiable() - { - return 0; - } - - /** - * @brief Eval Internal Residual - */ - template - int BusPartitionInterface::evaluateInternalResidual() - { - return 0; - } - - /** - * @brief Evaluate the wrapped component's contributions to the bus residual. - * - * The wrapped component is evaluated using state information supplied through - * the interface. Residual contributions associated with bus variables are - * extracted from the component's external residual and accumulated into the - * corresponding interface residual entries. - * - * @return 0 on success, or the error code returned by the wrapped component. - */ - template - int BusPartitionInterface::evaluateExternalResidual() - { - std::fill_n(component_f_ext_.get(), component_->getExternSize(), ScalarT{}); + if (int err_code = component_->evaluateExternalResidual()) + { + return err_code; + } - updateComponentPointers(); + // Only contributions associated with the bus are accumulated + // into the interface residual below. + for (size_t i = 0; i < bus_input_ports_.size(); ++i) + { + *f_ext_[bus_input_ports_[i]] += component_f_ext_[bus_output_ports_[i]]; + } - if (int err_code = component_->evaluateExternalResidual()) - { - return err_code; + return 0; } - // Only contributions associated with the bus are accumulated - // into the interface residual below. - for (size_t i = 0; i < bus_input_ports_.size(); ++i) + /** + * @brief Evaluate the Jacobian contributions associated with the bus. + * + * Evaluates the Jacobian of the wrapped component and extracts the entries + * whose residual rows correspond to variables belonging to the connected bus. + * The selected entries are then used to assemble the interface Jacobian. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::evaluateJacobian() { - *f_ext_[bus_input_ports_[i]] += component_f_ext_[bus_output_ports_[i]]; - } - return 0; - } - - /** - * @brief Evaluate the Jacobian contributions associated with the bus. - * - * Evaluates the Jacobian of the wrapped component and extracts the entries - * whose residual rows correspond to variables belonging to the connected bus. - * The selected entries are then used to assemble the interface Jacobian. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::evaluateJacobian() - { + this->zeroJacMatrix(); - this->zeroJacMatrix(); + updateComponentPointers(); - updateComponentPointers(); + component_->evaluateJacobian(); - component_->evaluateJacobian(); + const IdxT* cooRows = component_->jacobianCooRows(); + const IdxT* cooCols = component_->jacobianCooCols(); + const RealT* cooVals = component_->jacobianCooValues(); - const IdxT* cooRows = component_->jacobianCooRows(); - const IdxT* cooCols = component_->jacobianCooCols(); - const RealT* cooVals = component_->jacobianCooValues(); + std::vector rows; + std::vector cols; + std::vector vals; - std::vector rows; - std::vector cols; - std::vector vals; + rows.reserve(jac_map_.size()); + cols.reserve(jac_map_.size()); + vals.reserve(jac_map_.size()); - rows.reserve(jac_map_.size()); - cols.reserve(jac_map_.size()); - vals.reserve(jac_map_.size()); + // Extract only the Jacobian entries whose residual rows belong to the bus. + for (const IdxT index : jac_map_) + { + rows.push_back(cooRows[index]); + cols.push_back(cooCols[index]); + vals.push_back(cooVals[index]); + } - // Extract only the Jacobian entries whose residual rows belong to the bus. - for (const IdxT index : jac_map_) - { - rows.push_back(cooRows[index]); - cols.push_back(cooCols[index]); - vals.push_back(cooVals[index]); - } + this->setJacValues(rows, cols, vals); - this->setJacValues(rows, cols, vals); - - return 0; - } - - /** - * @brief Update the wrapped component with state and output residual pointers. - * - * Reconstructs the state required to evaluate the wrapped component using - * data supplied through the interface. External component variables are - * connected directly to the interface data, while internal component - * variables are copied into the interface's private storage, which the wrapped - * component already keeps track of for its internal state. - * - * @pre The interface must be allocated and its external state pointers must - * reference valid state and derivative data. - * - * @post The wrapped component is configured with the state, derivative, and - * residual pointers required for evaluation. - * - * @param residual Storage for the component's external residual contributions, - * or nullptr when residual output is not required. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::updateComponentPointers() - { - size_t internal_index = 0; - size_t external_index = 0; + return 0; + } - // Reconstruct the state expected by the wrapped component from the - // interface's external data. Route the external residual to component_f_ext_. - for (size_t i = 0; i < static_cast(component_->size()); ++i) + /** + * @brief Update the wrapped component with state and output residual pointers. + * + * Reconstructs the state required to evaluate the wrapped component using + * data supplied through the interface. External component variables are + * connected directly to the interface data, while internal component + * variables are copied into the interface's private storage, which the wrapped + * component already keeps track of for its internal state. + * + * @pre The interface must be allocated and its external state pointers must + * reference valid state and derivative data. + * + * @post The wrapped component is configured with the state, derivative, and + * residual pointers required for evaluation. + * + * @param residual Storage for the component's external residual contributions, + * or nullptr when residual output is not required. + * + * @return 0 on success. + */ + template + int BusPartitionInterface::updateComponentPointers() { - if (is_external_[i]) - { - ExternalConnection connection{ - .y_ = y_ext_[i], - .yp_ = yp_ext_[i], - .f_ = &component_f_ext_[external_index], - .idx_ = component_->getNodeConnection(i)}; - - component_->setExternalConnectionNodes(i, connection); - ++external_index; - } - else + size_t internal_index = 0; + size_t external_index = 0; + + // Reconstruct the state expected by the wrapped component from the + // interface's external data. Route the external residual to component_f_ext_. + for (size_t i = 0; i < static_cast(component_->size()); ++i) { - component_y_int_[internal_index] = *y_ext_[i]; - component_yp_int_[internal_index] = *yp_ext_[i]; - ++internal_index; + if (is_external_[i]) + { + ExternalConnection connection{ + .y_ = y_ext_[i], + .yp_ = yp_ext_[i], + .f_ = &component_f_ext_[external_index], + .idx_ = component_->getNodeConnection(i)}; + + component_->setExternalConnectionNodes(i, connection); + ++external_index; + } + else + { + component_y_int_[internal_index] = *y_ext_[i]; + component_yp_int_[internal_index] = *yp_ext_[i]; + ++internal_index; + } } - } - - return 0; - } - template - int BusPartitionInterface::evaluateIntegrand() - { - return 0; - } + return 0; + } - template - int BusPartitionInterface::initializeAdjoint() - { - return 0; - } + template + int BusPartitionInterface::evaluateIntegrand() + { + return 0; + } - template - int BusPartitionInterface::evaluateAdjointResidual() - { - return 0; - } + template + int BusPartitionInterface::initializeAdjoint() + { + return 0; + } - template - int BusPartitionInterface::evaluateAdjointIntegrand() - { - return 0; - } + template + int BusPartitionInterface::evaluateAdjointResidual() + { + return 0; + } - // Available template instantiations - template class BusPartitionInterface; - template class BusPartitionInterface; - template class BusPartitionInterface; - template class BusPartitionInterface; + template + int BusPartitionInterface::evaluateAdjointIntegrand() + { + return 0; + } + // Available template instantiations + template class BusPartitionInterface; + template class BusPartitionInterface; + template class BusPartitionInterface; + template class BusPartitionInterface; + } // namespace PowerElectronics } // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp index 1f1109720d..5a557120a9 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp @@ -14,155 +14,158 @@ namespace GridKit { - /** - * @brief Partition interface that provides a wrapped component's current - * injection contributions to a bus. - * - * A BusPartitionInterface is used when a bus and one of its connected - * components belong to different partitions. The interface represents the - * component within the partition containing the bus and provides the current - * injection contributions that the wrapped component makes to that bus. - * - * The interface has the same number of variables as the wrapped component, - * with a one-to-one correspondence between interface variables and wrapped - * component variables. However, all variables are external from the - * interface's point of view, regardless of whether the corresponding variable - * is internal or external to the wrapped component: - * - * @code - * Interface variables: [ext_, ext_, ext_, ext_, ext_, ext_, ext_] - * | | | | | | | - * Wrapped component: [int_, int_, ext_, int_, ext_, ext_, ext_] - * @endcode - * - * This allows the partition containing the wrapped component to provide all - * state information required to reconstruct and evaluate the component on the - * bus side of the partition boundary. Variables that are internal to the - * wrapped component are stored in private interface storage during evaluation, - * while variables that are external to the wrapped component are connected - * directly to the corresponding interface data. - * - * After evaluating the wrapped component, the interface extracts the residual - * and Jacobian contributions associated with the bus and exposes them to the - * partition containing the bus. - * - * * @note The interface residual contributions are zero for all variables except - * those connected to the bus. Only variables connected to the bus expose - * the wrapped component's contribution to the partition. - * - * @note This interface must be added to the partition containing the bus - * to ensure that the system is properly partitioned. - * - * @tparam ScalarT Scalar type used by the model. - * @tparam IdxT Index type used for variables and connections. - */ - template - class BusPartitionInterface : public PartitionInterface + namespace PowerElectronics { - - using component_type = CircuitComponent; - using node_type = typename PowerElectronics::NodeBase; - using RealT = typename CircuitComponent::RealT; - - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::time_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::tag_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::g_; - using CircuitComponent::yB_; - using CircuitComponent::ypB_; - using CircuitComponent::fB_; - using CircuitComponent::gB_; - using CircuitComponent::param_; - using CircuitComponent::idc_; - - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - using CircuitComponent::connection_nodes_; - - public: - BusPartitionInterface(node_type* bus, component_type* component, IdxT id); - virtual ~BusPartitionInterface(); - - int allocate() final; - int initialize() final; - int tagDifferentiable() final; - int setAbsoluteTolerance(RealT) final; - int evaluateInternalResidual() final; - int evaluateExternalResidual() final; - int evaluateJacobian() final; - int evaluateIntegrand() final; - - int initializeAdjoint() final; - int evaluateAdjointResidual() final; - // int evaluateAdjointJacobian(); - int evaluateAdjointIntegrand() final; - - private: - int updateComponentPointers(); - - /** - * @brief Circuit component participating in the partition split. - */ - component_type* component_; - - /** - * @brief Bus associated with the partition boundary. - */ - node_type* bus_; - - /** - * @brief Storage for the wrapped component's internal state variables. - */ - std::unique_ptr component_y_int_; - - /** - * @brief Storage for the wrapped component's internal state derivatives. - */ - std::unique_ptr component_yp_int_; - - /** - * @brief Storage for the wrapped component's internal residual values. - */ - std::unique_ptr component_f_int_; - - /** - * @brief Storage for the wrapped component's external residual values. - */ - std::unique_ptr component_f_ext_; - - /** - * @brief Wrapped component ports that receive variables from the bus. - */ - std::vector bus_input_ports_; - - /** - * @brief Wrapped component ports whose residual contributions are exposed - * to the bus. - */ - std::vector bus_output_ports_; - - /** - * @brief Mapping from interface Jacobian entries to wrapped component - * Jacobian entries. - */ - std::vector jac_map_; - /** - * @brief Cached lookup indicating whether each wrapped component variable is external. + * @brief Partition interface that provides a wrapped component's current + * injection contributions to a bus. + * + * A BusPartitionInterface is used when a bus and one of its connected + * components belong to different partitions. The interface represents the + * component within the partition containing the bus and provides the current + * injection contributions that the wrapped component makes to that bus. + * + * The interface has the same number of variables as the wrapped component, + * with a one-to-one correspondence between interface variables and wrapped + * component variables. However, all variables are external from the + * interface's point of view, regardless of whether the corresponding variable + * is internal or external to the wrapped component: + * + * @code + * Interface variables: [ext_, ext_, ext_, ext_, ext_, ext_, ext_] + * | | | | | | | + * Wrapped component: [int_, int_, ext_, int_, ext_, ext_, ext_] + * @endcode + * + * This allows the partition containing the wrapped component to provide all + * state information required to reconstruct and evaluate the component on the + * bus side of the partition boundary. Variables that are internal to the + * wrapped component are stored in private interface storage during evaluation, + * while variables that are external to the wrapped component are connected + * directly to the corresponding interface data. + * + * After evaluating the wrapped component, the interface extracts the residual + * and Jacobian contributions associated with the bus and exposes them to the + * partition containing the bus. + * + * * @note The interface residual contributions are zero for all variables except + * those connected to the bus. Only variables connected to the bus expose + * the wrapped component's contribution to the partition. + * + * @note This interface must be added to the partition containing the bus + * to ensure that the system is properly partitioned. * - * Avoids repeatedly searching `extern_indices_` when determining whether - * a wrapped component variable is internal or external. + * @tparam ScalarT Scalar type used by the model. + * @tparam IdxT Index type used for variables and connections. */ - std::vector is_external_; - }; + template + class BusPartitionInterface : public PartitionInterface + { + + using component_type = CircuitComponent; + using node_type = typename PowerElectronics::NodeBase; + using RealT = typename CircuitComponent::RealT; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::time_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::tag_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::g_; + using CircuitComponent::yB_; + using CircuitComponent::ypB_; + using CircuitComponent::fB_; + using CircuitComponent::gB_; + using CircuitComponent::param_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + using CircuitComponent::connection_nodes_; + + public: + BusPartitionInterface(node_type* bus, component_type* component, IdxT id); + virtual ~BusPartitionInterface(); + + int allocate() final; + int initialize() final; + int tagDifferentiable() final; + int setAbsoluteTolerance(RealT) final; + int evaluateInternalResidual() final; + int evaluateExternalResidual() final; + int evaluateJacobian() final; + int evaluateIntegrand() final; + + int initializeAdjoint() final; + int evaluateAdjointResidual() final; + // int evaluateAdjointJacobian(); + int evaluateAdjointIntegrand() final; + + private: + int updateComponentPointers(); + + /** + * @brief Circuit component participating in the partition split. + */ + std::unique_ptr component_; + + /** + * @brief Bus associated with the partition boundary. + */ + node_type* bus_; + + /** + * @brief Storage for the wrapped component's internal state variables. + */ + std::unique_ptr component_y_int_; + + /** + * @brief Storage for the wrapped component's internal state derivatives. + */ + std::unique_ptr component_yp_int_; + + /** + * @brief Storage for the wrapped component's internal residual values. + */ + std::unique_ptr component_f_int_; + + /** + * @brief Storage for the wrapped component's external residual values. + */ + std::unique_ptr component_f_ext_; + + /** + * @brief Wrapped component ports that receive variables from the bus. + */ + std::vector bus_input_ports_; + + /** + * @brief Wrapped component ports whose residual contributions are exposed + * to the bus. + */ + std::vector bus_output_ports_; + + /** + * @brief Mapping from interface Jacobian entries to wrapped component + * Jacobian entries. + */ + std::vector jac_map_; + + /** + * @brief Cached lookup indicating whether each wrapped component variable is external. + * + * Avoids repeatedly searching `extern_indices_` when determining whether + * a wrapped component variable is internal or external. + */ + std::vector is_external_; + }; + } // namespace PowerElectronics } // namespace GridKit From ed86caadfba2102ddf9c98280d26418e3840dbc6 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sat, 19 Sep 2026 00:15:24 +0000 Subject: [PATCH 14/31] Update test case --- .../UnitTests/PowerElectronics/CMakeLists.txt | 4 +- .../SubsystemModelWithHiresTest.hpp | 54 ++++++++----------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index 9dd7db2ea0..a55122514d 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -6,7 +6,9 @@ target_link_libraries( add_executable(test_subsystem_model_with_hires runSubsystemModelWithHiresTest.cpp) target_link_libraries( test_subsystem_model_with_hires - PRIVATE GridKit::power_elec_partition_interfaces GridKit::testing GridKit::sparse_matrix) + PRIVATE GridKit::power_elec_partition_interfaces + GridKit::testing GridKit::sparse_matrix + GridKit::Utilities) add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( diff --git a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp index 71a1aaaefd..927d7c948f 100644 --- a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp +++ b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include namespace GridKit { @@ -541,7 +543,7 @@ namespace GridKit // Construct the partition interface // --------------------------------------------------------------------- - bus_interface_ = new BusPartitionInterface(&bus_, comp3_, 4); + bus_interface_ = new PowerElectronics::BusPartitionInterface(&bus_, comp3_, 4); bus_interface_->allocate(); @@ -571,7 +573,8 @@ namespace GridKit delete system_; } - void distributeVariables(const std::vector& y, const std::vector& yp) + template + void distributeVariables(const T& y, T& yp) { for (auto* partition : partitions_) { @@ -640,18 +643,13 @@ namespace GridKit auto* reference_residual = system_->getResidual().getData(); - RealT max_error = 0.0; + RealT tolerance = 100 * std::numeric_limits::epsilon(); for (size_t i = 0; i < system_->size(); ++i) { - double error = std::abs(partition_residual[i] - reference_residual[i]) / std::abs(reference_residual[i] + 1); - max_error = std::max(max_error, error); + success *= Testing::isEqual(partition_residual[i], reference_residual[i], tolerance); } - std::cout << "max error " << max_error << std::endl; - - success *= max_error <= std::numeric_limits::epsilon(); - return success.report(__func__); } @@ -698,35 +696,26 @@ namespace GridKit auto* partition_jac = partition->getCsrJacobian(); - const auto* row_ptr = partition_jac->getRowData(); - const auto* cols = partition_jac->getColData(); - const auto* vals = partition_jac->getValues(); - - const size_t n = partition->getInternalSize(); - - std::vector> dense_jac(n, std::vector(n, 0.0)); - - // Convert the partition CSR Jacobian to a dense matrix. - for (size_t row = 0; row < n; ++row) - { - for (IdxT k = row_ptr[row]; k < row_ptr[row + 1]; ++k) - { - dense_jac[row][cols[k]] = vals[k]; - } - } + const auto jac_map = Testing::MapFromCsr(partition_jac); + const size_t partition_size = partition->getInternalSize(); // Compare with the corresponding entries of the full Jacobian. - for (size_t row = 0; row < n; ++row) + for (size_t row = 0; row < partition_size; ++row) { const IdxT global_row = partition->getNodeConnection(row); const IdxT ref_row = sysmodel_to_hires[global_row]; - for (size_t col = 0; col < n; ++col) + for (size_t col = 0; col < partition_size; ++col) { const IdxT global_col = partition->getNodeConnection(col); const IdxT ref_col = sysmodel_to_hires[global_col]; - success *= std::abs(dense_jac[row][col] - reference_jac[ref_row][ref_col]) <= std::numeric_limits::epsilon(); + const auto& jac_row = jac_map[row]; + const RealT jac_value = jac_row.contains(col) ? jac_row.at(col) : RealT{0}; + const RealT ref_jac_value = reference_jac[ref_row][ref_col]; + const RealT tolerance = 100 * std::numeric_limits::epsilon(); + + success *= Testing::isEqual(jac_value, ref_jac_value, tolerance); } } } @@ -735,6 +724,9 @@ namespace GridKit } private: + /// @brief system size. + static constexpr size_t SYSTEM_SIZE = 8; + /// @brief Bus used to define the connection between the HIRES components. Bus bus_; @@ -757,16 +749,16 @@ namespace GridKit HiresComponent3* comp3_; /// @brief Partition interface representing the bus connection across the subsystem boundary. - BusPartitionInterface* bus_interface_; + PowerElectronics::BusPartitionInterface* bus_interface_; /// @brief Collection of subsystems comprising the partitioned HIRES system. std::vector partitions_; /// @brief State vector used to initialize and evaluate the HIRES system. - std::vector y_; + std::array y_; /// @brief State derivative vector used to initialize and evaluate the HIRES system. - std::vector yp_; + std::array yp_; }; } // namespace Testing From daba0eeb34b75fc1ea0bfdec98f2ac895201de8b Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Mon, 12 Oct 2026 05:38:17 +0000 Subject: [PATCH 15/31] Bug fix --- GridKit/Model/PowerElectronics/SubsystemModel.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index a28439d688..71b78af388 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -700,7 +700,7 @@ namespace GridKit n_intern_ = static_cast(internal_size); n_extern_ = static_cast(local_connection - internal_size); - size_ = local_connection; + size_ = internal_size; return 0; } From ceb7c5c0201ffd0a4dd55939ebcf2cb4f1e0c48f Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Mon, 12 Oct 2026 13:10:27 +0000 Subject: [PATCH 16/31] Update SubsystemModel --- .../Model/PowerElectronics/SubsystemModel.hpp | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 71b78af388..27c1c635c9 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -161,22 +161,26 @@ namespace GridKit return err_code; } - // Allocate subsystem vectors. - y_ext_data_.resize(n_extern_); - yp_ext_data_.resize(n_extern_); - f_ext_data_.resize(n_extern_); + // Allocate external subsystem vectors. + y_ext_data_.resize(static_cast(n_extern_)); + yp_ext_data_.resize(static_cast(n_extern_)); + f_ext_data_.resize(static_cast(n_extern_)); + // Contains global external indicies used for + // fetching external variables from global state vectors + external_data_indices_.resize(n_extern_); + + // Contains internal and external connection indicies connection_nodes_ = std::make_unique(n_intern_ + n_extern_); if (!this->isAllocated()) { + // Allocate internal subsystem vectors. allocateVectors(static_cast(size_), true); abs_tol_.setToZero(memory::HOST); } tag_.resize(size_); - external_data_indices_.resize(n_extern_); - // Store the mapping from local subsystem indices back to their global system indices for (const auto [global_idx, local_idx] : global_to_local_map_) { @@ -191,10 +195,11 @@ namespace GridKit counter++; } - // Start node internal indexing after all component internals for proper KLU ordering + // Map each node's global connection indices to subsystem-local indices, + // then set the corresponding state, derivative, and residual pointers. for (node_type* node : nodes_) { - for (size_t i = 0; i < node->getInternalSize(); i++) + for (IdxT i = 0; i < static_cast(node->getInternalSize()); i++) { const IdxT node_global_connection = node->getNodeConnection(i).idx_; const IdxT node_local_connection = global_to_local_map_.at(node_global_connection); @@ -213,6 +218,7 @@ namespace GridKit ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); ScalarT* f_ext_data_ptr = f_ext_data_.getData(); + // Map connections from global indices to subsystem-local indices and set data pointers accordingly. size_t component_internal_idx = 0; for (component_type* comp : components_) { @@ -225,22 +231,24 @@ namespace GridKit const auto& external_indices = comp->getExternIndices(); + // Map each component connection from its global index to the + // corresponding subsystem-local index. for (IdxT i = 0; i < comp->size(); i++) { const IdxT comp_global_connection = comp->getNodeConnection(i); const IdxT comp_local_connection = global_to_local_map_.at(comp_global_connection); - // Internal component variables use their subsystem-local connection indices. if (!external_indices.contains(i)) { + // If the connection is internal to the component, we know it is internal to the subsystem, + // so only update its index. The pointers have already been updated in the outer loop. comp->setConnectionNodes(i, comp_local_connection); - continue; } - - if (comp_local_connection < this->getInternalSize()) + else if (comp_local_connection < static_cast(this->getInternalSize())) { - // This variable is external to the component but internal to the subsystem, - // since it is owned by another component within the subsystem. + // Otherwise, if the variable is external to this component but internal to + // another component or node within the subsystem (which it is when the local index + // is less than internal size), connect it to the subsystem's internal storage. ExternalConnection connection{ .y_ = y_int_ + comp_local_connection, @@ -252,8 +260,9 @@ namespace GridKit } else { - // Otherwise the variable is owned outside this subsystem. Connect the - // component to the subsystem's external coupling-data storage instead. + // Otherwise, the variable is external to the subsystem, so connect + // it to the external coupling storage. + const IdxT external_offset = comp_local_connection - static_cast(this->getInternalSize()); ExternalConnection connection{ @@ -290,8 +299,8 @@ namespace GridKit return false; } - const bool row_is_internal = row < this->getInternalSize(); - const bool col_is_internal = col < this->getInternalSize(); + const bool row_is_internal = static_cast(row) < this->getInternalSize(); + const bool col_is_internal = static_cast(col) < this->getInternalSize(); return (row_is_internal && col_is_internal); }; From bc769a9d19646257ed674d45dd88c5637e99eb7a Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Mon, 12 Oct 2026 13:11:10 +0000 Subject: [PATCH 17/31] Add BusLinePartitionInterface --- .../BusLinePartitionInterface.cpp | 237 ++++++++++++++++++ .../BusLinePartitionInterface.hpp | 113 +++++++++ 2 files changed, 350 insertions(+) create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp create mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp new file mode 100644 index 0000000000..4e46fb7fc3 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp @@ -0,0 +1,237 @@ + +#include "BusLinePartitionInterface.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + + namespace PowerElectronics + { + + /** + * @brief Construct a partition interface between a bus and a line. + * + * The interface consists of four external connections: two bus variables + * and two line current variables. During partitioning, these connections + * are mapped to the appropriate subsystem storage. + * + * The line currents contribute to the bus residual equations, with signs + * determined by the orientation of the line relative to the bus. + * + * @param bus Bus associated with the partition interface. + * @param component Line component across the partition boundary. + * @param id Unique identifier for the partition interface. + */ + template + BusLinePartitionInterface::BusLinePartitionInterface(node_type* bus, component_type* component, IdxT id) + : component_(component->clone()), + bus_(bus) + { + if (dynamic_cast*>(bus) == nullptr) + { + throw std::invalid_argument("BusLinePartitionInterface requires a MicrogridBus."); + } + + if (dynamic_cast*>(component) == nullptr) + { + throw std::invalid_argument("BusLinePartitionInterface requires a MicrogridLine."); + } + + size_ = 4; + n_intern_ = 0; + n_extern_ = 4; + idc_ = id; + extern_indices_ = {0, 1, 2, 3}; + nnz_ = 2; + } + + template + BusLinePartitionInterface::~BusLinePartitionInterface() + { + } + + template + int BusLinePartitionInterface::allocate() + { + + CircuitComponent::allocate(); + + // Connection indices associated with the two ends of the line. + auto comp_bus1_idx_i = component_->getNodeConnection(1); + auto comp_bus1_idx_j = component_->getNodeConnection(2); + auto comp_bus2_idx_i = component_->getNodeConnection(3); + auto comp_bus2_idx_j = component_->getNodeConnection(4); + + // Global indices associated with the local bus. + auto bus_idx_i = bus_->getNodeConnection(0).idx_; + auto bus_idx_j = bus_->getNodeConnection(1).idx_; + + // Determine whether the local bus is connected to the left or right end of the line. + bool is_left_connection = (comp_bus1_idx_i == bus_idx_i) && (comp_bus1_idx_j == bus_idx_j); + bool is_right_connection = (comp_bus2_idx_i == bus_idx_i) && (comp_bus2_idx_j == bus_idx_j); + + // Neither end or both ends matched -> invalid interface. Exactly one end should match. + if (is_left_connection == is_right_connection) + { + GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " + << "Bus(ID=" << bus_->busID() + << "), Component(ID=" << component_->getIDcomponent() + << "). Malformed topology or no partitionable connection was found." + << std::endl; + + return 1; + } + + if (is_left_connection) + { + // Use the connection indices associated with the first end of the line. + this->setConnectionNodes(0, component_->getNodeConnection(1)); + this->setConnectionNodes(1, component_->getNodeConnection(2)); + } + else + { + // Use the connection indices associated with the second end of the line. + this->setConnectionNodes(0, component_->getNodeConnection(3)); + this->setConnectionNodes(1, component_->getNodeConnection(4)); + } + + // Map the line current variables that provide the current injection to the bus. + this->setConnectionNodes(2, component_->getNodeConnection(5)); + this->setConnectionNodes(3, component_->getNodeConnection(6)); + + // Store the partition orientation for residual and Jacobian evaluation. + is_left_of_Line_ = is_left_connection; + + return 0; + } + + template + int BusLinePartitionInterface::setAbsoluteTolerance(RealT abs_tol) + { + abs_tol_.setToConst(static_cast(abs_tol)); + return 0; + } + + /** + * @brief Initialize the partition interface. + * + * @return 0 on success. + */ + template + int BusLinePartitionInterface::initialize() + { + return 0; + } + + /** + * @brief Identify differential variables + */ + template + int BusLinePartitionInterface::tagDifferentiable() + { + return 0; + } + + /** + * @brief Evaluates the internal residual. + * + * All variables are external from the interface's point of view, so there + * are no internal residual contributions to evaluate. + * + * @return 0 on success. + */ + template + int BusLinePartitionInterface::evaluateInternalResidual() + { + return 0; + } + + /** + * @brief Evaluate the line contribution to the external bus residual. + * + * Adds the two line current residual contributions to the corresponding + * bus residual equations. The sign of each contribution is determined by + * whether the bus is connected to the left or right end of the line, + * consistent with the line current orientation. + * + * @return 0 on success. + */ + template + int BusLinePartitionInterface::evaluateExternalResidual() + { + + const int sign = is_left_of_Line_ ? -1 : 1; + + *f_ext_[0] += sign * *y_ext_[2]; + *f_ext_[1] += sign * *y_ext_[3]; + + return 0; + } + + /** + * @brief Evaluate the Jacobian contributions associated with the bus. + * + * Evaluates the Jacobian of the entries whose residual rows correspond + * to variables belonging to the connected bus. + * + * @note Some of these entries may not be required to form the Jacobian of the + * subsystem, but may be useful for parallel Jacobian assembly. + * + * @return 0 on success. + */ + template + int BusLinePartitionInterface::evaluateJacobian() + { + this->zeroJacMatrix(); + + const int sign = is_left_of_Line_ ? -1 : 1; + + // Jacobian entries corresponding to the two transferred line contributions. + std::vector rtemp{0, 1}; + std::vector ctemp{2, 3}; + std::vector valtemp{sign * 1.0, sign * 1.0}; + + this->setJacValues(rtemp, ctemp, valtemp); + + return 0; + } + + template + int BusLinePartitionInterface::evaluateIntegrand() + { + return 0; + } + + template + int BusLinePartitionInterface::initializeAdjoint() + { + return 0; + } + + template + int BusLinePartitionInterface::evaluateAdjointResidual() + { + return 0; + } + + template + int BusLinePartitionInterface::evaluateAdjointIntegrand() + { + return 0; + } + + // Available template instantiations + template class BusLinePartitionInterface; + template class BusLinePartitionInterface; + template class BusLinePartitionInterface; + template class BusLinePartitionInterface; + } // namespace PowerElectronics +} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp new file mode 100644 index 0000000000..1909c1c881 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp @@ -0,0 +1,113 @@ +/** + * @file BusLinePartitionInterface.hpp + * @author Abdourahman Barry (abdourahman@vt.edu) + * + */ + +#pragma once + +#include + +#include +#include +#include + +namespace GridKit +{ + namespace PowerElectronics + { + /** + * @brief Partition interface between a bus and a line. + * + * The BusLinePartitionInterface provides a mechanism for a partition to access + * the current injection it requires from a line located in a neighboring + * partition, without evaluating the line within the local partition. + * + * The interface tracks four variables associated with the bus-line connection. + * The first two variables belong to the bus participating in the partition + * boundary, while the last two belong to the line connected to that bus. All + * four variables are treated as external variables by the interface. + * + * During partitioning, the interface is added to the partition containing the + * bus. Since the first two interface variables belong to components within the + * partition, the subsystem maps them to its local variables. The last two + * variables belong to the line in the neighboring partition and are therefore + * identified by the subsystem as external variables. + * + * During subsystem evaluation, the interface uses these external line variables + * to provide the corresponding current injection to the bus equations. The + * current contribution is added to or subtracted from the bus residual according + * to which end of the line is connected to the bus. + * + * @tparam ScalarT Scalar type used for state and residual values. + * @tparam IdxT Index type used for component and variable indices. + */ + template + class BusLinePartitionInterface : public PartitionInterface + { + + using component_type = CircuitComponent; + using node_type = typename PowerElectronics::NodeBase; + using RealT = typename CircuitComponent::RealT; + + using CircuitComponent::size_; + using CircuitComponent::nnz_; + using CircuitComponent::time_; + using CircuitComponent::alpha_; + using CircuitComponent::y_ext_; + using CircuitComponent::y_int_; + using CircuitComponent::yp_ext_; + using CircuitComponent::yp_int_; + using CircuitComponent::tag_; + using CircuitComponent::abs_tol_; + using CircuitComponent::f_ext_; + using CircuitComponent::f_int_; + using CircuitComponent::g_; + using CircuitComponent::yB_; + using CircuitComponent::ypB_; + using CircuitComponent::fB_; + using CircuitComponent::gB_; + using CircuitComponent::param_; + using CircuitComponent::idc_; + + using CircuitComponent::extern_indices_; + using CircuitComponent::n_extern_; + using CircuitComponent::n_intern_; + using CircuitComponent::connection_nodes_; + + public: + BusLinePartitionInterface(node_type* bus, component_type* component, IdxT id); + virtual ~BusLinePartitionInterface(); + + int allocate() final; + int initialize() final; + int tagDifferentiable() final; + int setAbsoluteTolerance(RealT) final; + int evaluateInternalResidual() final; + int evaluateExternalResidual() final; + int evaluateJacobian() final; + int evaluateIntegrand() final; + + int initializeAdjoint() final; + int evaluateAdjointResidual() final; + // int evaluateAdjointJacobian(); + int evaluateAdjointIntegrand() final; + + private: + /** + * @brief Owned copy of the line component across the partition boundary. + */ + std::unique_ptr component_; + + /** + * @brief Non-owning pointer to the bus within the local partition. + */ + node_type* bus_; + + /** + * @brief True if the bus is connected to the left end of the line. + */ + bool is_left_of_Line_; + }; + } // namespace PowerElectronics +} // namespace GridKit From 88043c46de1ad351b37b61143027c16d0d837557 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Mon, 12 Oct 2026 13:11:43 +0000 Subject: [PATCH 18/31] Update CMake --- .../PartitionInterface/CMakeLists.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt index 9f013ad673..f14aa7b555 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -7,3 +7,14 @@ gridkit_add_library( GridKit::dense_vector PUBLIC GridKit::utilities_logger) + +gridkit_add_library( + power_elec_bus_line_interface + SOURCES BusLinePartitionInterface.cpp + HEADERS BusLinePartitionInterface.hpp + LINK_LIBRARIES + PUBLIC + GridKit::dense_vector + GridKit::utilities_logger + GridKit::power_elec_microline +) From ccfaa9f2773b13f6c78df1f881d04caa548cd2ee Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 02:58:04 +0000 Subject: [PATCH 19/31] Forward time updates to enclosed component in BusPartitionInterface --- .../PartitionInterface/BusPartitionInterface.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp index c84e35855a..533b918ce4 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp @@ -311,6 +311,8 @@ namespace GridKit size_t internal_index = 0; size_t external_index = 0; + component_->updateTime(time_, alpha_); + // Reconstruct the state expected by the wrapped component from the // interface's external data. Route the external residual to component_f_ext_. for (size_t i = 0; i < static_cast(component_->size()); ++i) From 3de0eba1a4e253d2555c8d6bcda0ba9aab68d111 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 02:58:50 +0000 Subject: [PATCH 20/31] Update SubsystemModel --- .../Model/PowerElectronics/SubsystemModel.hpp | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 27c1c635c9..82b0ea527e 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -54,13 +54,7 @@ namespace GridKit class SubsystemModel : public PowerElectronicsModel { public: - struct ForcingData - { - std::vector y; - std::vector yp; - }; - - using TimeFunction = std::function; + using TimeFunction = std::function; protected: using SystemModel = PowerElectronicsModel; @@ -153,7 +147,10 @@ namespace GridKit { if (this->isAllocated()) { - restoreGlobalConnection(); + if (int err_code = restoreGlobalConnection()) + { + return err_code; + } } if (int err_code = buildConnectionMap()) @@ -172,12 +169,7 @@ namespace GridKit // Contains internal and external connection indicies connection_nodes_ = std::make_unique(n_intern_ + n_extern_); - if (!this->isAllocated()) - { - // Allocate internal subsystem vectors. - allocateVectors(static_cast(size_), true); - abs_tol_.setToZero(memory::HOST); - } + allocateVectors(static_cast(size_), true); // Allocate internal subsystem vectors. tag_.resize(size_); @@ -392,14 +384,18 @@ namespace GridKit /** * @brief Update the subsystem external state and derivative data. * - * If a forcing function is provided, evaluate it at the current subsystem - * time and copy the returned coupling values into the external state vectors. + * If a forcing function is provided, invoke it at the current subsystem + * time to update the external coupling variables. * - * @post y_ext_data_ and yp_ext_data_ contain the external coupling values - * returned by the forcing function, if one is set. + * @note The user can supply external coupling data in two ways: + * - Through a forcing function that updates getExternalDataY() + * and getExternalDataYP() before each evaluation. This is useful + * for time-dependent or streaming external data. + * - By directly updating these vectors when no forcing function + * is provided, as in zero-order hold (ZOH) coupling. * - * @throws std::runtime_error If the forcing function returns vectors whose - * sizes do not match the subsystem external-data vectors. + * @post If a forcing function is set, y_ext_data_ and yp_ext_data_ + * contain the external coupling values supplied by the function. * * @return 0 on success. */ @@ -408,17 +404,10 @@ namespace GridKit if (forcing_function_) { - const auto forcing = (*forcing_function_)(time_); - - if (forcing.y.size() != y_ext_data_.getSize() || forcing.yp.size() != yp_ext_data_.getSize()) - { - throw std::runtime_error( - "SubsystemModel::distributeExternalVectors: forcing function " - "returned vectors with incorrect sizes."); - } + (*forcing_function_)(time_); - std::copy(forcing.y.begin(), forcing.y.end(), y_ext_data_.getData()); - std::copy(forcing.yp.begin(), forcing.yp.end(), yp_ext_data_.getData()); + y_ext_data_.setDataUpdated(); + yp_ext_data_.setDataUpdated(); } return 0; @@ -468,10 +457,14 @@ namespace GridKit */ void addComponent(component_type* component) { + if (this->isAllocated()) + { + throw std::logic_error("SubsystemModel::addComponent: cannot add a component to an allocated subsystem."); + } + if (!component->isAllocated()) { - throw std::logic_error( - "SubsystemModel::addComponent: cannot add an unallocated component."); + throw std::logic_error("SubsystemModel::addComponent: cannot add an unallocated component."); } SystemModel::addComponent(component); @@ -488,6 +481,11 @@ namespace GridKit */ void addNode(node_type* node) { + if (this->isAllocated()) + { + throw std::logic_error("SubsystemModel::addNode: cannot add a node to an allocated subsystem."); + } + if (!node->isAllocated()) { throw std::logic_error( @@ -740,9 +738,9 @@ namespace GridKit VectorT f_ext_data_; /** - * @brief Optional forcing function used to provide external subsystem data. + * @brief Optional time-dependent callback for updating external coupling data. * - * It is continuous function that can be sampled at different times + * The callback is invoked before residual and Jacobian evaluations. */ std::optional forcing_function_; From 768622fd27bd7e5f83ccd9e671a50c24f037f4cd Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:07:17 +0000 Subject: [PATCH 21/31] Remove cloned component and move connection index initialization to constructor --- .../BusLinePartitionInterface.cpp | 93 +++++++------------ .../BusLinePartitionInterface.hpp | 20 ++-- 2 files changed, 40 insertions(+), 73 deletions(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp index 4e46fb7fc3..6565aecd8e 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp @@ -1,14 +1,12 @@ #include "BusLinePartitionInterface.hpp" -#include -#include #include +#include #include #include #include -#include namespace GridKit { @@ -26,14 +24,14 @@ namespace GridKit * The line currents contribute to the bus residual equations, with signs * determined by the orientation of the line relative to the bus. * + * @pre The bus and line connection indices have been allocated. + * * @param bus Bus associated with the partition interface. * @param component Line component across the partition boundary. * @param id Unique identifier for the partition interface. */ template BusLinePartitionInterface::BusLinePartitionInterface(node_type* bus, component_type* component, IdxT id) - : component_(component->clone()), - bus_(bus) { if (dynamic_cast*>(bus) == nullptr) { @@ -51,64 +49,42 @@ namespace GridKit idc_ = id; extern_indices_ = {0, 1, 2, 3}; nnz_ = 2; - } - - template - BusLinePartitionInterface::~BusLinePartitionInterface() - { - } - - template - int BusLinePartitionInterface::allocate() - { - - CircuitComponent::allocate(); - - // Connection indices associated with the two ends of the line. - auto comp_bus1_idx_i = component_->getNodeConnection(1); - auto comp_bus1_idx_j = component_->getNodeConnection(2); - auto comp_bus2_idx_i = component_->getNodeConnection(3); - auto comp_bus2_idx_j = component_->getNodeConnection(4); // Global indices associated with the local bus. - auto bus_idx_i = bus_->getNodeConnection(0).idx_; - auto bus_idx_j = bus_->getNodeConnection(1).idx_; + auto bus_idx_i = bus->getNodeConnection(0).idx_; + auto bus_idx_j = bus->getNodeConnection(1).idx_; - // Determine whether the local bus is connected to the left or right end of the line. - bool is_left_connection = (comp_bus1_idx_i == bus_idx_i) && (comp_bus1_idx_j == bus_idx_j); - bool is_right_connection = (comp_bus2_idx_i == bus_idx_i) && (comp_bus2_idx_j == bus_idx_j); + const bool is_left_connection = bus_idx_i == component->getNodeConnection(1) && bus_idx_j == component->getNodeConnection(2); + const bool is_right_connection = bus_idx_i == component->getNodeConnection(3) && bus_idx_j == component->getNodeConnection(4); - // Neither end or both ends matched -> invalid interface. Exactly one end should match. if (is_left_connection == is_right_connection) { - GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " - << "Bus(ID=" << bus_->busID() - << "), Component(ID=" << component_->getIDcomponent() - << "). Malformed topology or no partitionable connection was found." - << std::endl; - - return 1; + throw std::invalid_argument( + std::format( + "Invalid partition interface detected. Bus(ID={}), Component(ID={}). " + "Malformed topology or no partitionable connection was found.", + bus->busID(), + component->getIDcomponent())); } - if (is_left_connection) - { - // Use the connection indices associated with the first end of the line. - this->setConnectionNodes(0, component_->getNodeConnection(1)); - this->setConnectionNodes(1, component_->getNodeConnection(2)); - } - else - { - // Use the connection indices associated with the second end of the line. - this->setConnectionNodes(0, component_->getNodeConnection(3)); - this->setConnectionNodes(1, component_->getNodeConnection(4)); - } + sign_ = is_left_connection ? -1.0 : 1.0; + connection_indices_.resize(static_cast(size_)); - // Map the line current variables that provide the current injection to the bus. - this->setConnectionNodes(2, component_->getNodeConnection(5)); - this->setConnectionNodes(3, component_->getNodeConnection(6)); + connection_indices_[0] = component->getNodeConnection(is_left_connection ? 1 : 3); + connection_indices_[1] = component->getNodeConnection(is_left_connection ? 2 : 4); + connection_indices_[2] = component->getNodeConnection(5); + connection_indices_[3] = component->getNodeConnection(6); + } - // Store the partition orientation for residual and Jacobian evaluation. - is_left_of_Line_ = is_left_connection; + template + int BusLinePartitionInterface::allocate() + { + CircuitComponent::allocate(); + + this->setConnectionNodes(0, connection_indices_[0]); + this->setConnectionNodes(1, connection_indices_[1]); + this->setConnectionNodes(2, connection_indices_[2]); + this->setConnectionNodes(3, connection_indices_[3]); return 0; } @@ -167,11 +143,8 @@ namespace GridKit template int BusLinePartitionInterface::evaluateExternalResidual() { - - const int sign = is_left_of_Line_ ? -1 : 1; - - *f_ext_[0] += sign * *y_ext_[2]; - *f_ext_[1] += sign * *y_ext_[3]; + *f_ext_[0] += sign_ * *y_ext_[2]; + *f_ext_[1] += sign_ * *y_ext_[3]; return 0; } @@ -192,12 +165,10 @@ namespace GridKit { this->zeroJacMatrix(); - const int sign = is_left_of_Line_ ? -1 : 1; - // Jacobian entries corresponding to the two transferred line contributions. std::vector rtemp{0, 1}; std::vector ctemp{2, 3}; - std::vector valtemp{sign * 1.0, sign * 1.0}; + std::vector valtemp{sign_, sign_}; this->setJacValues(rtemp, ctemp, valtemp); diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp index 1909c1c881..c151b1a207 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp @@ -6,8 +6,6 @@ #pragma once -#include - #include #include #include @@ -77,7 +75,7 @@ namespace GridKit public: BusLinePartitionInterface(node_type* bus, component_type* component, IdxT id); - virtual ~BusLinePartitionInterface(); + ~BusLinePartitionInterface() override = default; int allocate() final; int initialize() final; @@ -95,19 +93,17 @@ namespace GridKit private: /** - * @brief Owned copy of the line component across the partition boundary. - */ - std::unique_ptr component_; - - /** - * @brief Non-owning pointer to the bus within the local partition. + * @brief Global connection indices for the bus voltages and line currents + * across the partition boundary. */ - node_type* bus_; + std::vector connection_indices_; /** - * @brief True if the bus is connected to the left end of the line. + * @brief Sign of the line current contribution to the bus residual. + * + * -1 for the left end of the line and +1 for the right end. */ - bool is_left_of_Line_; + RealT sign_; }; } // namespace PowerElectronics } // namespace GridKit From b023eb37518f25f0713b15dada45e0d1bc3c02e9 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:11:29 +0000 Subject: [PATCH 22/31] Remove Hires Test problem --- .../SubsystemModelWithHiresTest.hpp | 766 ------------------ .../runSubsystemModelWithHiresTest.cpp | 12 - 2 files changed, 778 deletions(-) delete mode 100644 tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp delete mode 100644 tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp diff --git a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp b/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp deleted file mode 100644 index 927d7c948f..0000000000 --- a/tests/UnitTests/PowerElectronics/SubsystemModelWithHiresTest.hpp +++ /dev/null @@ -1,766 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace GridKit -{ - - /*! - * @brief Hires Component 1 class. - * - */ - template - class HiresComponent1 : public CircuitComponent - { - using RealT = typename CircuitComponent::RealT; - using NodeT = typename PowerElectronics::NodeBase; - - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::idc_; - - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - - public: - HiresComponent1(NodeT* bus, IdxT id) - : node_ref_(bus) - { - size_ = 5; - n_intern_ = 3; - n_extern_ = 2; - extern_indices_ = {0, 1}; - idc_ = id; - nnz_ = 12; - } - - ~HiresComponent1() - { - } - - int allocate() final - { - CircuitComponent::allocate(); - - this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); - this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); - - return 0; - } - - int initialize() final - { - return 0; - } - - int tagDifferentiable() final - { - return 0; - } - - int evaluateInternalResidual() final - { - // Internals - f_int_[0] = -yp_int_[0] - 1.71 * y_int_[0] + 0.43 * y_int_[1] + 8.32 * y_int_[2] + 0.0007; - f_int_[1] = -yp_int_[1] + 1.71 * y_int_[0] - 8.75 * y_int_[1]; - f_int_[2] = -yp_int_[2] - 10.03 * y_int_[2] + 0.43 * *y_ext_[0] + 0.035 * *y_ext_[1]; - - return 0; - } - - int evaluateExternalResidual() - { - // outputs - *f_ext_[0] += 8.32 * y_int_[1] + 1.71 * y_int_[2] - 0.1 * *y_ext_[0]; - *f_ext_[1] += -0.7 * *y_ext_[1]; - - return 0; - } - - int evaluateJacobian() final - { - - this->zeroJacMatrix(); - - // Internal Jacobian Entries - std::vector row = {2, 2, 2, 3, 3, 4, 4, 4}; - std::vector col = {2, 3, 4, 2, 3, 4, 0, 1}; - std::vector val = {-1.71 - alpha_, 0.43, 8.32, 1.71, -8.75 - alpha_, -10.03 - alpha_, 0.43, 0.035}; - - this->setJacValues(row, col, val); - - // External Jacobian Entries - row = {0, 0, 0, 1}; - col = {3, 4, 0, 1}; - val = {8.32, 1.71, -0.1, -0.7}; - - this->setJacValues(row, col, val); - - return 0; - } - - int evaluateIntegrand() final - { - return 0; - } - - int initializeAdjoint() final - { - return 0; - } - - int evaluateAdjointResidual() final - { - return 0; - } - - int evaluateAdjointIntegrand() final - { - return 0; - } - - /** - * @brief Compute the absolute tolerance for each variable in the model - */ - int setAbsoluteTolerance(RealT rel_tol) final - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - CircuitComponent* clone() const - { - return new HiresComponent1(*this); - } - - private: - NodeT* node_ref_; - }; - - /*! - * @brief Hires Bus Component (Component 2). - * - */ - template - class HiresBus : public CircuitComponent - { - - using RealT = typename CircuitComponent::RealT; - using NodeT = typename PowerElectronics::NodeBase; - - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::idc_; - - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - - public: - HiresBus(NodeT* bus, IdxT id) - : node_ref_(bus) - { - size_ = 2; - n_intern_ = 0; - n_extern_ = 2; - extern_indices_ = {0, 1}; - idc_ = id; - nnz_ = 2; - } - - ~HiresBus() - { - } - - int allocate() final - { - CircuitComponent::allocate(); - - this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); - this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); - - return 0; - } - - int initialize() final - { - return 0; - } - - int tagDifferentiable() final - { - return 0; - } - - int evaluateInternalResidual() final - { - return 0; - } - - int evaluateExternalResidual() final - { - *f_ext_[0] += -*yp_ext_[0] - *y_ext_[0]; - *f_ext_[1] += -*yp_ext_[1] - *y_ext_[1]; - - return 0; - } - - int evaluateJacobian() final - { - this->zeroJacMatrix(); - - std::vector row = {0, 1}; - std::vector col = {0, 1}; - std::vector val = {-alpha_ - 1.0, -alpha_ - 1.0}; - - this->setJacValues(row, col, val); - - return 0; - } - - int evaluateIntegrand() final - { - return 0; - } - - int initializeAdjoint() final - { - return 0; - } - - int evaluateAdjointResidual() final - { - return 0; - } - - int evaluateAdjointIntegrand() final - { - return 0; - } - - /** - * @brief Compute the absolute tolerance for each variable in the model - */ - int setAbsoluteTolerance(RealT rel_tol) final - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - CircuitComponent* clone() const - { - return new HiresBus(*this); - } - - private: - NodeT* node_ref_; - }; - - /*! - * @brief Hires Component 3 class. - * - */ - template - class HiresComponent3 : public CircuitComponent - { - using RealT = typename CircuitComponent::RealT; - using NodeT = typename PowerElectronics::NodeBase; - - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::idc_; - - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - - public: - HiresComponent3(NodeT* bus, IdxT id) - : node_ref_(bus) - { - size_ = 5; - n_intern_ = 3; - n_extern_ = 2; - extern_indices_ = {0, 1}; - idc_ = id; - nnz_ = 15; - } - - ~HiresComponent3() - { - } - - int allocate() - { - CircuitComponent::allocate(); - - this->setExternalConnectionNodes(0, node_ref_->getNodeConnection(0)); - this->setExternalConnectionNodes(1, node_ref_->getNodeConnection(1)); - - return 0; - } - - int initialize() - { - return 0; - } - - int tagDifferentiable() - { - return 0; - } - - int evaluateInternalResidual() - { - - // Internals - f_int_[0] = -yp_int_[0] - 280 * y_int_[0] * y_int_[2] + 0.69 * *y_ext_[0] + 1.71 * *y_ext_[1] - 0.43 * y_int_[0] + 0.69 * y_int_[1]; - f_int_[1] = -yp_int_[1] + 280 * y_int_[0] * y_int_[2] - 1.81 * y_int_[1]; - f_int_[2] = -yp_int_[2] - 280 * y_int_[0] * y_int_[2] + 1.81 * y_int_[1]; - - return 0; - } - - int evaluateExternalResidual() - { - // Externals - *f_ext_[0] += -0.02 * *y_ext_[0]; - *f_ext_[1] += -0.045 * *y_ext_[1] + 0.43 * y_int_[0] + 0.43 * y_int_[1]; - - return 0; - } - - int evaluateJacobian() - { - this->zeroJacMatrix(); - - // Internal Jacobian Entries [row 1] - std::vector row = {2, 2, 2, 2, 2}; - std::vector col = {2, 3, 4, 0, 1}; - std::vector val = {-280 * y_int_[2] - 0.43 - alpha_, 0.69, -280 * y_int_[0], 0.69, 1.71}; - - this->setJacValues(row, col, val); - - // Internal Jacobian Entries [row 2] - row = {3, 3, 3}; - col = {2, 3, 4}; - val = {280 * y_int_[2], -1.81 - alpha_, 280 * y_int_[0]}; - - this->setJacValues(row, col, val); - - // Internal Jacobian Entries [row 3] - row = {4, 4, 4}; - col = {2, 3, 4}; - val = {-280 * y_int_[2], 1.81, -280 * y_int_[0] - alpha_}; - - this->setJacValues(row, col, val); - - // External Jacobian Entries - row = {0, 1, 1, 1}; - col = {0, 2, 3, 1}; - val = {-0.02, 0.43, 0.43, -0.045}; - - this->setJacValues(row, col, val); - - return 0; - } - - int evaluateIntegrand() - { - return 0; - } - - int initializeAdjoint() - { - return 0; - } - - int evaluateAdjointResidual() - { - return 0; - } - - int evaluateAdjointIntegrand() - { - return 0; - } - - /** - * @brief Compute the absolute tolerance for each variable in the model - * - */ - int setAbsoluteTolerance(RealT rel_tol) - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - CircuitComponent* clone() const - { - return new HiresComponent3(*this); - } - - private: - NodeT* node_ref_; - }; - - namespace Testing - { - - /** - * This example assembles the HIRES problem and partitions it to demonstrate - * that the partitioned system can reconstruct the full system. - * - * The HIRES problem consists of eight ODE equations and is divided into three - * components. HiresComponent1 contains equations 1, 2, and 3, - * HiresComponent3 contains equations 6, 7, and 8, and HiresBus contains - * equations 4 and 5. HiresBus plays a role similar to MicrogridBusDQ, where - * other components connected to the bus contribute terms to its equations. - * - * In the equations below, terms enclosed in parentheses ( ) are contributions - * from HiresComponent1, terms enclosed in square brackets [ ] are contributions - * from HiresComponent3, and the remaining terms in equations 4 and 5 belong - * to HiresBus. - *\f[ - * f_1 = dy_1/dt + 1.71y_1 - 0.43y_2 - 8.32y_3 - 0.0007 - * - * f_2 = dy_2/dt - 1.71y_1 + 8.75y_2 - * - * f_3 = dy_3/dt + 10.03y_3 - 0.43y_4 - 0.035y_5 - * - * f_4 = dy_4/dt + y_4 + (0.1y_4 - 8.32y_2 - 1.71y_3) + [0.02y_4] - * - * f_5 = dy_5/dt + y_5 + \left(0.7y_5 \right) + \left[0.045y_5 - 0.43y_6 - 0.43y_7 \right] - * - * f_6 = dy_6/dt - 280y_6y_8 + 0.69y_4 + 1.71y_5 - 0.43y_6 + 0.69y_7 - * - * f_7 = dy_7/dt + 280y_6y_8 - 1.81y_7 - * - * f_8 = dy_8/dt - 280y_6y_8 + 1.81y_7 - *\f] - * - * The assembled system has the following structure: - * - * (HiresComponent1) -------- (HiresBus) -------- (HiresComponent3) - * - * The system is partitioned between HiresBus and HiresComponent3. A bus - * partition interface is introduced in the partition containing HiresBus to - * preserve the contribution of HiresComponent3 to the bus equations. The - * residuals evaluated independently by the partitions are then printed with - * the residual of the full system for eye-ball comparision. - * - * This example will also be useful later for testing the order of accuracy of co-simulation methods. - */ - template - class SubsystemModelWithHiresTests - { - using RealT = typename CircuitComponent::RealT; - using Bus = PowerElectronics::MicrogridBus; - using Subsystem = SubsystemModel; - using System = PowerElectronicsModel; - - public: - /** - * @brief Construct the HIRES reference system and subsystem partitions. - */ - SubsystemModelWithHiresTests() - : system_(new System()), - partition1_(new Subsystem()), - partition2_(new Subsystem()), - comp1_(new HiresComponent1(&bus_, 1)), - bus1_(new HiresBus(&bus_, 2)), - comp3_(new HiresComponent3(&bus_, 3)) - { - - y_ = {1, 2, 3, 4, 5, 6, 7, 8}; - yp_ = {1, 2, 3, 4, 5, 6, 7, 8}; - // --------------------------------------------------------------------- - // Assemble and allocate the monolithic reference system - // --------------------------------------------------------------------- - - system_->addComponent(comp1_); - system_->addComponent(comp3_); - system_->addComponent(bus1_); - system_->addNode(&bus_); - - system_->allocate(); - - auto* system_y = system_->y().getData(); - auto* system_yp = system_->yp().getData(); - - for (size_t i = 0; i < system_->size(); ++i) - { - system_y[i] = y_[i]; - system_yp[i] = yp_[i]; - } - - system_->y().setDataUpdated(); - system_->yp().setDataUpdated(); - - system_->updateTime(1.0, 2.0); - system_->evaluateResidual(); - - // --------------------------------------------------------------------- - // Construct the partition interface - // --------------------------------------------------------------------- - - bus_interface_ = new PowerElectronics::BusPartitionInterface(&bus_, comp3_, 4); - - bus_interface_->allocate(); - - // --------------------------------------------------------------------- - // Assemble the subsystem partitions - // --------------------------------------------------------------------- - - partition1_->addComponent(comp1_); - partition1_->addComponent(bus1_); - partition1_->addInterface(bus_interface_); - partition1_->addNode(&bus_); - - partition2_->addComponent(comp3_); - - partition1_->allocate(); - partition2_->allocate(); - partitions_ = {partition1_, partition2_}; - - // Distribute variables to all partitions - distributeVariables(y_, yp_); - } - - ~SubsystemModelWithHiresTests() - { - delete partition1_; - delete partition2_; - delete system_; - } - - template - void distributeVariables(const T& y, T& yp) - { - for (auto* partition : partitions_) - { - - // Test the forcing function mechanism. - auto forcing_function = [partition, &y, &yp]([[maybe_unused]] ScalarT t) - { - typename Subsystem::ForcingData forcing_data; - - forcing_data.y.resize(partition->getExternSize()); - forcing_data.yp.resize(partition->getExternSize()); - - for (size_t i = 0; i < partition->getExternSize(); ++i) - { - const auto global_index = partition->getExternalDataIndices()[i]; - - forcing_data.y[i] = y[global_index]; - forcing_data.yp[i] = yp[global_index]; - } - - return forcing_data; - }; - - partition->setForcingFunction(std::move(forcing_function)); - - auto* partition_y = partition->y().getData(); - auto* partition_yp = partition->yp().getData(); - - for (size_t i = 0; i < partition->getInternalSize(); ++i) - { - const auto global_index = partition->getNodeConnection(i); - - partition_y[i] = y[global_index]; - partition_yp[i] = yp[global_index]; - } - - partition->y().setDataUpdated(); - partition->yp().setDataUpdated(); - } - } - - /** - * @brief Verify that the partitioned HIRES residual matches the - * monolithic residual. - */ - TestOutcome residual() - { - TestStatus success = true; - - std::vector partition_residual(system_->size(), 0.0); - - for (auto* partition : partitions_) - { - partition->updateTime(1.0, 2.0); - partition->evaluateResidual(); - - auto* residual = partition->getResidual().getData(); - - for (size_t i = 0; i < partition->getInternalSize(); ++i) - { - partition_residual[partition->getNodeConnection(i)] = residual[i]; - } - - partition->getResidual().setDataUpdated(); - } - - auto* reference_residual = system_->getResidual().getData(); - - RealT tolerance = 100 * std::numeric_limits::epsilon(); - - for (size_t i = 0; i < system_->size(); ++i) - { - success *= Testing::isEqual(partition_residual[i], reference_residual[i], tolerance); - } - - return success.report(__func__); - } - - /** - * @brief Verify that each subsystem Jacobian matches the corresponding - * entries of the monolithic HIRES Jacobian. - */ - TestOutcome jacobian() - { - TestStatus success = true; - - RealT alpha = 2.0; - - /* - * GridKit stores the HIRES variables in component assembly order: - * - * System index: 0 1 2 3 4 5 6 7 - * HIRES index: 0 1 2 5 6 7 3 4 - * Variable: y1 y2 y3 y6 y7 y8 y4 y5 - */ - const std::array sysmodel_to_hires = { - 0, 1, 2, 5, 6, 7, 3, 4}; - - const std::array hires_to_sysmodel = { - 0, 1, 2, 6, 7, 3, 4, 5}; - - const RealT y5 = y_[hires_to_sysmodel[5]]; - const RealT y7 = y_[hires_to_sysmodel[7]]; - - std::array, 8> reference_jac = - {{{-alpha - 1.71, 0.43, 8.32, 0.0, 0.0, 0.0, 0.0, 0.0}, - {1.71, -alpha - 8.75, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {0.0, 0.0, -alpha - 10.03, 0.43, 0.035, 0.0, 0.0, 0.0}, - {0.0, 8.32, 1.71, -alpha - 1.12, 0.0, 0.0, 0.0, 0.0}, - {0.0, 0.0, 0.0, 0.0, -alpha - 1.745, 0.43, 0.43, 0.0}, - {0.0, 0.0, 0.0, 0.69, 1.71, -alpha - 280.0 * y7 - 0.43, 0.69, -280.0 * y5}, - {0.0, 0.0, 0.0, 0.0, 0.0, 280.0 * y7, -alpha - 1.81, 280.0 * y5}, - {0.0, 0.0, 0.0, 0.0, 0.0, -280.0 * y7, 1.81, -alpha - 280.0 * y5}}}; - - for (auto* partition : partitions_) - { - partition->updateTime(1.0, alpha); - partition->evaluateJacobian(); - - auto* partition_jac = partition->getCsrJacobian(); - - const auto jac_map = Testing::MapFromCsr(partition_jac); - const size_t partition_size = partition->getInternalSize(); - - // Compare with the corresponding entries of the full Jacobian. - for (size_t row = 0; row < partition_size; ++row) - { - const IdxT global_row = partition->getNodeConnection(row); - const IdxT ref_row = sysmodel_to_hires[global_row]; - - for (size_t col = 0; col < partition_size; ++col) - { - const IdxT global_col = partition->getNodeConnection(col); - const IdxT ref_col = sysmodel_to_hires[global_col]; - - const auto& jac_row = jac_map[row]; - const RealT jac_value = jac_row.contains(col) ? jac_row.at(col) : RealT{0}; - const RealT ref_jac_value = reference_jac[ref_row][ref_col]; - const RealT tolerance = 100 * std::numeric_limits::epsilon(); - - success *= Testing::isEqual(jac_value, ref_jac_value, tolerance); - } - } - } - - return success.report(__func__); - } - - private: - /// @brief system size. - static constexpr size_t SYSTEM_SIZE = 8; - - /// @brief Bus used to define the connection between the HIRES components. - Bus bus_; - - /// @brief Complete monolithic HIRES system. - System* system_; - - /// @brief First subsystem of the partitioned HIRES system. - Subsystem* partition1_; - - /// @brief Second subsystem of the partitioned HIRES system. - Subsystem* partition2_; - - /// @brief First component of the HIRES system. - HiresComponent1* comp1_; - - /// @brief Bus component connecting the two parts of the HIRES system. - HiresBus* bus1_; - - /// @brief Third component of the HIRES system. - HiresComponent3* comp3_; - - /// @brief Partition interface representing the bus connection across the subsystem boundary. - PowerElectronics::BusPartitionInterface* bus_interface_; - - /// @brief Collection of subsystems comprising the partitioned HIRES system. - std::vector partitions_; - - /// @brief State vector used to initialize and evaluate the HIRES system. - std::array y_; - - /// @brief State derivative vector used to initialize and evaluate the HIRES system. - std::array yp_; - }; - - } // namespace Testing - -} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp b/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp deleted file mode 100644 index 2b8ef11b08..0000000000 --- a/tests/UnitTests/PowerElectronics/runSubsystemModelWithHiresTest.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "SubsystemModelWithHiresTest.hpp" - -int main() -{ - GridKit::Testing::TestingResults result; - GridKit::Testing::SubsystemModelWithHiresTests test; - - result += test.residual(); - result += test.jacobian(); - - return result.summary(); -} From 39f879c98601a20242cf35bfb9181622dc5fbe9e Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:12:25 +0000 Subject: [PATCH 23/31] Add partition PowerElectronics Test problem --- .../PartitionPowerElectronicsTests.hpp | 397 ++++++++++++++++++ .../runPartitionPowerElectronicsTests.cpp | 16 + 2 files changed, 413 insertions(+) create mode 100644 tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp create mode 100644 tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp diff --git a/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp new file mode 100644 index 0000000000..fe320f8644 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp @@ -0,0 +1,397 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class PartitionPowerElectronicsTests + { + using RealT = typename CircuitComponent::RealT; + using System = PowerElectronicsModel; + using Subsystem = SubsystemModel; + + using Bus = PowerElectronics::MicrogridBus; + using Signal = PowerElectronics::SignalNode; + using Generator = DistributedGenerator; + using BusDQ = MicrogridBusDQ; + using Line = MicrogridLine; + using Load = MicrogridLoad; + + using Parameters = DistributedGeneratorParameters; + using Interface = PowerElectronics::BusLinePartitionInterface; + + public: + PartitionPowerElectronicsTests() + { + createComponents(); + assembleMonolithic(); + saveReferenceResidual(); + saveReferenceJacobian(); + assemblePartitions(); + distributeVariables(); + } + + /** + * @brief Verify sequential partition residuals against + * the saved monolithic reference. + */ + TestOutcome residual() + { + const auto partition_residual = evaluatePartitions(false); + + return compareResiduals(partition_residual, reference_residual_).report(__func__); + } + + /** + * @brief Verify that parallel partition evaluation produces + * the same result as sequential evaluation. + */ + TestOutcome parallelResidual() + { + const auto sequential = evaluatePartitions(false); + const auto parallel = evaluatePartitions(true); + + TestStatus success = true; + + success *= compareResiduals(parallel, sequential); + success *= compareResiduals(parallel, reference_residual_); + + return success.report(__func__); + } + + /** + * @brief Verify that the subsystem Jacobians match the corresponding + * blocks of the monolithic Jacobian. + */ + TestOutcome jacobian() + { + TestStatus success = true; + + const RealT tolerance = 100 * std::numeric_limits::epsilon(); + + for (auto* partition : partitions_) + { + partition->updateTime(1.0, 2.0); + partition->evaluateJacobian(); + + const auto jac_map = Testing::MapFromCsr(partition->getCsrJacobian()); + const auto partition_size = partition->getInternalSize(); + + for (size_t row = 0; row < partition_size; ++row) + { + const auto global_row = partition->getNodeConnection(row); + + for (size_t col = 0; col < partition_size; ++col) + { + const auto global_col = partition->getNodeConnection(col); + + const auto& partition_row = jac_map[row]; + const auto& reference_row = reference_jacobian_[global_row]; + + const RealT partition_value = partition_row.contains(col) ? partition_row.at(col) : RealT{0}; + const RealT reference_value = reference_row.contains(global_col) ? reference_row.at(global_col) : RealT{0}; + + success *= Testing::isEqual(partition_value, reference_value, tolerance); + } + } + } + + return success.report(__func__); + } + + private: + /** + * @brief Construct the physical microgrid components. + */ + void createComponents() + { + constexpr RealT pi = std::numbers::pi_v; + + Parameters parms{}; + + parms.wb_ = 2.0 * pi * 50.0; + parms.wc_ = 31.41; + parms.mp_ = 9.4e-5; + parms.Vn_ = 380.0; + parms.nq_ = 1.3e-3; + parms.F_ = 0.75; + parms.Kiv_ = 420.0; + parms.Kpv_ = 0.1; + parms.Kic_ = 2.0e4; + parms.Kpc_ = 15.0; + parms.Cf_ = 5.0e-5; + parms.rLf_ = 0.1; + parms.Lf_ = 1.35e-3; + parms.rLc_ = 0.03; + parms.Lc_ = 0.35e-3; + + dg1_ = new Generator(1, parms, true, &signal_, &bus1_); + dg2_ = new Generator(2, parms, false, &signal_, &bus2_); + + line_ = new Line(3, 0.23, 0.1 / (2.0 * pi * 50.0), &signal_, &bus1_, &bus2_); + + load1_ = new Load(4, 3.0, 2.0 / (2.0 * pi * 50.0), &signal_, &bus1_); + load2_ = new Load(5, 2.0, 1.0 / (2.0 * pi * 50.0), &signal_, &bus2_); + + busDQ1_ = new BusDQ(6, 1.0e4, &bus1_); + busDQ2_ = new BusDQ(7, 1.0e4, &bus2_); + } + + /** + * @brief Assemble the monolithic reference system. + */ + void assembleMonolithic() + { + system_ = std::make_unique(); + + system_->addComponent(dg1_); + system_->addComponent(load1_); + system_->addComponent(busDQ1_); + system_->addComponent(line_); + system_->addComponent(dg2_); + system_->addComponent(load2_); + system_->addComponent(busDQ2_); + + system_->addNode(&signal_); + system_->addNode(&bus1_); + system_->addNode(&bus2_); + + system_->allocate(); + + y_.resize(system_->size()); + yp_.resize(system_->size()); + + for (size_t i = 0; i < system_->size(); ++i) + { + const auto index = static_cast(i + 1); + + y_[i] = static_cast(0.01 * index); + yp_[i] = static_cast(0.001 * index); + } + } + + /** + * @brief Evaluate the monolithic residual exactly once, + * before constructing the partitions. + */ + void saveReferenceResidual() + { + auto* system_y = system_->y().getData(); + auto* system_yp = system_->yp().getData(); + + for (size_t i = 0; i < system_->size(); ++i) + { + system_y[i] = y_[i]; + system_yp[i] = yp_[i]; + } + + system_->y().setDataUpdated(); + system_->yp().setDataUpdated(); + + system_->updateTime(1.0, 2.0); + system_->evaluateResidual(); + + const auto* residual = system_->getResidual().getData(); + + reference_residual_.assign(residual, residual + system_->size()); + } + + /** + * @brief Save the monolithic Jacobian before partitioning. + */ + void saveReferenceJacobian() + { + system_->updateTime(1.0, 2.0); + system_->evaluateJacobian(); + + reference_jacobian_ = Testing::MapFromCsr(system_->getCsrJacobian()); + } + + /** + * @brief Assemble the two subsystem partitions. + */ + void assemblePartitions() + { + partition1_ = std::make_unique(); + partition2_ = std::make_unique(); + + // Boundary between the line and Bus2. + interface_ = new Interface(&bus2_, line_, 8); + interface_->allocate(); + + // Partition 1: DG1, Load1, Bus1, Line + partition1_->addComponent(dg1_); + partition1_->addComponent(load1_); + partition1_->addComponent(busDQ1_); + partition1_->addComponent(line_); + + partition1_->addNode(&signal_); + partition1_->addNode(&bus1_); + + // Partition 2: DG2, Load2, Bus2 + partition2_->addComponent(dg2_); + partition2_->addComponent(load2_); + partition2_->addComponent(busDQ2_); + partition2_->addInterface(interface_); + + partition2_->addNode(&bus2_); + + partition1_->allocate(); + partition2_->allocate(); + + partitions_ = {partition1_.get(), partition2_.get()}; + } + + /** + * @brief Distribute global states to the partitions. + */ + void distributeVariables() + { + for (auto* partition : partitions_) + { + auto forcing = [this, partition](ScalarT) -> void + { + auto* y_ext = partition->getExternalDataY().getData(); + auto* yp_ext = partition->getExternalDataYP().getData(); + + const auto& indices = partition->getExternalDataIndices(); + + for (size_t i = 0; i < partition->getExternSize(); ++i) + { + y_ext[i] = y_[indices[i]]; + yp_ext[i] = yp_[indices[i]]; + } + }; + + partition->setForcingFunction(std::move(forcing)); + + auto* y = partition->y().getData(); + auto* yp = partition->yp().getData(); + + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + const auto index = partition->getNodeConnection(i); + + y[i] = y_[index]; + yp[i] = yp_[index]; + } + + partition->y().setDataUpdated(); + partition->yp().setDataUpdated(); + } + } + + /** + * @brief Evaluate partitions and reconstruct the global residual. + * + * @param parallel Enable OpenMP parallel evaluation. + */ + std::vector evaluatePartitions([[maybe_unused]] bool parallel) + { + const int num_partitions = static_cast(partitions_.size()); + +#ifdef _OPENMP +#pragma omp parallel for if (parallel) schedule(static) +#endif + for (int p = 0; p < num_partitions; ++p) + { + auto* partition = partitions_[p]; + + partition->updateTime(1.0, 2.0); + partition->evaluateResidual(); + } + + std::vector residual(reference_residual_.size(), 0.0); + + for (auto* partition : partitions_) + { + const auto* f_values = partition->getResidual().getData(); + + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + const auto index = partition->getNodeConnection(i); + residual[index] = f_values[i]; + } + } + + return residual; + } + + /** + * @brief Compare two residual vectors. + */ + TestStatus compareResiduals( + const std::vector& actual, + const std::vector& expected) const + { + TestStatus success = true; + + const RealT tolerance = 100 * std::numeric_limits::epsilon(); + + for (size_t i = 0; i < expected.size(); ++i) + { + success *= Testing::isEqual(actual[i], expected[i], tolerance); + } + + return success; + } + + // nodes + Signal signal_; + Bus bus1_; + Bus bus2_; + + // components + Generator* dg1_ = nullptr; + Generator* dg2_ = nullptr; + Line* line_ = nullptr; + Load* load1_ = nullptr; + Load* load2_ = nullptr; + BusDQ* busDQ1_ = nullptr; + BusDQ* busDQ2_ = nullptr; + + // Partition interface + Interface* interface_ = nullptr; + + // Reference system + std::unique_ptr system_; + + // Partitioned system + std::unique_ptr partition1_; + std::unique_ptr partition2_; + std::vector partitions_; + + // Test data + std::vector y_; + std::vector yp_; + std::vector reference_residual_; + + std::vector reference_jacobian_; + }; + + } // namespace Testing + +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp new file mode 100644 index 0000000000..3e4faeb944 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp @@ -0,0 +1,16 @@ +#include + +#include "PartitionPowerElectronicsTests.hpp" +#include + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::Testing::PartitionPowerElectronicsTests test; + + result += test.residual(); + result += test.jacobian(); + result += test.parallelResidual(); + + return result.summary(); +} \ No newline at end of file From 73869f1ddd7606e39e537808389079e6a3134936 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:12:57 +0000 Subject: [PATCH 24/31] Update CMake file in tests --- .../UnitTests/PowerElectronics/CMakeLists.txt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index a55122514d..31e4e4727d 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -3,12 +3,18 @@ target_link_libraries( test_power_electronics_node PRIVATE GridKit::power_electronics_circuit_node GridKit::testing) -add_executable(test_subsystem_model_with_hires runSubsystemModelWithHiresTest.cpp) +add_executable(test_partition_power_electronics runPartitionPowerElectronicsTests.cpp) target_link_libraries( - test_subsystem_model_with_hires - PRIVATE GridKit::power_elec_partition_interfaces - GridKit::testing GridKit::sparse_matrix - GridKit::Utilities) + test_partition_power_electronics + PRIVATE + GridKit::power_elec_bus_line_interface + GridKit::power_elec_disgen + GridKit::power_elec_microline + GridKit::power_elec_microload + GridKit::power_elec_microbusdq + GridKit::testing + GridKit::sparse_matrix + GridKit::Utilities) add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( @@ -20,10 +26,9 @@ target_link_libraries( GridKit::testing) add_test(NAME PowerElectronicsNodeTest COMMAND $) -add_test(NAME SubsystemModelWithHires COMMAND $) -add_test(NAME PowerElectronicsComponentCloneTest - COMMAND $) +add_test(NAME PartitionPowerElectronicsTest COMMAND $) +add_test(NAME PowerElectronicsComponentCloneTest COMMAND $) install(TARGETS test_power_electronics_node RUNTIME DESTINATION bin) -install(TARGETS test_subsystem_model_with_hires RUNTIME DESTINATION bin) +install(TARGETS test_partition_power_electronics RUNTIME DESTINATION bin) install(TARGETS test_power_electronics_component_clone RUNTIME DESTINATION bin) From 489655c267775fe3b8deba1320392ee32e8cd4d1 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:15:20 +0000 Subject: [PATCH 25/31] Remove BusPartitionInterface as requested during code review --- .../BusPartitionInterface.cpp | 372 ------------------ .../BusPartitionInterface.hpp | 171 -------- .../PartitionInterface/CMakeLists.txt | 12 +- 3 files changed, 1 insertion(+), 554 deletions(-) delete mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp delete mode 100644 GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp deleted file mode 100644 index 533b918ce4..0000000000 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.cpp +++ /dev/null @@ -1,372 +0,0 @@ - -#include "BusPartitionInterface.hpp" - -#include -#include -#include - -#include -#include - -namespace GridKit -{ - - namespace PowerElectronics - { - - /** - * @brief Constructs a partition interface between a bus and a circuit component. - * - * Initializes the interface with a clone of the component across the partition - * boundary and marks all interface variables as external. The constructor also - * identifies the component Jacobian rows associated with the bus and stores the - * corresponding Jacobian entries for use by the interface. - * - * @param bus Bus associated with the partition interface. - * @param component Component across the partition boundary to clone. - * @param id Unique identifier for the interface. - */ - template - BusPartitionInterface::BusPartitionInterface(node_type* bus, component_type* component, IdxT id) - : component_(component->clone()), - bus_(bus) - { - size_ = component_->size(); - n_intern_ = 0; - n_extern_ = static_cast(component_->size()); - idc_ = id; - - // Mark all interface variables as external. - for (IdxT i = 0; i < size_; i++) - { - extern_indices_.insert(i); - } - - // Collect the global indices of variables associated with the bus. - std::vector bus_connections(static_cast(bus_->size())); - - for (size_t i = 0; i < bus_connections.size(); ++i) - { - bus_connections[i] = bus_->getNodeConnection(i).idx_; - } - - // Identify bus-associated Jacobian rows and collect all entries in each row. - const IdxT* coo_rows = component_->jacobianCooRows(); - - nnz_ = 0; - - for (IdxT k = 0; k < component_->nnz(); ++k) - { - const IdxT row_node = component_->getNodeConnection(static_cast(coo_rows[k])); - - bool is_bus_row = std::find(bus_connections.begin(), bus_connections.end(), row_node) != bus_connections.end(); - - if (is_bus_row) - { - ++nnz_; - jac_map_.push_back(k); // Keep track of the entries so they can be easily extracted - } - } - } - - template - BusPartitionInterface::~BusPartitionInterface() - { - } - - /** - * @brief Allocate storage and initialize the interface mappings. - * - * Identifies the external variables of the wrapped component that are - * connected to the bus and builds the mappings used to transfer residual - * contributions between the component and the interface. Private storage is - * also allocated for the internal variables of the wrapped component. - * - * @return 0 on success and a nonzero value if the component does not contain - * all variables required by the bus interface. - */ - template - int BusPartitionInterface::allocate() - { - - CircuitComponent::allocate(); - - // Build a lookup of the global indices belonging to the bus. - std::unordered_map bus_connections; - - for (size_t i = 0; i < static_cast(bus_->size()); ++i) - { - bus_connections[bus_->getNodeConnection(i).idx_] = i; - } - - const auto& external_indices = component_->getExternIndices(); - - is_external_.assign(static_cast(size_), false); - - bus_input_ports_.clear(); - bus_output_ports_.clear(); - - size_t external_index = 0; - - // Identify which component variables are external and which of those external - // variables are connected to the bus. bus_input_ports_ stores the corresponding - // variable position in this interface, while bus_output_ports_ stores its position - // in the wrapped component's external residual vector. - for (size_t i = 0; i < static_cast(size_); ++i) - { - const IdxT connection_index = component_->getNodeConnection(i); - - this->setConnectionNodes(i, connection_index); - - if (external_indices.contains(static_cast(i))) - { - is_external_[i] = true; - - if (bus_connections.contains(connection_index)) - { - bus_input_ports_.push_back(i); - bus_output_ports_.push_back(external_index); - } - - ++external_index; - } - } - - // A valid bus interface must contain every variable belonging to the bus. - // If fewer bus variables are found, the wrapped component does not provide - // the complete coupling required by this interface. - const size_t bus_size = static_cast(bus_->size()); - - if (bus_input_ports_.size() != bus_size) - { - GridKit::Utilities::Logger::error() << "ERROR: Invalid partition interface detected. " - << "Bus(ID=" << bus_->busID() - << "), Component(ID=" << component_->getIDcomponent() - << "). Expected " << bus_size - << " bus connections, but found " - << bus_input_ports_.size() << "." - << std::endl; - - return 1; - } - - // The wrapped component is evaluated independently by the interface. - // Allocate storage for its internal variables and residuals and redirect - // its internal pointers to this private storage. - const size_t internal_size = static_cast(component_->getInternalSize()); - - component_y_int_ = std::make_unique(internal_size); - component_yp_int_ = std::make_unique(internal_size); - component_f_int_ = std::make_unique(internal_size); - - component_->setInternalPointer(component_y_int_.get()); - component_->setInternalDerivativePointer(component_yp_int_.get()); - component_->setInternalResidualPointer(component_f_int_.get()); - - component_f_ext_ = std::make_unique(component_->getExternSize()); - - return 0; - } - - template - int BusPartitionInterface::setAbsoluteTolerance(RealT rel_tol) - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - /** - * @brief Initialize the partition interface. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::initialize() - { - return 0; - } - - /** - * @brief Identify differential variables - */ - template - int BusPartitionInterface::tagDifferentiable() - { - return 0; - } - - /** - * @brief Evaluates the internal residual. - * - * All variables are external from the interface's point of view, so there - * are no internal residual contributions to evaluate. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::evaluateInternalResidual() - { - return 0; - } - - /** - * @brief Evaluate the wrapped component's contributions to the bus residual. - * - * The wrapped component is evaluated using state information supplied through - * the interface. Residual contributions associated with bus variables are - * extracted from the component's external residual and accumulated into the - * corresponding interface residual entries. - * - * @return 0 on success, or the error code returned by the wrapped component. - */ - template - int BusPartitionInterface::evaluateExternalResidual() - { - std::fill_n(component_f_ext_.get(), component_->getExternSize(), ScalarT{}); - - updateComponentPointers(); - - if (int err_code = component_->evaluateExternalResidual()) - { - return err_code; - } - - // Only contributions associated with the bus are accumulated - // into the interface residual below. - for (size_t i = 0; i < bus_input_ports_.size(); ++i) - { - *f_ext_[bus_input_ports_[i]] += component_f_ext_[bus_output_ports_[i]]; - } - - return 0; - } - - /** - * @brief Evaluate the Jacobian contributions associated with the bus. - * - * Evaluates the Jacobian of the wrapped component and extracts the entries - * whose residual rows correspond to variables belonging to the connected bus. - * The selected entries are then used to assemble the interface Jacobian. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::evaluateJacobian() - { - - this->zeroJacMatrix(); - - updateComponentPointers(); - - component_->evaluateJacobian(); - - const IdxT* cooRows = component_->jacobianCooRows(); - const IdxT* cooCols = component_->jacobianCooCols(); - const RealT* cooVals = component_->jacobianCooValues(); - - std::vector rows; - std::vector cols; - std::vector vals; - - rows.reserve(jac_map_.size()); - cols.reserve(jac_map_.size()); - vals.reserve(jac_map_.size()); - - // Extract only the Jacobian entries whose residual rows belong to the bus. - for (const IdxT index : jac_map_) - { - rows.push_back(cooRows[index]); - cols.push_back(cooCols[index]); - vals.push_back(cooVals[index]); - } - - this->setJacValues(rows, cols, vals); - - return 0; - } - - /** - * @brief Update the wrapped component with state and output residual pointers. - * - * Reconstructs the state required to evaluate the wrapped component using - * data supplied through the interface. External component variables are - * connected directly to the interface data, while internal component - * variables are copied into the interface's private storage, which the wrapped - * component already keeps track of for its internal state. - * - * @pre The interface must be allocated and its external state pointers must - * reference valid state and derivative data. - * - * @post The wrapped component is configured with the state, derivative, and - * residual pointers required for evaluation. - * - * @param residual Storage for the component's external residual contributions, - * or nullptr when residual output is not required. - * - * @return 0 on success. - */ - template - int BusPartitionInterface::updateComponentPointers() - { - size_t internal_index = 0; - size_t external_index = 0; - - component_->updateTime(time_, alpha_); - - // Reconstruct the state expected by the wrapped component from the - // interface's external data. Route the external residual to component_f_ext_. - for (size_t i = 0; i < static_cast(component_->size()); ++i) - { - if (is_external_[i]) - { - ExternalConnection connection{ - .y_ = y_ext_[i], - .yp_ = yp_ext_[i], - .f_ = &component_f_ext_[external_index], - .idx_ = component_->getNodeConnection(i)}; - - component_->setExternalConnectionNodes(i, connection); - ++external_index; - } - else - { - component_y_int_[internal_index] = *y_ext_[i]; - component_yp_int_[internal_index] = *yp_ext_[i]; - ++internal_index; - } - } - - return 0; - } - - template - int BusPartitionInterface::evaluateIntegrand() - { - return 0; - } - - template - int BusPartitionInterface::initializeAdjoint() - { - return 0; - } - - template - int BusPartitionInterface::evaluateAdjointResidual() - { - return 0; - } - - template - int BusPartitionInterface::evaluateAdjointIntegrand() - { - return 0; - } - - // Available template instantiations - template class BusPartitionInterface; - template class BusPartitionInterface; - template class BusPartitionInterface; - template class BusPartitionInterface; - } // namespace PowerElectronics -} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp deleted file mode 100644 index 5a557120a9..0000000000 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusPartitionInterface.hpp +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @file BusPartitionInterface.hpp - * @author Abdourahman Barry (abdourahman@vt.edu) - * - */ - -#pragma once - -#include - -#include -#include -#include - -namespace GridKit -{ - namespace PowerElectronics - { - /** - * @brief Partition interface that provides a wrapped component's current - * injection contributions to a bus. - * - * A BusPartitionInterface is used when a bus and one of its connected - * components belong to different partitions. The interface represents the - * component within the partition containing the bus and provides the current - * injection contributions that the wrapped component makes to that bus. - * - * The interface has the same number of variables as the wrapped component, - * with a one-to-one correspondence between interface variables and wrapped - * component variables. However, all variables are external from the - * interface's point of view, regardless of whether the corresponding variable - * is internal or external to the wrapped component: - * - * @code - * Interface variables: [ext_, ext_, ext_, ext_, ext_, ext_, ext_] - * | | | | | | | - * Wrapped component: [int_, int_, ext_, int_, ext_, ext_, ext_] - * @endcode - * - * This allows the partition containing the wrapped component to provide all - * state information required to reconstruct and evaluate the component on the - * bus side of the partition boundary. Variables that are internal to the - * wrapped component are stored in private interface storage during evaluation, - * while variables that are external to the wrapped component are connected - * directly to the corresponding interface data. - * - * After evaluating the wrapped component, the interface extracts the residual - * and Jacobian contributions associated with the bus and exposes them to the - * partition containing the bus. - * - * * @note The interface residual contributions are zero for all variables except - * those connected to the bus. Only variables connected to the bus expose - * the wrapped component's contribution to the partition. - * - * @note This interface must be added to the partition containing the bus - * to ensure that the system is properly partitioned. - * - * @tparam ScalarT Scalar type used by the model. - * @tparam IdxT Index type used for variables and connections. - */ - template - class BusPartitionInterface : public PartitionInterface - { - - using component_type = CircuitComponent; - using node_type = typename PowerElectronics::NodeBase; - using RealT = typename CircuitComponent::RealT; - - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::time_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::tag_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::g_; - using CircuitComponent::yB_; - using CircuitComponent::ypB_; - using CircuitComponent::fB_; - using CircuitComponent::gB_; - using CircuitComponent::param_; - using CircuitComponent::idc_; - - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - using CircuitComponent::connection_nodes_; - - public: - BusPartitionInterface(node_type* bus, component_type* component, IdxT id); - virtual ~BusPartitionInterface(); - - int allocate() final; - int initialize() final; - int tagDifferentiable() final; - int setAbsoluteTolerance(RealT) final; - int evaluateInternalResidual() final; - int evaluateExternalResidual() final; - int evaluateJacobian() final; - int evaluateIntegrand() final; - - int initializeAdjoint() final; - int evaluateAdjointResidual() final; - // int evaluateAdjointJacobian(); - int evaluateAdjointIntegrand() final; - - private: - int updateComponentPointers(); - - /** - * @brief Circuit component participating in the partition split. - */ - std::unique_ptr component_; - - /** - * @brief Bus associated with the partition boundary. - */ - node_type* bus_; - - /** - * @brief Storage for the wrapped component's internal state variables. - */ - std::unique_ptr component_y_int_; - - /** - * @brief Storage for the wrapped component's internal state derivatives. - */ - std::unique_ptr component_yp_int_; - - /** - * @brief Storage for the wrapped component's internal residual values. - */ - std::unique_ptr component_f_int_; - - /** - * @brief Storage for the wrapped component's external residual values. - */ - std::unique_ptr component_f_ext_; - - /** - * @brief Wrapped component ports that receive variables from the bus. - */ - std::vector bus_input_ports_; - - /** - * @brief Wrapped component ports whose residual contributions are exposed - * to the bus. - */ - std::vector bus_output_ports_; - - /** - * @brief Mapping from interface Jacobian entries to wrapped component - * Jacobian entries. - */ - std::vector jac_map_; - - /** - * @brief Cached lookup indicating whether each wrapped component variable is external. - * - * Avoids repeatedly searching `extern_indices_` when determining whether - * a wrapped component variable is internal or external. - */ - std::vector is_external_; - }; - } // namespace PowerElectronics -} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt index f14aa7b555..0cb50865b3 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -1,17 +1,7 @@ -gridkit_add_library( - power_elec_partition_interfaces - SOURCES BusPartitionInterface.cpp - HEADERS BusPartitionInterface.hpp PartitionInterface.hpp - LINK_LIBRARIES - PUBLIC - GridKit::dense_vector - PUBLIC - GridKit::utilities_logger) - gridkit_add_library( power_elec_bus_line_interface SOURCES BusLinePartitionInterface.cpp - HEADERS BusLinePartitionInterface.hpp + HEADERS BusLinePartitionInterface.hpp PartitionInterface.hpp LINK_LIBRARIES PUBLIC GridKit::dense_vector From 3c0aa34eb47651125760250b8a52cf56c90fadec Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 03:23:25 +0000 Subject: [PATCH 26/31] change "counter" to "index" --- .../Model/PowerElectronics/SubsystemModel.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index 82b0ea527e..c54484d54e 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -180,11 +180,11 @@ namespace GridKit } // Store the global indices of all external coupling variables. - size_t counter = 0; + size_t index = 0; for (size_t i = n_intern_; i < (n_intern_ + n_extern_); i++) { - external_data_indices_[counter] = this->getNodeConnection(i); - counter++; + external_data_indices_[index] = this->getNodeConnection(i); + index++; } // Map each node's global connection indices to subsystem-local indices, @@ -322,7 +322,7 @@ namespace GridKit IdxT* cols_dup = new IdxT[nnz_dup]; RealT* vals_dup = new RealT[nnz_dup]; - counter = 0; + index = 0; for (const component_type* component : components_) { const IdxT* r = component->jacobianCooRows(); @@ -340,11 +340,11 @@ namespace GridKit continue; } - rows_dup[counter] = row; - cols_dup[counter] = col; - vals_dup[counter] = v[i]; + rows_dup[index] = row; + cols_dup[index] = col; + vals_dup[index] = v[i]; - ++counter; + ++index; } } From 4a1235e26cb81814d9144f725c207c2fd3853e08 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Sun, 18 Oct 2026 04:45:46 +0000 Subject: [PATCH 27/31] Set f_ext_data_ to zero --- GridKit/Model/PowerElectronics/SubsystemModel.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index c54484d54e..b89b358cd7 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -112,6 +112,8 @@ namespace GridKit ~SubsystemModel() override { + restoreGlobalConnection(); + for (auto* comp : interfaces_) { delete comp; @@ -410,6 +412,8 @@ namespace GridKit yp_ext_data_.setDataUpdated(); } + f_ext_data_.setToZero(); + return 0; } From b6581b65552decfd80f4c75f1917af47d9ddfd2d Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 30 Oct 2026 19:58:53 +0000 Subject: [PATCH 28/31] Updated SystemModels --- .../Model/PowerElectronics/SubsystemModel.hpp | 1179 +++++++++-------- .../Model/PowerElectronics/SystemModel.hpp | 23 +- .../SystemModelPowerElectronics.hpp | 531 -------- 3 files changed, 606 insertions(+), 1127 deletions(-) delete mode 100644 GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index b89b358cd7..e2319fb9de 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -1,5 +1,5 @@ /** - * @file BusPartitionInterface.hpp + * @file SubsystemModel.hpp * @author Abdourahman Barry (abdourahman@vt.edu) * */ @@ -17,745 +17,746 @@ #include #include -#include -#include +#include +#include #include -#include +#include #include namespace GridKit { - - /** - * @brief Represents a subset of a PowerElectronicsModel that can be evaluated - * independently. - * - * A SubsystemModel contains a collection of existing GridKit components and - * nodes taken from a larger system. Variables owned by those components and - * nodes become internal variables of the subsystem. Variables needed by those - * components but owned outside the subsystem become external coupling - * variables. - * - * Components normally store connection indices in the global system indexing. - * During subsystem allocation, these indices are temporarily replaced with a - * contiguous local subsystem indexing so that the subsystem can be evaluated - * like an independent PowerElectronicsModel. - * - * External coupling values must be supplied before residual or Jacobian - * evaluation, either directly through the external-data vectors or through a - * forcing function. - * - * @todo Find a better name for this class and its base class. - * - * @tparam ScalarT Scalar type used by the model. - * @tparam IdxT Index type used for variable and connection indices. - */ - template - class SubsystemModel : public PowerElectronicsModel + namespace PowerElectronics { - public: - using TimeFunction = std::function; - - protected: - using SystemModel = PowerElectronicsModel; - using VectorT = typename SystemModel::VectorT; - using RealT = typename CircuitComponent::RealT; - using CsrMatrixT = typename CircuitComponent::CsrMatrixT; - using component_type = CircuitComponent; - using node_type = PowerElectronics::NodeBase; - using interface_type = PartitionInterface; - - using SystemModel::abs_tol_; - using SystemModel::allocated_; - using SystemModel::allocateVectors; - using SystemModel::alpha_; - using SystemModel::connection_nodes_; - using SystemModel::f_int_; - using SystemModel::n_extern_; - using SystemModel::n_intern_; - using SystemModel::nnz_; - using SystemModel::size_; - using SystemModel::tag_; - using SystemModel::time_; - using SystemModel::y_int_; - using SystemModel::yp_int_; - - using SystemModel::components_; - using SystemModel::csr_jac_; - using SystemModel::jac_call_count_; - using SystemModel::map_to_csr_; - using SystemModel::nodes_; - using SystemModel::use_jac_; - - public: /** - * @brief Default constructor for the system model + * @brief Represents a subset of a PowerElectronicsModel that can be evaluated + * independently. * - * @post System model parameters set as default - */ - - SubsystemModel() - : SystemModel(false) - { - } - - /** - * @brief Default constructor for the system model + * A SubsystemModel contains a collection of existing GridKit components and + * nodes taken from a larger system. Variables owned by those components and + * nodes become internal variables of the subsystem. Variables needed by those + * components but owned outside the subsystem become external coupling + * variables. + * + * Components normally store connection indices in the global system indexing. + * During subsystem allocation, these indices are temporarily replaced with a + * contiguous local subsystem indexing so that the subsystem can be evaluated + * like an independent PowerElectronicsModel. + * + * External coupling values must be supplied before residual or Jacobian + * evaluation, either directly through the external-data vectors or through a + * forcing function. * - * @post System model parameters set as default + * @todo Find a better name for this class and its base class. + * + * @tparam ScalarT Scalar type used by the model. + * @tparam IdxT Index type used for variable and connection indices. */ - - SubsystemModel(bool use_jac) - : SystemModel(use_jac) + template + class SubsystemModel : public SystemModel { - } + public: + using TimeFunction = std::function; + + protected: + using System = SystemModel; + using VectorT = typename System::VectorT; + using RealT = typename Component::RealT; + using CsrMatrixT = typename Component::CsrMatrixT; + using component_type = Component; + using node_type = NodeBase; + using interface_type = PartitionInterface; + + using System::abs_tol_; + using System::allocated_; + using System::allocateVectors; + using System::alpha_; + using System::connection_nodes_; + using System::f_int_; + using System::n_extern_; + using System::n_intern_; + using System::nnz_; + using System::size_; + using System::tag_; + using System::time_; + using System::y_int_; + using System::yp_int_; + + using System::components_; + using System::csr_jac_; + using System::jac_call_count_; + using System::map_to_csr_; + using System::nodes_; + using System::use_jac_; + + public: + /** + * @brief Default constructor for the system model + * + * @post System model parameters set as default + */ + + SubsystemModel() + : System(false) + { + } - ~SubsystemModel() override - { - restoreGlobalConnection(); + /** + * @brief Default constructor for the system model + * + * @post System model parameters set as default + */ - for (auto* comp : interfaces_) + SubsystemModel(bool use_jac) + : System(use_jac) { - delete comp; } - // SubsystemModel does not own components_/nodes_ — they belong to (and are - // deleted by) the parent system model. - components_.clear(); - nodes_.clear(); - } - /** - * @brief Allocate the subsystem for independent evaluation. - * - * Converts the selected components and nodes from global system indexing to - * local subsystem indexing, allocates storage for the subsystem's internal - * variables, and creates storage for coupling variables that lie outside the - * subsystem. - * - * Component pointers are then redirected to either the subsystem's internal - * vectors or its external coupling-data vectors. Finally, the subsystem - * Jacobian structure is assembled using only entries whose row and column - * belong to internal subsystem variables. - * - * @pre Components and nodes added to the subsystem must already be allocated - * by the parent system. - * - * @post Component and node connection indices use local subsystem indexing, - * and the subsystem is ready for residual and Jacobian evaluation. - * - * @return 0 on success, otherwise an error code returned during allocation. - */ - int allocate() override - { - if (this->isAllocated()) + ~SubsystemModel() override { - if (int err_code = restoreGlobalConnection()) + restoreGlobalConnection(); + + for (auto* comp : interfaces_) { - return err_code; + delete comp; } + // SubsystemModel does not own components_/nodes_ — they belong to (and are + // deleted by) the parent system model. + components_.clear(); + nodes_.clear(); } - if (int err_code = buildConnectionMap()) + /** + * @brief Allocate the subsystem for independent evaluation. + * + * Converts the selected components and nodes from global system indexing to + * local subsystem indexing, allocates storage for the subsystem's internal + * variables, and creates storage for coupling variables that lie outside the + * subsystem. + * + * Component pointers are then redirected to either the subsystem's internal + * vectors or its external coupling-data vectors. Finally, the subsystem + * Jacobian structure is assembled using only entries whose row and column + * belong to internal subsystem variables. + * + * @pre Components and nodes added to the subsystem must already be allocated + * by the parent system. + * + * @post Component and node connection indices use local subsystem indexing, + * and the subsystem is ready for residual and Jacobian evaluation. + * + * @return 0 on success, otherwise an error code returned during allocation. + */ + int allocate() override { - return err_code; - } - - // Allocate external subsystem vectors. - y_ext_data_.resize(static_cast(n_extern_)); - yp_ext_data_.resize(static_cast(n_extern_)); - f_ext_data_.resize(static_cast(n_extern_)); + if (this->isAllocated()) + { + if (int err_code = restoreGlobalConnection()) + { + return err_code; + } + } - // Contains global external indicies used for - // fetching external variables from global state vectors - external_data_indices_.resize(n_extern_); + if (int err_code = buildConnectionMap()) + { + return err_code; + } - // Contains internal and external connection indicies - connection_nodes_ = std::make_unique(n_intern_ + n_extern_); - allocateVectors(static_cast(size_), true); // Allocate internal subsystem vectors. + // Allocate external subsystem vectors. + y_ext_data_.resize(static_cast(n_extern_)); + yp_ext_data_.resize(static_cast(n_extern_)); + f_ext_data_.resize(static_cast(n_extern_)); - tag_.resize(size_); + // Contains global external indicies used for + // fetching external variables from global state vectors + external_data_indices_.resize(n_extern_); - // Store the mapping from local subsystem indices back to their global system indices - for (const auto [global_idx, local_idx] : global_to_local_map_) - { - this->setConnectionNodes(local_idx, global_idx); - } + // Contains internal and external connection indicies + connection_nodes_ = std::make_unique(n_intern_ + n_extern_); + allocateVectors(static_cast(size_), true); // Allocate internal subsystem vectors. - // Store the global indices of all external coupling variables. - size_t index = 0; - for (size_t i = n_intern_; i < (n_intern_ + n_extern_); i++) - { - external_data_indices_[index] = this->getNodeConnection(i); - index++; - } + tag_.resize(size_); - // Map each node's global connection indices to subsystem-local indices, - // then set the corresponding state, derivative, and residual pointers. - for (node_type* node : nodes_) - { - for (IdxT i = 0; i < static_cast(node->getInternalSize()); i++) + // Store the mapping from local subsystem indices back to their global system indices + for (const auto [global_idx, local_idx] : global_to_local_map_) { - const IdxT node_global_connection = node->getNodeConnection(i).idx_; - const IdxT node_local_connection = global_to_local_map_.at(node_global_connection); - - ExternalConnection node_connection{ - .y_ = y_int_ + node_local_connection, - .yp_ = yp_int_ + node_local_connection, - .f_ = f_int_ + node_local_connection, - .idx_ = static_cast(node_local_connection)}; + this->setConnectionNodes(local_idx, global_idx); + } - node->setExternalConnectionNodes(i, node_connection); + // Store the global indices of all external coupling variables. + size_t index = 0; + for (size_t i = n_intern_; i < (n_intern_ + n_extern_); i++) + { + external_data_indices_[index] = this->getNodeConnection(i); + index++; } - } - ScalarT* y_ext_data_ptr = y_ext_data_.getData(); - ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); - ScalarT* f_ext_data_ptr = f_ext_data_.getData(); + // Map each node's global connection indices to subsystem-local indices, + // then set the corresponding state, derivative, and residual pointers. + for (node_type* node : nodes_) + { + for (IdxT i = 0; i < static_cast(node->getInternalSize()); i++) + { + const IdxT node_global_connection = node->getNodeConnection(i).idx_; + const IdxT node_local_connection = global_to_local_map_.at(node_global_connection); - // Map connections from global indices to subsystem-local indices and set data pointers accordingly. - size_t component_internal_idx = 0; - for (component_type* comp : components_) - { - // Update component internal pointers to their correct offsets - comp->setInternalPointer(&y_int_[component_internal_idx]); - comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); - comp->setInternalResidualPointer(&f_int_[component_internal_idx]); + ExternalConnection node_connection{ + .y_ = y_int_ + node_local_connection, + .yp_ = yp_int_ + node_local_connection, + .f_ = f_int_ + node_local_connection, + .idx_ = static_cast(node_local_connection)}; - component_internal_idx += comp->getInternalSize(); + node->setExternalConnectionNodes(i, node_connection); + } + } - const auto& external_indices = comp->getExternIndices(); + ScalarT* y_ext_data_ptr = y_ext_data_.getData(); + ScalarT* yp_ext_data_ptr = yp_ext_data_.getData(); + ScalarT* f_ext_data_ptr = f_ext_data_.getData(); - // Map each component connection from its global index to the - // corresponding subsystem-local index. - for (IdxT i = 0; i < comp->size(); i++) + // Map connections from global indices to subsystem-local indices and set data pointers accordingly. + size_t component_internal_idx = 0; + for (component_type* comp : components_) { - const IdxT comp_global_connection = comp->getNodeConnection(i); - const IdxT comp_local_connection = global_to_local_map_.at(comp_global_connection); + // Update component internal pointers to their correct offsets + comp->setInternalPointer(&y_int_[component_internal_idx]); + comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); + comp->setInternalResidualPointer(&f_int_[component_internal_idx]); - if (!external_indices.contains(i)) - { - // If the connection is internal to the component, we know it is internal to the subsystem, - // so only update its index. The pointers have already been updated in the outer loop. - comp->setConnectionNodes(i, comp_local_connection); - } - else if (comp_local_connection < static_cast(this->getInternalSize())) - { - // Otherwise, if the variable is external to this component but internal to - // another component or node within the subsystem (which it is when the local index - // is less than internal size), connect it to the subsystem's internal storage. + component_internal_idx += comp->getInternalSize(); - ExternalConnection connection{ - .y_ = y_int_ + comp_local_connection, - .yp_ = yp_int_ + comp_local_connection, - .f_ = f_int_ + comp_local_connection, - .idx_ = comp_local_connection}; + const auto& external_indices = comp->getExternIndices(); - comp->setExternalConnectionNodes(i, connection); - } - else + // Map each component connection from its global index to the + // corresponding subsystem-local index. + for (IdxT i = 0; i < comp->size(); i++) { - // Otherwise, the variable is external to the subsystem, so connect - // it to the external coupling storage. + const IdxT comp_global_connection = comp->getNodeConnection(i); + const IdxT comp_local_connection = global_to_local_map_.at(comp_global_connection); + + if (!external_indices.contains(i)) + { + // If the connection is internal to the component, we know it is internal to the subsystem, + // so only update its index. The pointers have already been updated in the outer loop. + comp->setConnectionNodes(i, comp_local_connection); + } + else if (comp_local_connection < static_cast(this->getInternalSize())) + { + // Otherwise, if the variable is external to this component but internal to + // another component or node within the subsystem (which it is when the local index + // is less than internal size), connect it to the subsystem's internal storage. + + ExternalConnection connection{ + .y_ = y_int_ + comp_local_connection, + .yp_ = yp_int_ + comp_local_connection, + .f_ = f_int_ + comp_local_connection, + .idx_ = comp_local_connection}; + + comp->setExternalConnectionNodes(i, connection); + } + else + { + // Otherwise, the variable is external to the subsystem, so connect + // it to the external coupling storage. - const IdxT external_offset = comp_local_connection - static_cast(this->getInternalSize()); + const IdxT external_offset = comp_local_connection - static_cast(this->getInternalSize()); - ExternalConnection connection{ - .y_ = y_ext_data_ptr + external_offset, - .yp_ = yp_ext_data_ptr + external_offset, - .f_ = f_ext_data_ptr + external_offset, - .idx_ = comp_local_connection}; + ExternalConnection connection{ + .y_ = y_ext_data_ptr + external_offset, + .yp_ = yp_ext_data_ptr + external_offset, + .f_ = f_ext_data_ptr + external_offset, + .idx_ = comp_local_connection}; - comp->setExternalConnectionNodes(i, connection); + comp->setExternalConnectionNodes(i, connection); + } } } - } - // Allocation always rebuilds the system Jacobian and its COO-to-CSR map. - delete csr_jac_; - csr_jac_ = nullptr; + // Allocation always rebuilds the system Jacobian and its COO-to-CSR map. + delete csr_jac_; + csr_jac_ = nullptr; - delete[] map_to_csr_; - map_to_csr_ = nullptr; + delete[] map_to_csr_; + map_to_csr_ = nullptr; - // Evaluate component Jacobians to get sparsity - for (component_type* component : components_) - { - component->evaluateJacobian(); - } - - // Check whether a Jacobian entry belongs to the subsystem Jacobian. - // Only entries whose row and column are both internal subsystem - // variables are retained. - auto isValidEntry = [this](IdxT row, IdxT col) - { - if (row == INVALID_INDEX || col == INVALID_INDEX) + // Evaluate component Jacobians to get sparsity + for (component_type* component : components_) { - return false; + component->evaluateJacobian(); } - const bool row_is_internal = static_cast(row) < this->getInternalSize(); - const bool col_is_internal = static_cast(col) < this->getInternalSize(); + // Check whether a Jacobian entry belongs to the subsystem Jacobian. + // Only entries whose row and column are both internal subsystem + // variables are retained. + auto isValidEntry = [this](IdxT row, IdxT col) + { + if (row == INVALID_INDEX || col == INVALID_INDEX) + { + return false; + } - return (row_is_internal && col_is_internal); - }; + const bool row_is_internal = static_cast(row) < this->getInternalSize(); + const bool col_is_internal = static_cast(col) < this->getInternalSize(); - IdxT nnz_dup = 0; + return (row_is_internal && col_is_internal); + }; - for (const component_type* component : components_) - { - const IdxT* r = component->jacobianCooRows(); - const IdxT* c = component->jacobianCooCols(); - const IdxT nnz = component->nnz(); + IdxT nnz_dup = 0; - for (IdxT i = 0; i < nnz; ++i) + for (const component_type* component : components_) { - const IdxT row = component->getNodeConnection(r[i]); - const IdxT col = component->getNodeConnection(c[i]); + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const IdxT nnz = component->nnz(); - if (isValidEntry(row, col)) + for (IdxT i = 0; i < nnz; ++i) { - ++nnz_dup; + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + if (isValidEntry(row, col)) + { + ++nnz_dup; + } } } - } - // Allocate COO triplet arrays (we own these until we hand off to CsrMatrix) - IdxT* rows_dup = new IdxT[nnz_dup]; - IdxT* cols_dup = new IdxT[nnz_dup]; - RealT* vals_dup = new RealT[nnz_dup]; + // Allocate COO triplet arrays (we own these until we hand off to CsrMatrix) + IdxT* rows_dup = new IdxT[nnz_dup]; + IdxT* cols_dup = new IdxT[nnz_dup]; + RealT* vals_dup = new RealT[nnz_dup]; - index = 0; - for (const component_type* component : components_) - { - const IdxT* r = component->jacobianCooRows(); - const IdxT* c = component->jacobianCooCols(); - const RealT* v = component->jacobianCooValues(); - const IdxT nnz = component->nnz(); - - for (IdxT i = 0; i < nnz; ++i) + index = 0; + for (const component_type* component : components_) { - const IdxT row = component->getNodeConnection(r[i]); - const IdxT col = component->getNodeConnection(c[i]); + const IdxT* r = component->jacobianCooRows(); + const IdxT* c = component->jacobianCooCols(); + const RealT* v = component->jacobianCooValues(); + const IdxT nnz = component->nnz(); - if (!isValidEntry(row, col)) + for (IdxT i = 0; i < nnz; ++i) { - continue; - } + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); - rows_dup[index] = row; - cols_dup[index] = col; - vals_dup[index] = v[i]; + if (!isValidEntry(row, col)) + { + continue; + } - ++index; - } - } + rows_dup[index] = row; + cols_dup[index] = col; + vals_dup[index] = v[i]; - // Build the system COO Jacobian - LinearAlgebra::CooMatrix jac(size_, size_, nnz_dup, &rows_dup, &cols_dup, &vals_dup); + ++index; + } + } - // Populate CSR data with sort and deduplicate - IdxT* row_ptrs = jac.getCsrRowData(); + // Build the system COO Jacobian + LinearAlgebra::CooMatrix jac(size_, size_, nnz_dup, &rows_dup, &cols_dup, &vals_dup); - // Deduplicated nnz - nnz_ = jac.getNnz(); + // Populate CSR data with sort and deduplicate + IdxT* row_ptrs = jac.getCsrRowData(); - // Allocate cols/vals with deduplicated nnz - IdxT* cols = new IdxT[nnz_]; - RealT* vals = new RealT[nnz_]; + // Deduplicated nnz + nnz_ = jac.getNnz(); - std::copy(jac.getColData(), jac.getColData() + nnz_, cols); - std::copy(jac.getValues(), jac.getValues() + nnz_, vals); + // Allocate cols/vals with deduplicated nnz + IdxT* cols = new IdxT[nnz_]; + RealT* vals = new RealT[nnz_]; - // Create the CSR Jacobian - csr_jac_ = new CsrMatrixT(size_, size_, nnz_, &row_ptrs, &cols, &vals); + std::copy(jac.getColData(), jac.getColData() + nnz_, cols); + std::copy(jac.getValues(), jac.getValues() + nnz_, vals); - const IdxT* map_to_sorted = jac.getMapToSorted(); - const IdxT* map_to_dedup = jac.getMapToDeduplicated(); + // Create the CSR Jacobian + csr_jac_ = new CsrMatrixT(size_, size_, nnz_, &row_ptrs, &cols, &vals); - // Build a mappping from original COO index to CSR index - map_to_csr_ = new IdxT[nnz_dup]; - for (IdxT i = 0; i < nnz_dup; ++i) - { - map_to_csr_[map_to_sorted[i]] = map_to_dedup[i]; - } + const IdxT* map_to_sorted = jac.getMapToSorted(); + const IdxT* map_to_dedup = jac.getMapToDeduplicated(); - allocated_ = true; - return 0; - } + // Build a mappping from original COO index to CSR index + map_to_csr_ = new IdxT[nnz_dup]; + for (IdxT i = 0; i < nnz_dup; ++i) + { + map_to_csr_[map_to_sorted[i]] = map_to_dedup[i]; + } - /** - * @brief Update the subsystem external state and derivative data. - * - * If a forcing function is provided, invoke it at the current subsystem - * time to update the external coupling variables. - * - * @note The user can supply external coupling data in two ways: - * - Through a forcing function that updates getExternalDataY() - * and getExternalDataYP() before each evaluation. This is useful - * for time-dependent or streaming external data. - * - By directly updating these vectors when no forcing function - * is provided, as in zero-order hold (ZOH) coupling. - * - * @post If a forcing function is set, y_ext_data_ and yp_ext_data_ - * contain the external coupling values supplied by the function. - * - * @return 0 on success. - */ - int distributeExternalVectors() - { + allocated_ = true; + return 0; + } - if (forcing_function_) + /** + * @brief Update the subsystem external state and derivative data. + * + * If a forcing function is provided, invoke it at the current subsystem + * time to update the external coupling variables. + * + * @note The user can supply external coupling data in two ways: + * - Through a forcing function that updates getExternalDataY() + * and getExternalDataYP() before each evaluation. This is useful + * for time-dependent or streaming external data. + * - By directly updating these vectors when no forcing function + * is provided, as in zero-order hold (ZOH) coupling. + * + * @post If a forcing function is set, y_ext_data_ and yp_ext_data_ + * contain the external coupling values supplied by the function. + * + * @return 0 on success. + */ + int distributeExternalVectors() { - (*forcing_function_)(time_); - y_ext_data_.setDataUpdated(); - yp_ext_data_.setDataUpdated(); - } + if (forcing_function_) + { + (*forcing_function_)(time_); - f_ext_data_.setToZero(); + y_ext_data_.setDataUpdated(); + yp_ext_data_.setDataUpdated(); + } - return 0; - } + f_ext_data_.setToZero(); - /** - * @brief Evaluate Residuals at each component then collect them - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int evaluateInternalResidual() override - { - if (int err_code = distributeExternalVectors()) - { - return err_code; + return 0; } - return SystemModel::evaluateInternalResidual(); - } - - /** - * @brief Creates the system Jacobian representing \f$\alpha dF/dy' + dF/dy\f$ - * - * Updates the CSR Jacobian values using the per-component mappings - * computed during allocate(). - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int evaluateJacobian() override - { - if (int err_code = distributeExternalVectors()) + /** + * @brief Evaluate Residuals at each component then collect them + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateInternalResidual() override { - return err_code; - } + if (int err_code = distributeExternalVectors()) + { + return err_code; + } - return SystemModel::evaluateJacobian(); - } + return System::evaluateInternalResidual(); + } - /** - * @brief Add a component to the subsystem. - * - * Rejected while connections are in the local-indexed state, since the - * component's stored connection indices would otherwise be interpreted - * inconsistently with the rest of the subsystem. Call release() first. - * - * @param[in] component Component to add. - */ - void addComponent(component_type* component) - { - if (this->isAllocated()) + /** + * @brief Creates the system Jacobian representing \f$\alpha dF/dy' + dF/dy\f$ + * + * Updates the CSR Jacobian values using the per-component mappings + * computed during allocate(). + * + * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable + */ + int evaluateJacobian() override { - throw std::logic_error("SubsystemModel::addComponent: cannot add a component to an allocated subsystem."); + if (int err_code = distributeExternalVectors()) + { + return err_code; + } + + return System::evaluateJacobian(); } - if (!component->isAllocated()) + /** + * @brief Add a component to the subsystem. + * + * Rejected while connections are in the local-indexed state, since the + * component's stored connection indices would otherwise be interpreted + * inconsistently with the rest of the subsystem. Call release() first. + * + * @param[in] component Component to add. + */ + void addComponent(component_type* component) { - throw std::logic_error("SubsystemModel::addComponent: cannot add an unallocated component."); - } + if (this->isAllocated()) + { + throw std::logic_error("SubsystemModel::addComponent: cannot add a component to an allocated subsystem."); + } - SystemModel::addComponent(component); - } + if (!component->isAllocated()) + { + throw std::logic_error("SubsystemModel::addComponent: cannot add an unallocated component."); + } - /** - * @brief Add a node to the subsystem. - * - * Rejected while connections are in the local-indexed state, since the - * node's stored connection indices would otherwise be interpreted - * inconsistently with the rest of the subsystem. Call release() first. - * - * @param[in] node Node to add. - */ - void addNode(node_type* node) - { - if (this->isAllocated()) - { - throw std::logic_error("SubsystemModel::addNode: cannot add a node to an allocated subsystem."); + System::addComponent(component); } - if (!node->isAllocated()) + /** + * @brief Add a node to the subsystem. + * + * Rejected while connections are in the local-indexed state, since the + * node's stored connection indices would otherwise be interpreted + * inconsistently with the rest of the subsystem. Call release() first. + * + * @param[in] node Node to add. + */ + void addNode(node_type* node) { - throw std::logic_error( - "SubsystemModel::addNode: cannot add an unallocated node."); - } + if (this->isAllocated()) + { + throw std::logic_error("SubsystemModel::addNode: cannot add a node to an allocated subsystem."); + } - SystemModel::addNode(node); - } + if (!node->isAllocated()) + { + throw std::logic_error( + "SubsystemModel::addNode: cannot add an unallocated node."); + } - /** - * @brief Add a partition interface to the subsystem. - * - * Adds the partition interface to the subsystem's component list and keeps a - * separate reference to it in the interface list. - * - * @param component Pointer to the interface component to add. - */ - void addInterface(interface_type* component) - { - addComponent(component); - interfaces_.push_back(component); - } + System::addNode(node); + } - /** - * @brief Restores the subsystem to global system indexing. - * - * Restores the original global connection indices of all components and nodes, - * then clears the subsystem's global-to-local connection map. The subsystem is - * marked unallocated so that its local indexing can be rebuilt when it is - * allocated again. - * - * @pre Component and node connections may use subsystem-local indices. - * - * @post Component and node connections use their original global system indices, - * `global_to_local_map_` is empty, and the subsystem is unallocated. - * - * @return 0 on success, otherwise an error code returned while restoring the - * global connections. - */ - int restoreGlobalConnection() - { - if (int err_code = mapLocalToGlobal()) + /** + * @brief Add a partition interface to the subsystem. + * + * Adds the partition interface to the subsystem's component list and keeps a + * separate reference to it in the interface list. + * + * @param component Pointer to the interface component to add. + */ + void addInterface(interface_type* component) { - return err_code; + addComponent(component); + interfaces_.push_back(component); } - global_to_local_map_.clear(); - - allocated_ = false; - - return 0; - } - - const std::vector& getExternalDataIndices() const - { - return external_data_indices_; - } + /** + * @brief Restores the subsystem to global system indexing. + * + * Restores the original global connection indices of all components and nodes, + * then clears the subsystem's global-to-local connection map. The subsystem is + * marked unallocated so that its local indexing can be rebuilt when it is + * allocated again. + * + * @pre Component and node connections may use subsystem-local indices. + * + * @post Component and node connections use their original global system indices, + * `global_to_local_map_` is empty, and the subsystem is unallocated. + * + * @return 0 on success, otherwise an error code returned while restoring the + * global connections. + */ + int restoreGlobalConnection() + { + if (int err_code = mapLocalToGlobal()) + { + return err_code; + } - VectorT& getExternalDataY() - { - return y_ext_data_; - } + global_to_local_map_.clear(); - VectorT& getExternalDataYP() - { - return yp_ext_data_; - } + allocated_ = false; - VectorT& getExternalDataF() - { - return f_ext_data_; - } + return 0; + } - void setForcingFunction(TimeFunction function) - { - forcing_function_ = std::move(function); - } + const std::vector& getExternalDataIndices() const + { + return external_data_indices_; + } - const std::unordered_map& getInternalMap() const - { - return global_to_local_map_; - } + VectorT& getExternalDataY() + { + return y_ext_data_; + } - private: - /** - * @brief Restores subsystem-local connection indices to global system indices. - * - * Replaces the local connection indices stored by each component and node with - * their corresponding global system indices. Each local index is mapped back to - * its original global connection using the subsystem connection table. - * - * This reverses the local indexing established during subsystem allocation - * without changing the underlying component or node connectivity. - * - * @pre Component and node connections use subsystem-local indices. - * - * @post Component and node connections use their original global system indices. - * - * @return 0 on success. - */ - int mapLocalToGlobal() - { - if (!this->isAllocated()) + VectorT& getExternalDataYP() { - return 0; + return yp_ext_data_; } - IdxT local_connection; + VectorT& getExternalDataF() + { + return f_ext_data_; + } - for (component_type* component : components_) + void setForcingFunction(TimeFunction function) { - for (IdxT i = 0; i < component->size(); i++) - { - local_connection = component->getNodeConnection(i); + forcing_function_ = std::move(function); + } - if (local_connection != INVALID_INDEX) - { - component->setConnectionNodes(i, this->getNodeConnection(local_connection)); - } - } + const std::unordered_map& getInternalMap() const + { + return global_to_local_map_; } - for (node_type* node : nodes_) + private: + /** + * @brief Restores subsystem-local connection indices to global system indices. + * + * Replaces the local connection indices stored by each component and node with + * their corresponding global system indices. Each local index is mapped back to + * its original global connection using the subsystem connection table. + * + * This reverses the local indexing established during subsystem allocation + * without changing the underlying component or node connectivity. + * + * @pre Component and node connections use subsystem-local indices. + * + * @post Component and node connections use their original global system indices. + * + * @return 0 on success. + */ + int mapLocalToGlobal() { - for (IdxT i = 0; i < node->size(); i++) + if (!this->isAllocated()) { - local_connection = node->getNodeConnection(i).idx_; - - if (local_connection != INVALID_INDEX) - { - node->setConnectionNodes(i, this->getNodeConnection(local_connection)); - } + return 0; } - } - - return 0; - } - - /** - * @brief Builds the global-to-local connection map for the subsystem. - * - * Assigns a local connection index to every variable referenced by the subsystem. - * Variables owned by components and nodes in the subsystem form the internal - * variables of the subsystem and are assigned the first N local indices [0, N). - * Variables required by the subsystem but owned outside it are then assigned - * local indices starting at N. - * - * A variable that is external to a component may still be internal to the - * subsystem if it is owned by another component or node in the same subsystem. - * Such variables retain the local indices assigned during the internal passes. - * - * @pre Component and node connections use global system indices. - * - * @post `global_to_local_map_` maps each referenced global connection to its - * subsystem-local index, with internal variables followed by external - * variables. - * - * @return 0 on success. - */ - int buildConnectionMap() - { - global_to_local_map_.clear(); + IdxT local_connection; - // Collect each component's internal variables and assign them local indices, - // since they make up the internal variables of the subsystem. - IdxT local_connection = 0; - for (component_type* comp : components_) - { - const auto& extern_indices = comp->getExternIndices(); - - for (IdxT i = 0; i < comp->size(); i++) + for (component_type* component : components_) { - const IdxT global_connection = comp->getNodeConnection(i); - - if (global_connection != INVALID_INDEX && !extern_indices.contains(i)) + for (IdxT i = 0; i < component->size(); i++) { - global_to_local_map_[global_connection] = local_connection++; + local_connection = component->getNodeConnection(i); + + if (local_connection != INVALID_INDEX) + { + component->setConnectionNodes(i, this->getNodeConnection(local_connection)); + } } } - } - // Node variables are also owned by the subsystem and therefore complete - // the set of subsystem-internal variables. - for (node_type* node : nodes_) - { - for (IdxT i = 0; i < node->size(); i++) + for (node_type* node : nodes_) { - const IdxT global_connection = node->getNodeConnection(i).idx_; - - if (global_connection != INVALID_INDEX) + for (IdxT i = 0; i < node->size(); i++) { - global_to_local_map_[global_connection] = local_connection++; + local_connection = node->getNodeConnection(i).idx_; + + if (local_connection != INVALID_INDEX) + { + node->setConnectionNodes(i, this->getNodeConnection(local_connection)); + } } } - } - // Everything assigned so far is internal to the subsystem. - const IdxT internal_size = local_connection; + return 0; + } - // Finally assign local indices to variables owned outside the subsystem. - for (component_type* comp : components_) + /** + * @brief Builds the global-to-local connection map for the subsystem. + * + * Assigns a local connection index to every variable referenced by the subsystem. + * Variables owned by components and nodes in the subsystem form the internal + * variables of the subsystem and are assigned the first N local indices [0, N). + * Variables required by the subsystem but owned outside it are then assigned + * local indices starting at N. + * + * A variable that is external to a component may still be internal to the + * subsystem if it is owned by another component or node in the same subsystem. + * Such variables retain the local indices assigned during the internal passes. + * + * @pre Component and node connections use global system indices. + * + * @post `global_to_local_map_` maps each referenced global connection to its + * subsystem-local index, with internal variables followed by external + * variables. + * + * @return 0 on success. + */ + int buildConnectionMap() { - auto extern_indices = comp->getExternIndices(); - for (IdxT j = 0; j < comp->size(); j++) + global_to_local_map_.clear(); + + // Collect each component's internal variables and assign them local indices, + // since they make up the internal variables of the subsystem. + IdxT local_connection = 0; + for (component_type* comp : components_) { - if (extern_indices.contains(j)) + const auto& extern_indices = comp->getExternIndices(); + + for (IdxT i = 0; i < comp->size(); i++) { - const IdxT global_connection = comp->getNodeConnection(j); + const IdxT global_connection = comp->getNodeConnection(i); - if (global_connection != INVALID_INDEX && !global_to_local_map_.contains(global_connection)) + if (global_connection != INVALID_INDEX && !extern_indices.contains(i)) { global_to_local_map_[global_connection] = local_connection++; } } } - } - - n_intern_ = static_cast(internal_size); - n_extern_ = static_cast(local_connection - internal_size); - size_ = internal_size; - - return 0; - } - /** - *@brief Maps global system connection indices to local subsystem connection indices. - */ - std::unordered_map global_to_local_map_; + // Node variables are also owned by the subsystem and therefore complete + // the set of subsystem-internal variables. + for (node_type* node : nodes_) + { + for (IdxT i = 0; i < node->size(); i++) + { + const IdxT global_connection = node->getNodeConnection(i).idx_; - /** - * @brief Global system index corresponding to each entry in the external subsystem vectors. - */ - std::vector external_data_indices_; + if (global_connection != INVALID_INDEX) + { + global_to_local_map_[global_connection] = local_connection++; + } + } + } - /** - * @brief subsystem external state data. - */ - VectorT y_ext_data_; + // Everything assigned so far is internal to the subsystem. + const IdxT internal_size = local_connection; - /** - * @brief subsystem external state derivative - */ - VectorT yp_ext_data_; + // Finally assign local indices to variables owned outside the subsystem. + for (component_type* comp : components_) + { + auto extern_indices = comp->getExternIndices(); - /** - * @brief subsystem external residual data - */ - VectorT f_ext_data_; + for (IdxT j = 0; j < comp->size(); j++) + { + if (extern_indices.contains(j)) + { + const IdxT global_connection = comp->getNodeConnection(j); - /** - * @brief Optional time-dependent callback for updating external coupling data. - * - * The callback is invoked before residual and Jacobian evaluations. - */ - std::optional forcing_function_; + if (global_connection != INVALID_INDEX && !global_to_local_map_.contains(global_connection)) + { + global_to_local_map_[global_connection] = local_connection++; + } + } + } + } - /** - * @brief Partition interfaces owned by the subsystem. - * - * These interfaces expose the contributions of components participating - * in the partition split. - */ - std::vector interfaces_; + n_intern_ = static_cast(internal_size); + n_extern_ = static_cast(local_connection - internal_size); + size_ = internal_size; - }; // class SubsystemModel + return 0; + } + /** + *@brief Maps global system connection indices to local subsystem connection indices. + */ + std::unordered_map global_to_local_map_; + + /** + * @brief Global system index corresponding to each entry in the external subsystem vectors. + */ + std::vector external_data_indices_; + + /** + * @brief subsystem external state data. + */ + VectorT y_ext_data_; + + /** + * @brief subsystem external state derivative + */ + VectorT yp_ext_data_; + + /** + * @brief subsystem external residual data + */ + VectorT f_ext_data_; + + /** + * @brief Optional time-dependent callback for updating external coupling data. + * + * The callback is invoked before residual and Jacobian evaluations. + */ + std::optional forcing_function_; + + /** + * @brief Partition interfaces owned by the subsystem. + * + * These interfaces expose the contributions of components participating + * in the partition split. + */ + std::vector interfaces_; + + }; // class SubsystemModel + } // namespace PowerElectronics } // namespace GridKit \ No newline at end of file diff --git a/GridKit/Model/PowerElectronics/SystemModel.hpp b/GridKit/Model/PowerElectronics/SystemModel.hpp index 022a118957..91f33859e6 100644 --- a/GridKit/Model/PowerElectronics/SystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SystemModel.hpp @@ -23,6 +23,7 @@ namespace GridKit using component_type = Component; using node_type = NodeBase; + protected: using Component::abs_tol_; using Component::allocated_; using Component::allocateVectors; @@ -59,7 +60,7 @@ namespace GridKit * * @post System model parameters set as input */ - SystemModel(bool use_jac = false) + SystemModel(bool use_jac) { // Can choose if to use jacobian use_jac_ = use_jac; @@ -117,7 +118,7 @@ namespace GridKit * * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable */ - int allocate() final + int allocate() override { size_t component_internal_size = 0; for (component_type* comp : components_) @@ -157,7 +158,7 @@ namespace GridKit { node->allocate(); - for (size_t i = 0; i < node->getInternalSize(); i++) + for (IdxT i = 0; i < static_cast(node->getInternalSize()); i++) { ExternalConnection node_connection{ .y_ = y_int_ + node_internal_idx, @@ -368,7 +369,7 @@ namespace GridKit * * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable */ - int evaluateInternalResidual() final + int evaluateInternalResidual() override { for (IdxT i = 0; i < size_; i++) { @@ -409,7 +410,7 @@ namespace GridKit * * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable */ - int evaluateJacobian() final + int evaluateJacobian() override { // Zero out values RealT* vals = csr_jac_->getValues(); @@ -431,7 +432,15 @@ namespace GridKit for (IdxT i = 0; i < nnz; ++i) { - if (component->getNodeConnection(r[i]) != neg1_ && component->getNodeConnection(c[i]) != neg1_) + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + const bool is_internal_entry = row != INVALID_INDEX + && col != INVALID_INDEX + && row < this->getInternalSize() + && col < this->getInternalSize(); + + if (is_internal_entry) { vals[map_to_csr_[counter]] += v[i]; ++counter; @@ -515,7 +524,7 @@ namespace GridKit allocated_ = false; } - private: + protected: static constexpr IdxT neg1_ = INVALID_INDEX; std::vector components_; diff --git a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp b/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp deleted file mode 100644 index 61c3a1933b..0000000000 --- a/GridKit/Model/PowerElectronics/SystemModelPowerElectronics.hpp +++ /dev/null @@ -1,531 +0,0 @@ - - -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace GridKit -{ - /** - * @todo Move all PowerElectronics models into the PowerElectronics namespace - * for consistency. - */ - template - class PowerElectronicsModel : public CircuitComponent - { - using RealT = typename CircuitComponent::RealT; - using CsrMatrixT = typename CircuitComponent::CsrMatrixT; - using component_type = CircuitComponent; - using node_type = PowerElectronics::NodeBase; - - protected: - using CircuitComponent::abs_tol_; - using CircuitComponent::allocated_; - using CircuitComponent::allocateVectors; - using CircuitComponent::alpha_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - using CircuitComponent::nnz_; - using CircuitComponent::size_; - using CircuitComponent::tag_; - using CircuitComponent::time_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - - public: - /** - * @brief Constructor for the system model - * - * @param[in] use_jac Boolean to choose if to use jacobian - * - * @post System model parameters set as input - */ - PowerElectronicsModel(bool use_jac = false) - { - // Can choose if to use jacobian - use_jac_ = use_jac; - } - - /** - * @brief Destructor for the system model - * - * @pre System components are allocated - * - * @post System components are deallocated - * - */ - virtual ~PowerElectronicsModel() - { - for (auto comp : this->components_) - { - delete comp; - } - delete csr_jac_; - delete[] map_to_csr_; - } - - /** - * @brief Will check if each component has jacobian avalible. If one doesn't have it, return false. - * - * - * @return true if all components have jacobian - * @return false otherwise - */ - bool hasJacobian() final - { - if (!this->use_jac_) - return false; - - for (const auto& component : components_) - { - if (!component->hasJacobian()) - { - return false; - } - } - return true; - } - - /** - * @brief Allocate system vectors and construct the system CSR Jacobian - * - * @post System model vectors allocated with the computed total number of unknowns - * @post CSR Jacobian sparsity pattern is computed - * @post COO->CSR mapping is computed - * @post Every component's \ref CircuitComponent::y_int_, \ref CircuitComponent::yp_int_, and \ref CircuitComponent::f_int_ pointers - * are set to their appropriate offsets in the system vector, allowing them to directly access their internal variables, derivatives, - * and residuals. - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int allocate() override - { - size_t component_internal_size = 0; - for (component_type* comp : components_) - { - component_internal_size += comp->getInternalSize(); - } - - size_t node_internal_size = 0; - for (node_type* node : nodes_) - { - node_internal_size += node->getInternalSize(); - } - - n_intern_ = component_internal_size + node_internal_size; - n_extern_ = 0; - size_ = n_intern_ + n_extern_; - - // Allocation always rebuilds the system Jacobian and its COO-to-CSR map. - delete csr_jac_; - csr_jac_ = nullptr; - - delete[] map_to_csr_; - map_to_csr_ = nullptr; - - if (!allocated_) - { - allocateVectors(static_cast(size_), true); - // Component and node offsets can change when topology is modified. - abs_tol_.setToZero(memory::HOST); - } - - tag_.resize(size_); - - { // Start node internal indexing after all component internals for proper KLU ordering - size_t node_internal_idx = component_internal_size; - for (node_type* node : nodes_) - { - node->allocate(); - - for (size_t i = 0; i < node->getInternalSize(); i++) - { - ExternalConnection node_connection{ - .y_ = y_int_ + node_internal_idx, - .yp_ = yp_int_ + node_internal_idx, - .f_ = f_int_ + node_internal_idx, - .idx_ = static_cast(node_internal_idx)}; - - node->setExternalConnectionNodes(i, node_connection); - node_internal_idx++; - } - } - } - - { - // The offset for each component's internal variables in the system vector. - // They start at 0, and are stacked on top of each other. - size_t component_internal_idx = 0; - for (component_type* comp : components_) - { - comp->allocate(); - - // Update component internal pointers to their correct offsets - comp->setInternalPointer(&y_int_[component_internal_idx]); - comp->setInternalDerivativePointer(&yp_int_[component_internal_idx]); - comp->setInternalResidualPointer(&f_int_[component_internal_idx]); - - const auto& external_indices = comp->getExternIndices(); - for (IdxT i = 0; i < comp->size(); i++) - { - if (!external_indices.contains(i)) - { - comp->setInternalConnectionNodes(i, component_internal_idx); - component_internal_idx++; - } - } - } - } - - // Evaluate component Jacobians to get sparsity - for (component_type* component : components_) - { - component->evaluateJacobian(); - } - - // Count the number of non-zeros - IdxT nnz_dup = 0; - for (const component_type* component : components_) - { - const IdxT* r = component->jacobianCooRows(); - const IdxT* c = component->jacobianCooCols(); - IdxT nnz = component->nnz(); - - for (IdxT i = 0; i < nnz; ++i) - { - if (component->getNodeConnection(r[i]) != neg1_ && component->getNodeConnection(c[i]) != neg1_) - { - ++nnz_dup; - } - } - } - - // Allocate COO triplet arrays (we own these until we hand off to CsrMatrix) - IdxT* rows_dup = new IdxT[nnz_dup]; - IdxT* cols_dup = new IdxT[nnz_dup]; - RealT* vals_dup = new RealT[nnz_dup]; - - IdxT counter = 0; - for (const component_type* component : components_) - { - const IdxT* r = component->jacobianCooRows(); - const IdxT* c = component->jacobianCooCols(); - const RealT* v = component->jacobianCooValues(); - IdxT nnz = component->nnz(); - - for (IdxT i = 0; i < nnz; ++i) - { - if (component->getNodeConnection(r[i]) != neg1_ && component->getNodeConnection(c[i]) != neg1_) - { - rows_dup[counter] = component->getNodeConnection(r[i]); - cols_dup[counter] = component->getNodeConnection(c[i]); - vals_dup[counter] = v[i]; - counter++; - } - } - } - - // Build the system COO Jacobian - LinearAlgebra::CooMatrix jac(size_, size_, nnz_dup, &rows_dup, &cols_dup, &vals_dup); - - // Populate CSR data with sort and deduplicate - IdxT* row_ptrs = jac.getCsrRowData(); - - // Deduplicated nnz - nnz_ = jac.getNnz(); - - // Allocate cols/vals with deduplicated nnz - IdxT* cols = new IdxT[nnz_]; - RealT* vals = new RealT[nnz_]; - - std::copy(jac.getColData(), jac.getColData() + nnz_, cols); - std::copy(jac.getValues(), jac.getValues() + nnz_, vals); - - // Create the CSR Jacobian - csr_jac_ = new CsrMatrixT(size_, size_, nnz_, &row_ptrs, &cols, &vals); - - const IdxT* map_to_sorted = jac.getMapToSorted(); - const IdxT* map_to_dedup = jac.getMapToDeduplicated(); - - // Build a mappping from original COO index to CSR index - map_to_csr_ = new IdxT[nnz_dup]; - for (IdxT i = 0; i < nnz_dup; ++i) - { - map_to_csr_[map_to_sorted[i]] = map_to_dedup[i]; - } - - allocated_ = true; - return 0; - } - - /** - * @brief Set intial y and y' of each component - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int initialize() override - { - // Initialize components - for (const auto& component : components_) - { - component->initialize(); - } - - return CircuitComponent::initialize(); - } - - /** - * @brief Tags all system variables as differentiable, based on what the - * components that own those variables tag them as. - * - * Starts by asking all components to tag their differentiables. This implementation - * assumes all node variables are algebraic, and will not ask nodes to tag their differentiables. - * Sets all variables to algebraic (`false`) to start, then loops over all component internal variables. - * Re-creates the same internal variable to system variables mapping as in \ref allocate() - all - * internal variables from the same component are stored contiguously in a block, and blocks are - * stored contiguously in the same order as \ref components_, with node variables at the end. - * Each internal variable's tag in the system is set to its tag in the component. - */ - int tagDifferentiable() override - { - // Ask all component to tag their differentiables - for (size_t i = 0; i < components_.size(); i++) - { - component_type* component = components_[i]; - - // Bubble up errors if necessary - if (int err = component->tagDifferentiable()) - { - return err; - } - } - - // Fill tags with a default value (false) for node variables. Assumed to be algebraic here. - std::fill(tag_.begin(), tag_.end(), false); - - // Copy tags for internal variables from their components - going in the order as described above - size_t idx = 0; - for (component_type* comp : components_) - { - const auto& external_indices = comp->getExternIndices(); - - // Loop over all component variables - including externals - for (IdxT i = 0; i < comp->size(); i++) - { - // Discard externals - if (!external_indices.contains(i)) - { - tag_[idx] = comp->tag()[i]; - - // Ensures internal variables are contiguous, and in the same order as the component - idx++; - } - } - } - - return 0; - } - - /** - * @brief Compute the absolute tolerance for each variable in the model - * - * @param rel_tol The relative tolerance which can be used to pick the - * absolute tolerance. - * @tparam ScalarT Scalar data type - * @tparam IdxT Index data type - * @return int 0 if successful, non-zero otherwise. - * - * This represents a "noise" level close to zero for which pure relative - * error cannot be used. - */ - int setAbsoluteTolerance(RealT rel_tol) final - { - abs_tol_.setToConst(static_cast(rel_tol)); - return 0; - } - - /** - * @brief Evaluate Residuals at each component then collect them - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int evaluateInternalResidual() override - { - for (IdxT i = 0; i < size_; i++) - { - f_int_[i] = 0.0; - } - - // Update system residual vector - - // Evaluate component internal residuals - this is embarassingly parallel - for (component_type* component : components_) - { - if (int err_code = component->evaluateInternalResidual()) - return err_code; - } - - for (component_type* component : components_) - { - if (int err_code = component->evaluateExternalResidual()) - return err_code; - } - - return 0; - } - - /** - * @todo implement this for nested systems - */ - int evaluateExternalResidual() final - { - return 0; - } - - /** - * @brief Creates the system Jacobian representing \f$\alpha dF/dy' + dF/dy\f$ - * - * Updates the CSR Jacobian values using the per-component mappings - * computed during allocate(). - * - * @return int 0 if successful, positive if there's a recoverable error, negative if unrecoverable - */ - int evaluateJacobian() override - { - // Zero out values - RealT* vals = csr_jac_->getValues(); - for (IdxT i = 0; i < csr_jac_->getNnz(); ++i) - { - vals[i] = 0.0; - } - - // Update CSR values from component Jacobians - IdxT counter = 0; - for (const auto& component : components_) - { - component->evaluateJacobian(); - - const IdxT* r = component->jacobianCooRows(); - const IdxT* c = component->jacobianCooCols(); - const RealT* v = component->jacobianCooValues(); - IdxT nnz = component->nnz(); - - for (IdxT i = 0; i < nnz; ++i) - { - const IdxT row = component->getNodeConnection(r[i]); - const IdxT col = component->getNodeConnection(c[i]); - - const bool is_internal_entry = row != INVALID_INDEX && col != INVALID_INDEX - && row < this->getInternalSize() && col < this->getInternalSize(); - - if (is_internal_entry) - { - vals[map_to_csr_[counter]] += v[i]; - ++counter; - } - } - } - - jac_call_count_++; - return 0; - } - - /** - * @brief Evaluate integrands for the system quadratures. - */ - int evaluateIntegrand() final - { - return 0; - } - - /** - * @brief Initialize system adjoint. - * - * Updates variables and optimization parameters, then initializes - * adjoints locally and copies them to the system adjoint vector. - */ - int initializeAdjoint() final - { - return 0; - } - - /** - * @brief Compute adjoint residual for the system model. - * - * - */ - int evaluateAdjointResidual() final - { - return 0; - } - - /** - * @brief Evaluate adjoint integrand for the system model. - * - * - */ - int evaluateAdjointIntegrand() final - { - return 0; - } - - /** - * @brief Distribute time and time scaling for each component - * - * @param t - * @param a - */ - void updateTime(RealT t, RealT a) final - { - for (const auto& component : components_) - { - component->updateTime(t, a); - } - time_ = t; - alpha_ = a; - } - - CsrMatrixT* getCsrJacobian() const override - { - return csr_jac_; - } - - void addComponent(component_type* component) - { - components_.push_back(component); - allocated_ = false; - } - - void addNode(node_type* node) - { - nodes_.push_back(node); - allocated_ = false; - } - - protected: - static constexpr IdxT neg1_ = INVALID_INDEX; - - std::vector components_; - std::vector nodes_; - - IdxT* map_to_csr_{nullptr}; - CsrMatrixT* csr_jac_{nullptr}; - - int jac_call_count_{0}; - bool use_jac_; - - }; // class PowerElectronicsModel - -} // namespace GridKit From 4f1fa56301f64af8de76301d49484428ae876e0b Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 30 Oct 2026 19:59:24 +0000 Subject: [PATCH 29/31] Update Interfaces --- .../BusLinePartitionInterface.cpp | 11 ++-- .../BusLinePartitionInterface.hpp | 52 +++++++++---------- .../PartitionInterface/PartitionInterface.hpp | 35 +++++++------ 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp index 6565aecd8e..dd085852f3 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp @@ -35,12 +35,17 @@ namespace GridKit { if (dynamic_cast*>(bus) == nullptr) { - throw std::invalid_argument("BusLinePartitionInterface requires a MicrogridBus."); + throw std::invalid_argument("BusLinePartitionInterface: Requires a MicrogridBus."); } if (dynamic_cast*>(component) == nullptr) { - throw std::invalid_argument("BusLinePartitionInterface requires a MicrogridLine."); + throw std::invalid_argument("BusLinePartitionInterface: Requires a MicrogridLine."); + } + + if (!bus->isAllocated() or !component->isAllocated()) + { + throw std::invalid_argument("BusLinePartitionInterface: Malformed network. Ensure components are allocated before partitioning."); } size_ = 4; @@ -79,7 +84,7 @@ namespace GridKit template int BusLinePartitionInterface::allocate() { - CircuitComponent::allocate(); + Component::allocate(); this->setConnectionNodes(0, connection_indices_[0]); this->setConnectionNodes(1, connection_indices_[1]); diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp index c151b1a207..1f386a263a 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp @@ -6,7 +6,7 @@ #pragma once -#include +#include #include #include @@ -44,34 +44,34 @@ namespace GridKit class BusLinePartitionInterface : public PartitionInterface { - using component_type = CircuitComponent; + using component_type = Component; using node_type = typename PowerElectronics::NodeBase; - using RealT = typename CircuitComponent::RealT; + using RealT = typename Component::RealT; - using CircuitComponent::size_; - using CircuitComponent::nnz_; - using CircuitComponent::time_; - using CircuitComponent::alpha_; - using CircuitComponent::y_ext_; - using CircuitComponent::y_int_; - using CircuitComponent::yp_ext_; - using CircuitComponent::yp_int_; - using CircuitComponent::tag_; - using CircuitComponent::abs_tol_; - using CircuitComponent::f_ext_; - using CircuitComponent::f_int_; - using CircuitComponent::g_; - using CircuitComponent::yB_; - using CircuitComponent::ypB_; - using CircuitComponent::fB_; - using CircuitComponent::gB_; - using CircuitComponent::param_; - using CircuitComponent::idc_; + using Component::size_; + using Component::nnz_; + using Component::time_; + using Component::alpha_; + using Component::y_ext_; + using Component::y_int_; + using Component::yp_ext_; + using Component::yp_int_; + using Component::tag_; + using Component::abs_tol_; + using Component::f_ext_; + using Component::f_int_; + using Component::g_; + using Component::yB_; + using Component::ypB_; + using Component::fB_; + using Component::gB_; + using Component::param_; + using Component::idc_; - using CircuitComponent::extern_indices_; - using CircuitComponent::n_extern_; - using CircuitComponent::n_intern_; - using CircuitComponent::connection_nodes_; + using Component::extern_indices_; + using Component::n_extern_; + using Component::n_intern_; + using Component::connection_nodes_; public: BusLinePartitionInterface(node_type* bus, component_type* component, IdxT id); diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp index e76930e649..adc82444e8 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp +++ b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp @@ -4,26 +4,29 @@ #include #include -#include +#include namespace GridKit { - /*! - * @brief Base class for partition interface components. - * - * A partition interface is simply a component that is designed to aid in - * marking partition boundary to facilitate partion evaluation and - * Jacobian assembly. All partition interface that will be implemented - * in the future should inherit from this class and all common features - * should be factored here. - */ - template - class PartitionInterface : public CircuitComponent + namespace PowerElectronics { - public: - PartitionInterface() = default; - ~PartitionInterface() = default; - }; + /*! + * @brief Base class for partition interface components. + * + * A partition interface is simply a component that is designed to aid in + * marking partition boundary to facilitate partion evaluation and + * Jacobian assembly. All partition interface that will be implemented + * in the future should inherit from this class and all common features + * should be factored here. + */ + template + class PartitionInterface : public Component + { + public: + PartitionInterface() = default; + ~PartitionInterface() = default; + }; + } // namespace PowerElectronics } // namespace GridKit From cde9d746ac9047c4e8e36f9bf09782ea6f453b52 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 30 Oct 2026 19:59:49 +0000 Subject: [PATCH 30/31] Update tests --- .../PartitionPowerElectronicsTests.hpp | 619 +++++++++--------- .../runPartitionPowerElectronicsTests.cpp | 4 +- 2 files changed, 324 insertions(+), 299 deletions(-) diff --git a/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp index fe320f8644..0de4ce2b43 100644 --- a/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp +++ b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp @@ -12,386 +12,411 @@ #include #include +#include #include #include #include #include #include #include -#include +#include #include #include +/* + * + * This network is partitioned into two subsystems, and the partitioned residuals and Jacobians + * are compared against those of the monolithic system. + * + * Signal Node + * | + * (shared signal) + * + * DG1 DG2 + * | | + * | | + * Bus1 -------- Line -------- Bus2 + * | | + * | | + * Load1 Load2 + * + * The partition boundary is located between the transmission line and Bus2. + * + */ namespace GridKit { - namespace Testing + namespace PowerElectronics { - template - class PartitionPowerElectronicsTests + namespace Testing { - using RealT = typename CircuitComponent::RealT; - using System = PowerElectronicsModel; - using Subsystem = SubsystemModel; - - using Bus = PowerElectronics::MicrogridBus; - using Signal = PowerElectronics::SignalNode; - using Generator = DistributedGenerator; - using BusDQ = MicrogridBusDQ; - using Line = MicrogridLine; - using Load = MicrogridLoad; - - using Parameters = DistributedGeneratorParameters; - using Interface = PowerElectronics::BusLinePartitionInterface; - - public: - PartitionPowerElectronicsTests() + template + class PartitionPowerElectronicsTests { - createComponents(); - assembleMonolithic(); - saveReferenceResidual(); - saveReferenceJacobian(); - assemblePartitions(); - distributeVariables(); - } - - /** - * @brief Verify sequential partition residuals against - * the saved monolithic reference. - */ - TestOutcome residual() - { - const auto partition_residual = evaluatePartitions(false); - - return compareResiduals(partition_residual, reference_residual_).report(__func__); - } + using TestOutcome = GridKit::Testing::TestOutcome; + using TestStatus = GridKit::Testing::TestStatus; + using RealT = typename Component::RealT; + using System = SystemModel; + using Subsystem = SubsystemModel; + + using Bus = MicrogridBus; + using Signal = SignalNode; + using Generator = DistributedGenerator; + using BusDQ = MicrogridBusDQ; + using Line = MicrogridLine; + using Load = MicrogridLoad; + + using Parameters = DistributedGeneratorParameters; + using Interface = BusLinePartitionInterface; + + public: + PartitionPowerElectronicsTests() + { + createComponents(); + assembleMonolithic(); + saveReferenceResidual(); + saveReferenceJacobian(); + assemblePartitions(); + distributeVariables(); + } - /** - * @brief Verify that parallel partition evaluation produces - * the same result as sequential evaluation. - */ - TestOutcome parallelResidual() - { - const auto sequential = evaluatePartitions(false); - const auto parallel = evaluatePartitions(true); + /** + * @brief Verify sequential partition residuals against + * the saved monolithic reference. + */ + TestOutcome residual() + { + const auto partition_residual = evaluatePartitions(false); - TestStatus success = true; + return compareResiduals(partition_residual, reference_residual_).report(__func__); + } - success *= compareResiduals(parallel, sequential); - success *= compareResiduals(parallel, reference_residual_); + /** + * @brief Verify that parallel partition evaluation produces + * the same result as sequential evaluation. + */ + TestOutcome parallelResidual() + { + const auto sequential = evaluatePartitions(false); + const auto parallel = evaluatePartitions(true); - return success.report(__func__); - } + TestStatus success = true; - /** - * @brief Verify that the subsystem Jacobians match the corresponding - * blocks of the monolithic Jacobian. - */ - TestOutcome jacobian() - { - TestStatus success = true; + success *= compareResiduals(parallel, sequential); + success *= compareResiduals(parallel, reference_residual_); - const RealT tolerance = 100 * std::numeric_limits::epsilon(); + return success.report(__func__); + } - for (auto* partition : partitions_) + /** + * @brief Verify that the subsystem Jacobians match the corresponding + * blocks of the monolithic Jacobian. + */ + TestOutcome jacobian() { - partition->updateTime(1.0, 2.0); - partition->evaluateJacobian(); + TestStatus success = true; - const auto jac_map = Testing::MapFromCsr(partition->getCsrJacobian()); - const auto partition_size = partition->getInternalSize(); + const RealT tolerance = 100 * std::numeric_limits::epsilon(); - for (size_t row = 0; row < partition_size; ++row) + for (auto* partition : partitions_) { - const auto global_row = partition->getNodeConnection(row); + partition->updateTime(1.0, 2.0); + partition->evaluateJacobian(); + + const auto jac_map = GridKit::Testing::MapFromCsr(partition->getCsrJacobian()); + const auto partition_size = partition->getInternalSize(); - for (size_t col = 0; col < partition_size; ++col) + for (size_t row = 0; row < partition_size; ++row) { - const auto global_col = partition->getNodeConnection(col); + const auto global_row = partition->getNodeConnection(row); + + for (size_t col = 0; col < partition_size; ++col) + { + const auto global_col = partition->getNodeConnection(col); - const auto& partition_row = jac_map[row]; - const auto& reference_row = reference_jacobian_[global_row]; + const auto& partition_row = jac_map[row]; + const auto& reference_row = reference_jacobian_[global_row]; - const RealT partition_value = partition_row.contains(col) ? partition_row.at(col) : RealT{0}; - const RealT reference_value = reference_row.contains(global_col) ? reference_row.at(global_col) : RealT{0}; + const RealT partition_value = partition_row.contains(col) ? partition_row.at(col) : RealT{0}; + const RealT reference_value = reference_row.contains(global_col) ? reference_row.at(global_col) : RealT{0}; - success *= Testing::isEqual(partition_value, reference_value, tolerance); + success *= GridKit::Testing::isEqual(partition_value, reference_value, tolerance); + } } } + + return success.report(__func__); } - return success.report(__func__); - } + private: + /** + * @brief Construct the physical microgrid components. + */ + void createComponents() + { + constexpr RealT pi = std::numbers::pi_v; + + Parameters parms{}; + + parms.wb_ = 2.0 * pi * 50.0; + parms.wc_ = 31.41; + parms.mp_ = 9.4e-5; + parms.Vn_ = 380.0; + parms.nq_ = 1.3e-3; + parms.F_ = 0.75; + parms.Kiv_ = 420.0; + parms.Kpv_ = 0.1; + parms.Kic_ = 2.0e4; + parms.Kpc_ = 15.0; + parms.Cf_ = 5.0e-5; + parms.rLf_ = 0.1; + parms.Lf_ = 1.35e-3; + parms.rLc_ = 0.03; + parms.Lc_ = 0.35e-3; + + dg1_ = new Generator(1, parms, true, &signal_, &bus1_); + dg2_ = new Generator(2, parms, false, &signal_, &bus2_); + + line_ = new Line(3, 0.23, 0.1 / (2.0 * pi * 50.0), &signal_, &bus1_, &bus2_); + + load1_ = new Load(4, 3.0, 2.0 / (2.0 * pi * 50.0), &signal_, &bus1_); + load2_ = new Load(5, 2.0, 1.0 / (2.0 * pi * 50.0), &signal_, &bus2_); + + busDQ1_ = new BusDQ(6, 1.0e4, &bus1_); + busDQ2_ = new BusDQ(7, 1.0e4, &bus2_); + } - private: - /** - * @brief Construct the physical microgrid components. - */ - void createComponents() - { - constexpr RealT pi = std::numbers::pi_v; - - Parameters parms{}; - - parms.wb_ = 2.0 * pi * 50.0; - parms.wc_ = 31.41; - parms.mp_ = 9.4e-5; - parms.Vn_ = 380.0; - parms.nq_ = 1.3e-3; - parms.F_ = 0.75; - parms.Kiv_ = 420.0; - parms.Kpv_ = 0.1; - parms.Kic_ = 2.0e4; - parms.Kpc_ = 15.0; - parms.Cf_ = 5.0e-5; - parms.rLf_ = 0.1; - parms.Lf_ = 1.35e-3; - parms.rLc_ = 0.03; - parms.Lc_ = 0.35e-3; - - dg1_ = new Generator(1, parms, true, &signal_, &bus1_); - dg2_ = new Generator(2, parms, false, &signal_, &bus2_); - - line_ = new Line(3, 0.23, 0.1 / (2.0 * pi * 50.0), &signal_, &bus1_, &bus2_); - - load1_ = new Load(4, 3.0, 2.0 / (2.0 * pi * 50.0), &signal_, &bus1_); - load2_ = new Load(5, 2.0, 1.0 / (2.0 * pi * 50.0), &signal_, &bus2_); - - busDQ1_ = new BusDQ(6, 1.0e4, &bus1_); - busDQ2_ = new BusDQ(7, 1.0e4, &bus2_); - } - - /** - * @brief Assemble the monolithic reference system. - */ - void assembleMonolithic() - { - system_ = std::make_unique(); + /** + * @brief Assemble the monolithic reference system. + */ + void assembleMonolithic() + { + system_ = std::make_unique(true); - system_->addComponent(dg1_); - system_->addComponent(load1_); - system_->addComponent(busDQ1_); - system_->addComponent(line_); - system_->addComponent(dg2_); - system_->addComponent(load2_); - system_->addComponent(busDQ2_); + system_->addComponent(dg1_); + system_->addComponent(load1_); + system_->addComponent(busDQ1_); + system_->addComponent(line_); + system_->addComponent(dg2_); + system_->addComponent(load2_); + system_->addComponent(busDQ2_); - system_->addNode(&signal_); - system_->addNode(&bus1_); - system_->addNode(&bus2_); + system_->addNode(&signal_); + system_->addNode(&bus1_); + system_->addNode(&bus2_); - system_->allocate(); + system_->allocate(); - y_.resize(system_->size()); - yp_.resize(system_->size()); + y_.resize(system_->size()); + yp_.resize(system_->size()); - for (size_t i = 0; i < system_->size(); ++i) - { - const auto index = static_cast(i + 1); + for (size_t i = 0; i < system_->size(); ++i) + { + const auto index = static_cast(i + 1); - y_[i] = static_cast(0.01 * index); - yp_[i] = static_cast(0.001 * index); + y_[i] = static_cast(0.01 * index); + yp_[i] = static_cast(0.001 * index); + } } - } - - /** - * @brief Evaluate the monolithic residual exactly once, - * before constructing the partitions. - */ - void saveReferenceResidual() - { - auto* system_y = system_->y().getData(); - auto* system_yp = system_->yp().getData(); - for (size_t i = 0; i < system_->size(); ++i) + /** + * @brief Evaluate the monolithic residual exactly once, + * before constructing the partitions. + */ + void saveReferenceResidual() { - system_y[i] = y_[i]; - system_yp[i] = yp_[i]; - } + auto* system_y = system_->y().getData(); + auto* system_yp = system_->yp().getData(); - system_->y().setDataUpdated(); - system_->yp().setDataUpdated(); + for (size_t i = 0; i < system_->size(); ++i) + { + system_y[i] = y_[i]; + system_yp[i] = yp_[i]; + } - system_->updateTime(1.0, 2.0); - system_->evaluateResidual(); + system_->y().setDataUpdated(); + system_->yp().setDataUpdated(); - const auto* residual = system_->getResidual().getData(); + system_->updateTime(1.0, 2.0); + system_->evaluateResidual(); - reference_residual_.assign(residual, residual + system_->size()); - } + const auto* residual = system_->getResidual().getData(); - /** - * @brief Save the monolithic Jacobian before partitioning. - */ - void saveReferenceJacobian() - { - system_->updateTime(1.0, 2.0); - system_->evaluateJacobian(); + reference_residual_.assign(residual, residual + system_->size()); + } - reference_jacobian_ = Testing::MapFromCsr(system_->getCsrJacobian()); - } + /** + * @brief Save the monolithic Jacobian before partitioning. + */ + void saveReferenceJacobian() + { + system_->updateTime(1.0, 2.0); + system_->evaluateJacobian(); - /** - * @brief Assemble the two subsystem partitions. - */ - void assemblePartitions() - { - partition1_ = std::make_unique(); - partition2_ = std::make_unique(); + reference_jacobian_ = GridKit::Testing::MapFromCsr(system_->getCsrJacobian()); + } - // Boundary between the line and Bus2. - interface_ = new Interface(&bus2_, line_, 8); - interface_->allocate(); + /** + * @brief Assemble the two subsystem partitions. + */ + void assemblePartitions() + { + partition1_ = std::make_unique(); + partition2_ = std::make_unique(); - // Partition 1: DG1, Load1, Bus1, Line - partition1_->addComponent(dg1_); - partition1_->addComponent(load1_); - partition1_->addComponent(busDQ1_); - partition1_->addComponent(line_); + // Boundary between the line and Bus2. + interface_ = new Interface(&bus2_, line_, 8); + interface_->allocate(); - partition1_->addNode(&signal_); - partition1_->addNode(&bus1_); + // Partition 1: DG1, Load1, Bus1, Line + partition1_->addComponent(dg1_); + partition1_->addComponent(load1_); + partition1_->addComponent(busDQ1_); + partition1_->addComponent(line_); - // Partition 2: DG2, Load2, Bus2 - partition2_->addComponent(dg2_); - partition2_->addComponent(load2_); - partition2_->addComponent(busDQ2_); - partition2_->addInterface(interface_); + partition1_->addNode(&signal_); + partition1_->addNode(&bus1_); - partition2_->addNode(&bus2_); + // Partition 2: DG2, Load2, Bus2 + partition2_->addComponent(dg2_); + partition2_->addComponent(load2_); + partition2_->addComponent(busDQ2_); + partition2_->addInterface(interface_); - partition1_->allocate(); - partition2_->allocate(); + partition2_->addNode(&bus2_); - partitions_ = {partition1_.get(), partition2_.get()}; - } + partition1_->allocate(); + partition2_->allocate(); - /** - * @brief Distribute global states to the partitions. - */ - void distributeVariables() - { - for (auto* partition : partitions_) + partitions_ = {partition1_.get(), partition2_.get()}; + } + + /** + * @brief Distribute global states to the partitions. + */ + void distributeVariables() { - auto forcing = [this, partition](ScalarT) -> void + for (auto* partition : partitions_) { - auto* y_ext = partition->getExternalDataY().getData(); - auto* yp_ext = partition->getExternalDataYP().getData(); - - const auto& indices = partition->getExternalDataIndices(); - - for (size_t i = 0; i < partition->getExternSize(); ++i) + auto forcing = [this, partition](ScalarT) -> void { - y_ext[i] = y_[indices[i]]; - yp_ext[i] = yp_[indices[i]]; - } - }; + auto* y_ext = partition->getExternalDataY().getData(); + auto* yp_ext = partition->getExternalDataYP().getData(); - partition->setForcingFunction(std::move(forcing)); + const auto& indices = partition->getExternalDataIndices(); - auto* y = partition->y().getData(); - auto* yp = partition->yp().getData(); + for (size_t i = 0; i < partition->getExternSize(); ++i) + { + y_ext[i] = y_[indices[i]]; + yp_ext[i] = yp_[indices[i]]; + } + }; - for (size_t i = 0; i < partition->getInternalSize(); ++i) - { - const auto index = partition->getNodeConnection(i); + partition->setForcingFunction(std::move(forcing)); - y[i] = y_[index]; - yp[i] = yp_[index]; - } + auto* y = partition->y().getData(); + auto* yp = partition->yp().getData(); - partition->y().setDataUpdated(); - partition->yp().setDataUpdated(); - } - } - - /** - * @brief Evaluate partitions and reconstruct the global residual. - * - * @param parallel Enable OpenMP parallel evaluation. - */ - std::vector evaluatePartitions([[maybe_unused]] bool parallel) - { - const int num_partitions = static_cast(partitions_.size()); + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + const auto index = partition->getNodeConnection(i); -#ifdef _OPENMP -#pragma omp parallel for if (parallel) schedule(static) -#endif - for (int p = 0; p < num_partitions; ++p) - { - auto* partition = partitions_[p]; + y[i] = y_[index]; + yp[i] = yp_[index]; + } - partition->updateTime(1.0, 2.0); - partition->evaluateResidual(); + partition->y().setDataUpdated(); + partition->yp().setDataUpdated(); + } } - std::vector residual(reference_residual_.size(), 0.0); - - for (auto* partition : partitions_) + /** + * @brief Evaluate partitions and reconstruct the global residual. + * + * @param parallel Enable OpenMP parallel evaluation. + */ + std::vector evaluatePartitions([[maybe_unused]] bool parallel) { - const auto* f_values = partition->getResidual().getData(); + const int num_partitions = static_cast(partitions_.size()); - for (size_t i = 0; i < partition->getInternalSize(); ++i) +#ifdef _OPENMP +#pragma omp parallel for if (parallel) schedule(static) +#endif + for (int p = 0; p < num_partitions; ++p) { - const auto index = partition->getNodeConnection(i); - residual[index] = f_values[i]; + auto* partition = partitions_[p]; + + partition->updateTime(1.0, 2.0); + partition->evaluateResidual(); } - } - return residual; - } + std::vector residual(reference_residual_.size(), 0.0); - /** - * @brief Compare two residual vectors. - */ - TestStatus compareResiduals( - const std::vector& actual, - const std::vector& expected) const - { - TestStatus success = true; + for (auto* partition : partitions_) + { + const auto* f_values = partition->getResidual().getData(); - const RealT tolerance = 100 * std::numeric_limits::epsilon(); + for (size_t i = 0; i < partition->getInternalSize(); ++i) + { + const auto index = partition->getNodeConnection(i); + residual[index] = f_values[i]; + } + } - for (size_t i = 0; i < expected.size(); ++i) - { - success *= Testing::isEqual(actual[i], expected[i], tolerance); + return residual; } - return success; - } - - // nodes - Signal signal_; - Bus bus1_; - Bus bus2_; - - // components - Generator* dg1_ = nullptr; - Generator* dg2_ = nullptr; - Line* line_ = nullptr; - Load* load1_ = nullptr; - Load* load2_ = nullptr; - BusDQ* busDQ1_ = nullptr; - BusDQ* busDQ2_ = nullptr; - - // Partition interface - Interface* interface_ = nullptr; - - // Reference system - std::unique_ptr system_; - - // Partitioned system - std::unique_ptr partition1_; - std::unique_ptr partition2_; - std::vector partitions_; + /** + * @brief Compare two residual vectors. + */ + TestStatus compareResiduals( + const std::vector& actual, + const std::vector& expected) const + { + TestStatus success = true; - // Test data - std::vector y_; - std::vector yp_; - std::vector reference_residual_; + const RealT tolerance = 100 * std::numeric_limits::epsilon(); - std::vector reference_jacobian_; - }; + for (size_t i = 0; i < expected.size(); ++i) + { + success *= GridKit::Testing::isEqual(actual[i], expected[i], tolerance); + } - } // namespace Testing + return success; + } + // nodes + Signal signal_; + Bus bus1_; + Bus bus2_; + + // components + Generator* dg1_ = nullptr; + Generator* dg2_ = nullptr; + Line* line_ = nullptr; + Load* load1_ = nullptr; + Load* load2_ = nullptr; + BusDQ* busDQ1_ = nullptr; + BusDQ* busDQ2_ = nullptr; + + // Partition interface + Interface* interface_ = nullptr; + + // Reference system + std::unique_ptr system_; + + // Partitioned system + std::unique_ptr partition1_; + std::unique_ptr partition2_; + std::vector partitions_; + + // Test data + std::vector y_; + std::vector yp_; + std::vector reference_residual_; + + std::vector reference_jacobian_; + }; + + } // namespace Testing + } // namespace PowerElectronics } // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp index 3e4faeb944..1bac1b098e 100644 --- a/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp +++ b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp @@ -5,8 +5,8 @@ int main() { - GridKit::Testing::TestingResults result; - GridKit::Testing::PartitionPowerElectronicsTests test; + GridKit::Testing::TestingResults result; + GridKit::PowerElectronics::Testing::PartitionPowerElectronicsTests test; result += test.residual(); result += test.jacobian(); From c96a6137e6158f40791412177e9ab7c1b6a39c35 Mon Sep 17 00:00:00 2001 From: abdourahmanbarry Date: Fri, 30 Oct 2026 20:02:14 +0000 Subject: [PATCH 31/31] Apply pre-commit fixes --- .../PartitionInterface/CMakeLists.txt | 7 +++---- .../Model/PowerElectronics/SubsystemModel.hpp | 2 +- .../UnitTests/PowerElectronics/CMakeLists.txt | 20 +++++++++---------- .../runPartitionPowerElectronicsTests.cpp | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt index 0cb50865b3..7c61862316 100644 --- a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -4,7 +4,6 @@ gridkit_add_library( HEADERS BusLinePartitionInterface.hpp PartitionInterface.hpp LINK_LIBRARIES PUBLIC - GridKit::dense_vector - GridKit::utilities_logger - GridKit::power_elec_microline -) + GridKit::dense_vector + GridKit::utilities_logger + GridKit::power_elec_microline) diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp index e2319fb9de..114eb35dce 100644 --- a/GridKit/Model/PowerElectronics/SubsystemModel.hpp +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -759,4 +759,4 @@ namespace GridKit }; // class SubsystemModel } // namespace PowerElectronics -} // namespace GridKit \ No newline at end of file +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index 31e4e4727d..a0757d0ef4 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -6,15 +6,14 @@ target_link_libraries( add_executable(test_partition_power_electronics runPartitionPowerElectronicsTests.cpp) target_link_libraries( test_partition_power_electronics - PRIVATE - GridKit::power_elec_bus_line_interface - GridKit::power_elec_disgen - GridKit::power_elec_microline - GridKit::power_elec_microload - GridKit::power_elec_microbusdq - GridKit::testing - GridKit::sparse_matrix - GridKit::Utilities) + PRIVATE GridKit::power_elec_bus_line_interface + GridKit::power_elec_disgen + GridKit::power_elec_microline + GridKit::power_elec_microload + GridKit::power_elec_microbusdq + GridKit::testing + GridKit::sparse_matrix + GridKit::Utilities) add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( @@ -27,7 +26,8 @@ target_link_libraries( add_test(NAME PowerElectronicsNodeTest COMMAND $) add_test(NAME PartitionPowerElectronicsTest COMMAND $) -add_test(NAME PowerElectronicsComponentCloneTest COMMAND $) +add_test(NAME PowerElectronicsComponentCloneTest + COMMAND $) install(TARGETS test_power_electronics_node RUNTIME DESTINATION bin) install(TARGETS test_partition_power_electronics RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp index 1bac1b098e..eb293fae57 100644 --- a/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp +++ b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp @@ -13,4 +13,4 @@ int main() result += test.parallelResidual(); return result.summary(); -} \ No newline at end of file +}