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/PartitionInterface/BusLinePartitionInterface.cpp b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp new file mode 100644 index 0000000000..dd085852f3 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.cpp @@ -0,0 +1,213 @@ + +#include "BusLinePartitionInterface.hpp" + +#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. + * + * @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) + { + 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."); + } + + if (!bus->isAllocated() or !component->isAllocated()) + { + throw std::invalid_argument("BusLinePartitionInterface: Malformed network. Ensure components are allocated before partitioning."); + } + + size_ = 4; + n_intern_ = 0; + n_extern_ = 4; + idc_ = id; + extern_indices_ = {0, 1, 2, 3}; + nnz_ = 2; + + // Global indices associated with the local bus. + auto bus_idx_i = bus->getNodeConnection(0).idx_; + auto bus_idx_j = bus->getNodeConnection(1).idx_; + + 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); + + if (is_left_connection == is_right_connection) + { + 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())); + } + + sign_ = is_left_connection ? -1.0 : 1.0; + connection_indices_.resize(static_cast(size_)); + + 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); + } + + template + int BusLinePartitionInterface::allocate() + { + Component::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; + } + + 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() + { + *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(); + + // Jacobian entries corresponding to the two transferred line contributions. + std::vector rtemp{0, 1}; + std::vector ctemp{2, 3}; + std::vector valtemp{sign_, sign_}; + + 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..1f386a263a --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/BusLinePartitionInterface.hpp @@ -0,0 +1,109 @@ +/** + * @file BusLinePartitionInterface.hpp + * @author Abdourahman Barry (abdourahman@vt.edu) + * + */ + +#pragma once + +#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 = Component; + using node_type = typename PowerElectronics::NodeBase; + using RealT = typename Component::RealT; + + 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 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); + ~BusLinePartitionInterface() override = default; + + 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 Global connection indices for the bus voltages and line currents + * across the partition boundary. + */ + std::vector connection_indices_; + + /** + * @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. + */ + RealT sign_; + }; + } // namespace PowerElectronics +} // namespace GridKit diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt new file mode 100644 index 0000000000..7c61862316 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/CMakeLists.txt @@ -0,0 +1,9 @@ +gridkit_add_library( + power_elec_bus_line_interface + SOURCES BusLinePartitionInterface.cpp + HEADERS BusLinePartitionInterface.hpp PartitionInterface.hpp + LINK_LIBRARIES + PUBLIC + GridKit::dense_vector + GridKit::utilities_logger + GridKit::power_elec_microline) diff --git a/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp new file mode 100644 index 0000000000..adc82444e8 --- /dev/null +++ b/GridKit/Model/PowerElectronics/PartitionInterface/PartitionInterface.hpp @@ -0,0 +1,32 @@ + + +#pragma once + +#include +#include +#include + +namespace GridKit +{ + namespace PowerElectronics + { + + /*! + * @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 diff --git a/GridKit/Model/PowerElectronics/SubsystemModel.hpp b/GridKit/Model/PowerElectronics/SubsystemModel.hpp new file mode 100644 index 0000000000..114eb35dce --- /dev/null +++ b/GridKit/Model/PowerElectronics/SubsystemModel.hpp @@ -0,0 +1,762 @@ +/** + * @file SubsystemModel.hpp + * @author Abdourahman Barry (abdourahman@vt.edu) + * + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace PowerElectronics + { + /** + * @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 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) + { + } + + /** + * @brief Default constructor for the system model + * + * @post System model parameters set as default + */ + + SubsystemModel(bool use_jac) + : System(use_jac) + { + } + + ~SubsystemModel() override + { + restoreGlobalConnection(); + + 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 (this->isAllocated()) + { + if (int err_code = restoreGlobalConnection()) + { + return err_code; + } + } + + if (int err_code = buildConnectionMap()) + { + 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_)); + + // 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_); + allocateVectors(static_cast(size_), true); // Allocate internal subsystem vectors. + + tag_.resize(size_); + + // 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); + } + + // 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++; + } + + // 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); + + 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); + } + } + + 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 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]); + + component_internal_idx += comp->getInternalSize(); + + 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); + + 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()); + + 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); + } + } + } + + // 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 == INVALID_INDEX || col == INVALID_INDEX) + { + return false; + } + + 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); + }; + + 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]; + + 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) + { + const IdxT row = component->getNodeConnection(r[i]); + const IdxT col = component->getNodeConnection(c[i]); + + if (!isValidEntry(row, col)) + { + continue; + } + + rows_dup[index] = row; + cols_dup[index] = col; + vals_dup[index] = v[i]; + + ++index; + } + } + + // 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, 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() + { + + if (forcing_function_) + { + (*forcing_function_)(time_); + + y_ext_data_.setDataUpdated(); + yp_ext_data_.setDataUpdated(); + } + + f_ext_data_.setToZero(); + + 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 System::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 System::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 (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."); + } + + System::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 (this->isAllocated()) + { + throw std::logic_error("SubsystemModel::addNode: cannot add a node to an allocated subsystem."); + } + + if (!node->isAllocated()) + { + throw std::logic_error( + "SubsystemModel::addNode: cannot add an unallocated node."); + } + + System::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 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; + } + + global_to_local_map_.clear(); + + allocated_ = false; + + return 0; + } + + const std::vector& getExternalDataIndices() const + { + return external_data_indices_; + } + + VectorT& getExternalDataY() + { + return y_ext_data_; + } + + VectorT& getExternalDataYP() + { + return yp_ext_data_; + } + + VectorT& getExternalDataF() + { + return f_ext_data_; + } + + void setForcingFunction(TimeFunction function) + { + forcing_function_ = std::move(function); + } + + const std::unordered_map& getInternalMap() const + { + return global_to_local_map_; + } + + 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()) + { + return 0; + } + + IdxT local_connection; + + for (component_type* component : components_) + { + for (IdxT i = 0; i < component->size(); i++) + { + local_connection = component->getNodeConnection(i); + + if (local_connection != INVALID_INDEX) + { + component->setConnectionNodes(i, this->getNodeConnection(local_connection)); + } + } + } + + for (node_type* node : nodes_) + { + for (IdxT i = 0; i < node->size(); i++) + { + local_connection = node->getNodeConnection(i).idx_; + + if (local_connection != INVALID_INDEX) + { + node->setConnectionNodes(i, this->getNodeConnection(local_connection)); + } + } + } + + 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(); + + // 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 global_connection = comp->getNodeConnection(i); + + if (global_connection != INVALID_INDEX && !extern_indices.contains(i)) + { + global_to_local_map_[global_connection] = 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++) + { + const IdxT global_connection = node->getNodeConnection(i).idx_; + + if (global_connection != INVALID_INDEX) + { + global_to_local_map_[global_connection] = local_connection++; + } + } + } + + // 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)) + { + const IdxT global_connection = comp->getNodeConnection(j); + + 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_ = internal_size; + + 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 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/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index dc3c167371..a0757d0ef4 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -3,6 +3,18 @@ target_link_libraries( test_power_electronics_node PRIVATE GridKit::power_electronics_circuit_node GridKit::testing) +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) + add_executable(test_power_electronics_component_clone runComponentCloneTests.cpp) target_link_libraries( test_power_electronics_component_clone @@ -13,8 +25,10 @@ target_link_libraries( GridKit::testing) add_test(NAME PowerElectronicsNodeTest COMMAND $) +add_test(NAME PartitionPowerElectronicsTest COMMAND $) add_test(NAME PowerElectronicsComponentCloneTest COMMAND $) install(TARGETS test_power_electronics_node RUNTIME DESTINATION bin) +install(TARGETS test_partition_power_electronics RUNTIME DESTINATION bin) install(TARGETS test_power_electronics_component_clone RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp new file mode 100644 index 0000000000..0de4ce2b43 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/PartitionPowerElectronicsTests.hpp @@ -0,0 +1,422 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#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 PowerElectronics + { + namespace Testing + { + template + class PartitionPowerElectronicsTests + { + 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 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 = GridKit::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 *= GridKit::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(true); + + 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_ = GridKit::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 *= GridKit::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 PowerElectronics +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/README.md b/tests/UnitTests/PowerElectronics/README.md new file mode 100644 index 0000000000..e5b4368b3b --- /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) ++\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) ++\left(0.7y_5\right) ++\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/runPartitionPowerElectronicsTests.cpp b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp new file mode 100644 index 0000000000..eb293fae57 --- /dev/null +++ b/tests/UnitTests/PowerElectronics/runPartitionPowerElectronicsTests.cpp @@ -0,0 +1,16 @@ +#include + +#include "PartitionPowerElectronicsTests.hpp" +#include + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::PowerElectronics::Testing::PartitionPowerElectronicsTests test; + + result += test.residual(); + result += test.jacobian(); + result += test.parallelResidual(); + + return result.summary(); +}