diff --git a/CHANGELOG.md b/CHANGELOG.md index a6dfeeae19..a63cd6b078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v.03 +- Added `BusSignalVoltageOut` bus model with voltage signal outlets and current signal inlets. +- Added `BusSignalVoltageIn` bus model with voltage signal inlets and current signal outlets. ## v0.2 diff --git a/GridKit/Model/PhasorDynamics/Bus/BusData.hpp b/GridKit/Model/PhasorDynamics/Bus/BusData.hpp index e28f0391d6..a3b43712ec 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusData.hpp +++ b/GridKit/Model/PhasorDynamics/Bus/BusData.hpp @@ -63,6 +63,8 @@ namespace GridKit INVALID, DEFAULT, SLACK, + SIGNAL_VOLTAGE_OUT, ///< Bus with voltage signal outputs and current signal inputs + SIGNAL_VOLTAGE_IN, ///< Bus with voltage signal inputs and current signal outputs }; BusType bus_type{BusType::INVALID}; ///< The kind of bus this data is for diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.cpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.cpp new file mode 100644 index 0000000000..39fbf40608 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.cpp @@ -0,0 +1,17 @@ +/** + * @file BusSignalVoltageIn.cpp + * @author Slaven Peles (peless@ornl.gov) + */ + +#include "BusSignalVoltageInImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + // Available template instantiations + template class BusSignalVoltageIn; + template class BusSignalVoltageIn; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp new file mode 100644 index 0000000000..1464014972 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp @@ -0,0 +1,175 @@ +/** + * @file BusSignalVoltageIn.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Declaration of a bus whose voltage is set by input signals. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /*! + * @brief Bus whose voltage is set by input signals. + * + * This is the mirror image of @ref BusSignalVoltageOut. The bus voltage + * components _Vr_ and _Vi_ are read directly from signal inlets + * `vr` and `vi` whenever Vr() or Vi() is called; the bus stores no + * voltage of its own and never modifies it. Both voltage inlets are + * mandatory: verify() logs an error and throws for an inlet that is not + * connected to a linked signal, and reading the voltage through an + * unlinked inlet throws. No default voltage is ever used. The bus has + * no unknowns and no equations (size() == 0, like @ref BusInfinite). + * Components attached to the bus add their current injections to Ir() + * and Ii(); the resulting sums are published on signal outlets `ir` + * and `ii`. + * + * @note Signal ports have to be connected before allocate() is called, since + * the output signals are linked there. + * + * @warning The current sums are complete only after all attached + * components have evaluated their residuals. A consumer of the + * `ir` and `ii` signals must be evaluated after them. + */ + template + class BusSignalVoltageIn : public BusBase + { + using BusBase::bus_id_; + using BusBase::size_; + using BusBase::nnz_; + using BusBase::variable_indices_; + using BusBase::residual_indices_; + using BusBase::coo_jac_; + using BusBase::monitor_; + using BusBase::allocated_; + + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename BusBase::RealT; + using CooMatrixT = typename BusBase::CooMatrixT; + using MonitorT = typename BusBase::MonitorT; + using ModelDataT = BusData; + using BusTypeT = typename BusData::BusType; + using SignalDataT = BusSignalVoltageInData; + using SignalPortsT = SignalPorts; + + BusSignalVoltageIn(); + /// Initial voltage arguments are ignored; the voltage comes from signals. + BusSignalVoltageIn(ScalarT Vr, ScalarT Vi); + /// Initial voltage in `data` is ignored; the voltage comes from signals. + BusSignalVoltageIn(const ModelDataT& data); + virtual ~BusSignalVoltageIn(); + + virtual int setBusID(IdxT) override final; + virtual int allocate() override final; + virtual int verify() const override final; + virtual int tagDifferentiable() override final; + virtual int setAbsoluteTolerance(RealT rel_tol) override final; + virtual int initialize() override final; + virtual int evaluateResidual() override final; + virtual int evaluateJacobian() override final; + + virtual BusTypeT BusType() const override final + { + return BusTypeT::SIGNAL_VOLTAGE_IN; + } + + /** + * @brief Bus voltage, real component, read from the `vr` inlet. + * + * The BusBase interface requires a mutable reference, but the voltage + * is owned by the signal source. Callers must not write through it. + */ + virtual ScalarT& Vr() override final + { + return const_cast(std::as_const(*this).Vr()); + } + + virtual const ScalarT& Vr() const override final + { + return readVoltage("vr"); + } + + /** + * @brief Bus voltage, imaginary component, read from the `vi` inlet. + * + * See Vr() for the note on the mutable overload. + */ + virtual ScalarT& Vi() override final + { + return const_cast(std::as_const(*this).Vi()); + } + + virtual const ScalarT& Vi() const override final + { + return readVoltage("vi"); + } + + virtual ScalarT& Ir() override final + { + return Ir_; + } + + virtual const ScalarT& Ir() const override final + { + return Ir_; + } + + virtual ScalarT& Ii() override final + { + return Ii_; + } + + virtual const ScalarT& Ii() const override final + { + return Ii_; + } + + SignalPortsT& getPorts() + { + return ports_; + } + + const SignalPortsT& getPorts() const + { + return ports_; + } + + private: + /// Read a voltage inlet, throwing if it has no linked signal. + template + const ScalarT& readVoltage(const char* name) const + { + const auto& port = ports_.in.template port(); + if (!port.linked()) + { + Log::error() << "BusSignalVoltageIn: voltage inlet " << name + << " read without a linked signal\n"; + throw std::runtime_error("BusSignalVoltageIn: voltage inlet has no linked signal"); + } + return port.readSignal(); + } + + ScalarT Ir_{0.0}; + ScalarT Ii_{0.0}; + + /// Current sums are not system variables; the output signals carry no valid index. + IdxT ir_index_{INVALID_INDEX}; + IdxT ii_index_{INVALID_INDEX}; + + /// Signal ports (inlets and outlets) + SignalPortsT ports_; + }; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInData.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInData.hpp new file mode 100644 index 0000000000..3b8cfa4d0e --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInData.hpp @@ -0,0 +1,57 @@ +/** + * @file BusSignalVoltageInData.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Signal port definitions for BusSignalVoltageIn. + * + * BusSignalVoltageIn is constructed from BusData like other buses. This header adds + * the port enumerations and a ComponentData alias that satisfies the + * ModelData concept, so the generic SignalPorts container can be reused for + * the bus's signal ports and connected from component-style data. + */ +#pragma once + +#include + +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /// BusSignalVoltageIn has no bus terminals of its own; it is a bus. + enum class BusSignalVoltageInBuses : size_t + { + }; + + /// Signal inlets of a bus with voltage signal inlets and current signal outlets (see BusSignalVoltageIn) + enum class BusSignalVoltageInInputs : size_t + { + vr, ///< Bus voltage, real component + vi, ///< Bus voltage, imaginary component + }; + + /// Signal outlets of a bus with voltage signal inlets and current signal outlets (see BusSignalVoltageIn) + enum class BusSignalVoltageInOutputs : size_t + { + ir, ///< Sum of real current injections from attached components + ii, ///< Sum of imaginary current injections from attached components + }; + + /** + * @brief Component-style data for BusSignalVoltageIn signal ports + * + * Reuses the bus parameter and monitorable-variable enumerations from + * BusData. Only the signal maps are used by the bus. + */ + template + using BusSignalVoltageInData = + ComponentData; + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInDependencyTracking.cpp new file mode 100644 index 0000000000..fb9f6c5232 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInDependencyTracking.cpp @@ -0,0 +1,17 @@ +/** + * @file BusSignalVoltageInDependencyTracking.cpp + * @author Slaven Peles (peless@ornl.gov) + */ + +#include "BusSignalVoltageInImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + // Available template instantiations + template class BusSignalVoltageIn; + template class BusSignalVoltageIn; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp new file mode 100644 index 0000000000..790ece6f6a --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp @@ -0,0 +1,238 @@ +/** + * @file BusSignalVoltageInImpl.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Implementation of a bus whose voltage is set by input signals. + */ + +#include +#include + +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /*! + * @brief Constructor for a bus whose voltage is set by input signals. + * + * - Number of equations = 0 (size_) + * - Number of variables = 0 (size_) + */ + template + BusSignalVoltageIn::BusSignalVoltageIn() + { + size_ = 0; + } + + /*! + * @brief Constructor with the signature of other buses. + * + * The voltage arguments are ignored; this bus reads its voltage from + * its input signals only. + */ + template + BusSignalVoltageIn::BusSignalVoltageIn(ScalarT /* Vr */, ScalarT /* Vi */) + { + size_ = 0; + } + + /** + * @brief Construct a new BusSignalVoltageIn from bus data. + * + * The initial voltage in `data` is ignored; this bus reads its voltage + * from its input signals only. + * + * @param[in] data - structure with bus data + */ + template + BusSignalVoltageIn::BusSignalVoltageIn(const ModelDataT& data) + { + bus_id_ = data.bus_id; + size_ = 0; + monitor_ = std::make_unique("Bus_" + data.name, data.monitored_variables); + using Variable = typename ModelDataT::MonitorableVariables; + monitor_->set(Variable::Vr, [this] + { return Vr(); }); + monitor_->set(Variable::Vi, [this] + { return Vi(); }); + monitor_->set(Variable::Vm, [this] + { return std::sqrt(Vr() * Vr() + Vi() * Vi()); }); + monitor_->set(Variable::Va, [this] + { return std::atan2(Vi(), Vr()); }); + } + + template + BusSignalVoltageIn::~BusSignalVoltageIn() + { + if (coo_jac_ != nullptr) + { + delete coo_jac_; + coo_jac_ = nullptr; + } + } + + /*! + * @brief Allocate (empty) bus storage and link output signals. + * + * Signal outlets `ir` and `ii` are linked to the current sums here, so + * they have to be connected before this method is called. The current + * sums are not system variables, so the linked indices are invalid. + */ + template + int BusSignalVoltageIn::allocate() + { + if (!allocated_) + { + this->allocateVectors(size_); + } + auto size = static_cast(size_); + + variable_indices_.resize(size); + residual_indices_.resize(size); + + if (auto ir_port = ports_.out.template port()) + { + ir_port.link(&Ir_, &ir_index_); + } + if (auto ii_port = ports_.out.template port()) + { + ii_port.link(&Ii_, &ii_index_); + } + + allocated_ = true; + return 0; + } + + /** + * @brief Check that the voltage inlets are connected and linked, and + * that connected outlets are linked. + * + * Both voltage inlets `vr` and `vi` are mandatory, since the bus has no + * voltage of its own and no default value is allowed. + * + * @throws std::runtime_error if any port fails the check. Each problem + * is logged before throwing. + * + * @return 0 (an error is reported by throwing). + */ + template + int BusSignalVoltageIn::verify() const + { + int errors = 0; + + auto check_input = [&](const char* name) + { + const auto& port = ports_.in.template port(); + if (!port.connected()) + { + Log::error() << "BusSignalVoltageIn: " << name + << " signal inlet is not connected; a default voltage is not allowed\n"; + errors += 1; + } + else if (!port.linked()) + { + Log::error() << "BusSignalVoltageIn: " << name << " signal attached with no linked source\n"; + errors += 1; + } + }; + + auto check_output = [&](const char* name) + { + const auto& port = ports_.out.template port(); + if (port.connected() && !port.linked()) + { + Log::error() << "BusSignalVoltageIn: " << name + << " signal attached but not linked; connect signal ports before allocate()\n"; + errors += 1; + } + }; + + check_input.template operator()("Vr"); + check_input.template operator()("Vi"); + check_output.template operator()("Ir"); + check_output.template operator()("Ii"); + + if (errors > 0) + { + throw std::runtime_error("BusSignalVoltageIn: signal ports are not correctly connected"); + } + + return 0; + } + + /** + * @brief Set the bus ID + */ + template + int BusSignalVoltageIn::setBusID(IdxT bus_id) + { + bus_id_ = bus_id; + return 0; + } + + /** + * @brief No variables to tag. + */ + template + int BusSignalVoltageIn::tagDifferentiable() + { + return 0; + } + + /** + * @brief No variables, nothing to set. + */ + template + int BusSignalVoltageIn::setAbsoluteTolerance(RealT) + { + return 0; + } + + /*! + * @brief Reset current sums. The voltage is owned by the signal sources. + */ + template + int BusSignalVoltageIn::initialize() + { + Ir_ = 0.0; + Ii_ = 0.0; + return 0; + } + + /*! + * @brief Reset current sums to zero. + * + * Components attached to the bus accumulate their injections into Ir() + * and Ii() afterwards. The voltage needs no update here: Vr() and Vi() + * read the input signals directly. + * + * @warning This implementation assumes bus residuals are always evaluated + * _before_ component model residuals. + */ + template + int BusSignalVoltageIn::evaluateResidual() + { + Ir_ = 0.0; + Ii_ = 0.0; + return 0; + } + + /** + * @brief There is no Jacobian for a bus without variables. + * + * @return int - error code + */ + template + int BusSignalVoltageIn::evaluateJacobian() + { + if (coo_jac_ == nullptr) + { + nnz_ = 0; + coo_jac_ = new CooMatrixT(0, 0, 0); + } + return 0; + } + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md new file mode 100644 index 0000000000..ce228b85c0 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md @@ -0,0 +1,78 @@ +# BusSignalVoltageIn + +`BusSignalVoltageIn` is the mirror image of `BusSignalVoltageOut`. Its +voltage is read directly from input signals, and the sum of current +injections from components attached to it is published on output signals. +The bus stores no voltage of its own and has no unknowns and no equations. + +## Notes + +- Signal ports must be connected before `allocate()` is called. The signal outlets + are linked to the current sums in `allocate()`. +- Both voltage inlets are mandatory. `verify()` logs each problem and + throws if a voltage inlet is not connected or not linked, or if a + connected outlet is not linked. Reading the voltage through an unlinked inlet + throws. No default voltage is ever used. +- The current sums are complete only after all attached components have + evaluated their residuals. Consumers of `ir` and `ii` must be evaluated + after them. +- Current entering the bus has positive sign. + +## Model Parameters + +Same as `Bus`. + +## Model Ports + +Signal | Direction | Units | Description | Note +-----|-----------|--------|-------------------------------------------------------|----- +`vr` | in | [p.u.] | Bus voltage, real component $V_r$ | Required +`vi` | in | [p.u.] | Bus voltage, imaginary component $V_i$ | Required +`ir` | out | [p.u.] | Sum of real current injections $I_r$ | +`ii` | out | [p.u.] | Sum of imaginary current injections $I_i$ | + +## Model Variables + +### Internal Variables + +None. + +### External Variables + +#### Algebraic + +Symbol | Units | Description | Note +-------|--------|-----------------------------------------------|----- +$V_r$ | [p.u.] | Bus voltage, real component from port `vr` | +$V_i$ | [p.u.] | Bus voltage, imaginary component from port `vi` | + +## Model Equations + +### Internal Equations + +None. The published outputs are + +```math +\begin{aligned} +I_r &= \sum_{d \in \mathcal{D}} I_{r,d} \\ +I_i &= \sum_{d \in \mathcal{D}} I_{i,d} +\end{aligned} +``` + +where $\mathcal{D}$ is the set of devices attached directly to the bus. +`Vr()` and `Vi()` return the connected signal value by reference. + +## Initialization + +Current sums are set to zero. The voltage is owned by the signal sources and +is not initialized by the bus; the initial voltage in bus data is ignored. + +## Monitors + +Same as `Bus`. + +## Testing + +Unit tests in `tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp` +cover construction, voltage inputs, current output accumulation and reset, +verification of unlinked inputs, and dependency-tracking derivatives. diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.cpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.cpp new file mode 100644 index 0000000000..f105803080 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.cpp @@ -0,0 +1,30 @@ +/** + * @file BusSignalVoltageOut.cpp + * @author Slaven Peles (peless@ornl.gov) + */ + +#include "BusSignalVoltageOutImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + /** + * @brief Jacobian evaluation not implemented + * + * @return int - error code + */ + template + int BusSignalVoltageOut::evaluateJacobian() + { + Log::misc() << "BusSignalVoltageOut: Jacobian evaluation is not implemented\n"; + + return 0; + } + + // Available template instantiations + template class BusSignalVoltageOut; + template class BusSignalVoltageOut; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp new file mode 100644 index 0000000000..f3307e5173 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp @@ -0,0 +1,201 @@ +/** + * @file BusSignalVoltageOut.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Declaration of a bus with signal ports. + */ + +#pragma once + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /*! + * @brief Bus with signal ports. + * + * Like @ref Bus, this model owns the bus voltage components _Vr_ and + * _Vi_ as algebraic variables and uses current balance in Cartesian + * coordinates as residuals. In addition, it + * - publishes _Vr_ and _Vi_ on signal outlets `vr` and `vi`, and + * - sets its residuals f[0] and f[1] to the current injections read from + * signal inlets `ir` and `ii`, respectively. Both inlets are + * mandatory: verify() throws if either is not connected to a linked + * signal, and no default value is ever used. + * + * Components attached to the bus directly (without signals) keep adding + * their currents to the residuals after the bus residual is evaluated, + * exactly as they do for @ref Bus. + * + * @note Signal ports have to be connected before allocate() is called, since + * the output signals are linked to the bus variables there. + */ + template + class BusSignalVoltageOut : public BusBase + { + using BusBase::bus_id_; + using BusBase::size_; + using BusBase::nnz_; + using BusBase::y_; + using BusBase::yp_; + using BusBase::f_; + using BusBase::tag_; + using BusBase::abs_tol_; + using BusBase::variable_indices_; + using BusBase::residual_indices_; + using BusBase::coo_jac_; + using BusBase::monitor_; + using BusBase::allocated_; + + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename BusBase::RealT; + using CooMatrixT = typename BusBase::CooMatrixT; + using MonitorT = typename BusBase::MonitorT; + using ModelDataT = BusData; + using BusTypeT = typename BusData::BusType; + using SignalDataT = BusSignalVoltageOutData; + using SignalPortsT = SignalPorts; + + BusSignalVoltageOut(); + BusSignalVoltageOut(ScalarT Vr, ScalarT Vi); + BusSignalVoltageOut(const ModelDataT& data); + virtual ~BusSignalVoltageOut(); + + virtual int setBusID(IdxT) override final; + virtual int allocate() override final; + virtual int verify() const override final; + virtual int tagDifferentiable() override final; + virtual int setAbsoluteTolerance(RealT rel_tol) override final; + virtual int initialize() override final; + virtual int evaluateResidual() override final; + virtual int evaluateJacobian() override final; + + virtual BusTypeT BusType() const override final + { + return BusTypeT::SIGNAL_VOLTAGE_OUT; + } + + virtual ScalarT& Vr() override final + { + return y_.getData()[0]; + } + + virtual const ScalarT& Vr() const override final + { + return y_.getData()[0]; + } + + virtual ScalarT& Vi() override final + { + return y_.getData()[1]; + } + + virtual const ScalarT& Vi() const override final + { + return y_.getData()[1]; + } + + virtual ScalarT& Ir() override final + { + return f_.getData()[0]; + } + + virtual const ScalarT& Ir() const override final + { + return f_.getData()[0]; + } + + virtual ScalarT& Ii() override final + { + return f_.getData()[1]; + } + + virtual const ScalarT& Ii() const override final + { + return f_.getData()[1]; + } + + SignalPortsT& getPorts() + { + return ports_; + } + + const SignalPortsT& getPorts() const + { + return ports_; + } + + protected: + int constructCoo() + { + if (coo_jac_ == nullptr) + { + IdxT num_rows = 0; + IdxT num_cols = 0; + for (IdxT i = 0; i < nnz_; ++i) + { + if (J_rows_buffer_[i] + 1 > num_rows) + { + num_rows = J_rows_buffer_[i] + 1; + } + if (J_cols_buffer_[i] + 1 > num_cols) + { + num_cols = J_cols_buffer_[i] + 1; + } + } + coo_jac_ = new CooMatrixT(num_rows, num_cols, nnz_); + coo_jac_->setDataPointers(J_rows_buffer_, J_cols_buffer_, J_vals_buffer_, memory::HOST); + } + + return 0; + } + + /** + * @brief Initialize DependencyTracking variable numbers. + * + * @note Assigns even indices to y and odd indices to yp. + * Should be called in initialize(), after variables have been set. + */ + int initializeDependencyTrackingVariableNumbers() + requires std::is_same_v + { + auto* y = y_.getData(); + auto* yp = yp_.getData(); + + for (IdxT j = 0; j < size_; ++j) + { + const IdxT var_idx = this->getVariableIndex(j); + if (var_idx != INVALID_INDEX) + { + // Even indices for y and odd indices for yp + y[j].setVariableNumber(static_cast(2 * var_idx)); + yp[j].setVariableNumber(static_cast(2 * var_idx + 1)); + } + } + + y_.setDataUpdated(); + yp_.setDataUpdated(); + + return 0; + } + + IdxT* J_rows_buffer_{nullptr}; + IdxT* J_cols_buffer_{nullptr}; + RealT* J_vals_buffer_{nullptr}; + + private: + ScalarT Vr0_{0.0}; + ScalarT Vi0_{0.0}; + + /// Signal ports + SignalPortsT ports_; + }; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutData.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutData.hpp new file mode 100644 index 0000000000..729cfc85ab --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutData.hpp @@ -0,0 +1,57 @@ +/** + * @file BusSignalVoltageOutData.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Signal port definitions for BusSignalVoltageOut. + * + * BusSignalVoltageOut is constructed from BusData like other buses. This header adds + * the port enumerations and a ComponentData alias that satisfies the + * ModelData concept, so the generic SignalPorts container can be reused for + * the bus's signal ports and connected from component-style data. + */ +#pragma once + +#include + +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /// BusSignalVoltageOut has no bus terminals of its own; it is a bus. + enum class BusSignalVoltageOutBuses : size_t + { + }; + + /// Signal inlets of a bus with voltage signal outlets and current signal inlets (see BusSignalVoltageOut) + enum class BusSignalVoltageOutInputs : size_t + { + ir, ///< Real current injection, added to the real current residual + ii, ///< Imaginary current injection, added to the imaginary current residual + }; + + /// Signal outlets of a bus with voltage signal outlets and current signal inlets (see BusSignalVoltageOut) + enum class BusSignalVoltageOutOutputs : size_t + { + vr, ///< Bus voltage, real component + vi, ///< Bus voltage, imaginary component + }; + + /** + * @brief Component-style data for BusSignalVoltageOut signal ports + * + * Reuses the bus parameter and monitorable-variable enumerations from + * BusData. Only the signal maps are used by the bus. + */ + template + using BusSignalVoltageOutData = + ComponentData; + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutDependencyTracking.cpp new file mode 100644 index 0000000000..0384e54511 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutDependencyTracking.cpp @@ -0,0 +1,31 @@ +/** + * @file BusSignalVoltageOutDependencyTracking.cpp + * @author Slaven Peles (peless@ornl.gov) + */ + +#include "BusSignalVoltageOutImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + /** + * @brief Evaluate DependencyTracking::Variable Jacobian. + * + * DependencyTracking::Variable stores the Jacobian as dependency maps, + * updated during calls to evaluateResidual(). Nothing to do here. + * + * @return int - error code + */ + template + int BusSignalVoltageOut::evaluateJacobian() + { + return 0; + } + + // Available template instantiations + template class BusSignalVoltageOut; + template class BusSignalVoltageOut; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutEnzyme.cpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutEnzyme.cpp new file mode 100644 index 0000000000..3ed0089baf --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutEnzyme.cpp @@ -0,0 +1,102 @@ +/** + * @file BusSignalVoltageOutEnzyme.cpp + * @author Slaven Peles (peless@ornl.gov) + */ + +#include "BusSignalVoltageOutImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + /** + * @brief Jacobian evaluation for the Enzyme build. + * + * The first four entries are zero placeholders for the bus voltage + * columns, as in Bus. They provide the indices for entries that other + * components contribute to and that are later deduplicated. + * + * One additional entry per connected signal inlet holds the derivative of + * the residual with respect to the signal variable, which is one. When + * the signal has no valid system variable index, the entry is stored as + * a zero duplicate of the bus-voltage placeholder so that the sparsity + * pattern stays fixed. + * + * @return int - error code + */ + template + int BusSignalVoltageOut::evaluateJacobian() + { + constexpr IdxT num_bus_entries = 4; + + const auto& ir_port = ports_.in.template port(); + const auto& ii_port = ports_.in.template port(); + + if (J_rows_buffer_ == nullptr) + { + nnz_ = num_bus_entries; + if (ir_port.connected()) + { + ++nnz_; + } + if (ii_port.connected()) + { + ++nnz_; + } + + const auto num_entries = static_cast(nnz_); + J_rows_buffer_ = new IdxT[num_entries]; + J_cols_buffer_ = new IdxT[num_entries]; + J_vals_buffer_ = new RealT[num_entries]; + } + + J_rows_buffer_[0] = residual_indices_.at(0); + J_rows_buffer_[1] = residual_indices_.at(0); + J_rows_buffer_[2] = residual_indices_.at(1); + J_rows_buffer_[3] = residual_indices_.at(1); + J_cols_buffer_[0] = variable_indices_.at(0); + J_cols_buffer_[1] = variable_indices_.at(1); + J_cols_buffer_[2] = variable_indices_.at(0); + J_cols_buffer_[3] = variable_indices_.at(1); + J_vals_buffer_[0] = 0.0; + J_vals_buffer_[1] = 0.0; + J_vals_buffer_[2] = 0.0; + J_vals_buffer_[3] = 0.0; + + IdxT k = num_bus_entries; + + auto add_signal_entry = [&](const auto& port, size_t residual) + { + if (!port.connected()) + { + return; + } + const IdxT signal_index = port.linked() ? port.signalVariableIndex() : INVALID_INDEX; + J_rows_buffer_[k] = residual_indices_.at(residual); + if (signal_index != INVALID_INDEX) + { + J_cols_buffer_[k] = signal_index; + J_vals_buffer_[k] = 1.0; + } + else + { + J_cols_buffer_[k] = variable_indices_.at(residual); + J_vals_buffer_[k] = 0.0; + } + ++k; + }; + + add_signal_entry(ir_port, 0); + add_signal_entry(ii_port, 1); + + this->constructCoo(); + + return 0; + } + + // Available template instantiations + template class BusSignalVoltageOut; + template class BusSignalVoltageOut; + + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp new file mode 100644 index 0000000000..19e51c88c1 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp @@ -0,0 +1,269 @@ +/** + * @file BusSignalVoltageOutImpl.hpp + * @author Slaven Peles (peless@ornl.gov) + * @brief Implementation of a bus with signal ports. + */ + +#include +#include + +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /*! + * @brief Constructor for a phasor dynamics bus with signal ports. + * + * The model is using current balance in Cartesian coordinates. + * - Number of equations = 2 (size_) + * - Number of variables = 2 (size_) + */ + template + BusSignalVoltageOut::BusSignalVoltageOut() + : Vr0_(0.0), Vi0_(0.0) + { + size_ = 2; + } + + /*! + * @brief Constructor setting initial values for the bus voltage. + */ + template + BusSignalVoltageOut::BusSignalVoltageOut(ScalarT Vr, ScalarT Vi) + : Vr0_(Vr), Vi0_(Vi) + { + size_ = 2; + } + + /** + * @brief Construct a new BusSignalVoltageOut from bus data. + * + * @param[in] data - structure with bus data + */ + template + BusSignalVoltageOut::BusSignalVoltageOut(const ModelDataT& data) + : Vr0_(data.Vr0), + Vi0_(data.Vi0) + { + bus_id_ = data.bus_id; + size_ = 2; + monitor_ = std::make_unique("Bus_" + data.name, data.monitored_variables); + using Variable = typename ModelDataT::MonitorableVariables; + monitor_->set(Variable::Vr, [this] + { return Vr(); }); + monitor_->set(Variable::Vi, [this] + { return Vi(); }); + monitor_->set(Variable::Vm, [this] + { return std::sqrt(Vr() * Vr() + Vi() * Vi()); }); + monitor_->set(Variable::Va, [this] + { return std::atan2(Vi(), Vr()); }); + } + + template + BusSignalVoltageOut::~BusSignalVoltageOut() + { + if (J_rows_buffer_ != nullptr) + { + delete[] J_rows_buffer_; + delete[] J_cols_buffer_; + delete[] J_vals_buffer_; + J_rows_buffer_ = nullptr; + J_cols_buffer_ = nullptr; + J_vals_buffer_ = nullptr; + } + + if (coo_jac_ != nullptr) + { + delete coo_jac_; + coo_jac_ = nullptr; + } + } + + /*! + * @brief Allocate bus storage and index maps, and link output signals. + * + * Signal outlets `vr` and `vi` are linked to the bus voltage variables + * and their (system) variable indices here, so they have to be + * connected before this method is called. + */ + template + int BusSignalVoltageOut::allocate() + { + if (!allocated_) + { + this->allocateVectors(size_); + } + size_t size = static_cast(size_); + + tag_.resize(size); + + variable_indices_.resize(size); + residual_indices_.resize(size); + + // Default variable and residual index mapping to local index + for (IdxT j = 0; j < size_; ++j) + { + this->setVariableIndex(j, j); + this->setResidualIndex(j, j); + } + + // Publish bus voltage on the signal outlets + if (auto vr_port = ports_.out.template port()) + { + vr_port.link(&y_.getData()[0], &(this->getVariableIndex(0))); + } + if (auto vi_port = ports_.out.template port()) + { + vi_port.link(&y_.getData()[1], &(this->getVariableIndex(1))); + } + + allocated_ = true; + return 0; + } + + /** + * @brief Check that the current inlets are connected and linked, and + * that connected outlets are linked. + * + * Both current inlets `ir` and `ii` are mandatory, since the residuals + * are set from them and no default value is allowed. + * + * @throws std::runtime_error if any port fails the check. Each problem + * is logged before throwing. + * + * @return 0 (an error is reported by throwing). + */ + template + int BusSignalVoltageOut::verify() const + { + int errors = 0; + + auto check_input = [&](const char* name) + { + const auto& port = ports_.in.template port(); + if (!port.connected()) + { + Log::error() << "BusSignalVoltageOut: " << name + << " signal inlet is not connected; a default current is not allowed\n"; + errors += 1; + } + else if (!port.linked()) + { + Log::error() << "BusSignalVoltageOut: " << name << " signal attached with no linked source\n"; + errors += 1; + } + }; + + auto check_output = [&](const char* name) + { + const auto& port = ports_.out.template port(); + if (port.connected() && !port.linked()) + { + Log::error() << "BusSignalVoltageOut: " << name + << " signal attached but not linked; connect signal ports before allocate()\n"; + errors += 1; + } + }; + + check_input.template operator()("Ir"); + check_input.template operator()("Ii"); + check_output.template operator()("Vr"); + check_output.template operator()("Vi"); + + if (errors > 0) + { + throw std::runtime_error("BusSignalVoltageOut: signal ports are not correctly connected"); + } + + return 0; + } + + /** + * @brief Set the bus ID + */ + template + int BusSignalVoltageOut::setBusID(IdxT bus_id) + { + bus_id_ = bus_id; + return 0; + } + + /*! + * @brief Bus variables are algebraic. + */ + template + int BusSignalVoltageOut::tagDifferentiable() + { + tag_[0] = false; + tag_[1] = false; + 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. + * @return int 0 if successful, non-zero otherwise. + */ + template + int BusSignalVoltageOut::setAbsoluteTolerance(RealT rel_tol) + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + /*! + * @brief initialize method sets bus variables to stored initial values. + */ + template + int BusSignalVoltageOut::initialize() + { + auto* y = y_.getData(); + auto* yp = yp_.getData(); + + y[0] = Vr0_; + y[1] = Vi0_; + yp[0] = 0.0; + yp[1] = 0.0; + + y_.setDataUpdated(); + yp_.setDataUpdated(); + + // For DependencyTracking::Variable, set variable numbers + if constexpr (std::is_same_v) + { + this->initializeDependencyTrackingVariableNumbers(); + } + + return 0; + } + + /*! + * @brief Set residuals to the current injections from input signals. + * + * Residuals f[0] and f[1] are set to the values read from the `ir` and + * `ii` signal inlets, respectively. Both inlets are mandatory; verify() + * throws if either is not connected to a linked signal, and no default + * value is used here. Components attached to the bus add their currents + * afterwards. + * + * @warning This implementation assumes bus residuals are always evaluated + * _before_ component model residuals. + */ + template + int BusSignalVoltageOut::evaluateResidual() + { + auto* f = f_.getData(); + + f[0] = ports_.in.template port().readSignal(); + f[1] = ports_.in.template port().readSignal(); + + f_.setDataUpdated(); + return 0; + } + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md new file mode 100644 index 0000000000..5224654e2a --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md @@ -0,0 +1,82 @@ +# BusSignalVoltageOut + +`BusSignalVoltageOut` is a bus with signal ports. It owns the same voltage variables +and current-balance residuals as `Bus`, publishes the voltage components on +output signals, and adds current injections received on input signals to its +residuals. Devices attached directly to the bus still add their currents after +the bus residual is evaluated. + +## Notes + +- Signal ports must be connected before `allocate()` is called. The signal outlets + are linked to the bus voltage variables and their system indices in + `allocate()`. +- Both current inlets are mandatory. `verify()` logs each problem and + throws if a current inlet is not connected or not linked, or if a + connected outlet is not linked. No default current is ever used. +- Current entering the bus has positive sign. + +## Model Parameters + +Same as `Bus`. + +## Model Ports + +Signal | Direction | Units | Description | Note +-----|-----------|--------|----------------------------------------------------|----- +`vr` | out | [p.u.] | Bus voltage, real component $V_r$ | +`vi` | out | [p.u.] | Bus voltage, imaginary component $V_i$ | +`ir` | in | [p.u.] | Current injection, real component $I_r^{s}$ | Required, sets $f_0$ +`ii` | in | [p.u.] | Current injection, imaginary component $I_i^{s}$ | Required, sets $f_1$ + +## Model Variables + +### Internal Variables + +#### Algebraic + +Symbol | Units | Description | Note +-------|--------|----------------------------------|----- +$V_r$ | [p.u.] | Bus voltage, real component | +$V_i$ | [p.u.] | Bus voltage, imaginary component | + +### External Variables + +#### Algebraic + +Symbol | Units | Description | Note +----------|--------|----------------------------------------|----- +$I_r^{s}$ | [p.u.] | Current injection on signal inlet `ir` | +$I_i^{s}$ | [p.u.] | Current injection on signal inlet `ii` | + +## Model Equations + +### Internal Equations + +#### Algebraic + +Let $\mathcal{D}$ denote the set of devices attached directly to the bus. + +```math +\begin{aligned} +0 &= I_r^{s} + \sum_{d \in \mathcal{D}} I_{r,d} \\ +0 &= I_i^{s} + \sum_{d \in \mathcal{D}} I_{i,d} +\end{aligned} +``` + +Both signal inlets must be connected; see `verify()`. + +## Initialization + +Same as `Bus`. + +## Monitors + +Same as `Bus`. + +## Testing + +Unit tests in `tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp` cover +construction, output signal linking, residual evaluation with input signals, +verification of unlinked inputs, dependency-tracking derivatives, and (when +Enzyme is enabled) the sparse Jacobian entries. diff --git a/GridKit/Model/PhasorDynamics/Bus/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Bus/CMakeLists.txt index 159ed8af51..b5504efa8c 100644 --- a/GridKit/Model/PhasorDynamics/Bus/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Bus/CMakeLists.txt @@ -10,24 +10,53 @@ set(_install_headers BusFactory.hpp BusInfinite.hpp) +set(_bus_signal_voltage_out_headers BusSignalVoltageOut/BusSignalVoltageOut.hpp + BusSignalVoltageOut/BusSignalVoltageOutData.hpp) + +set(_bus_signal_voltage_in_headers BusSignalVoltageIn/BusSignalVoltageIn.hpp + BusSignalVoltageIn/BusSignalVoltageInData.hpp) + if(GRIDKIT_ENABLE_ENZYME) - gridkit_add_library( - phasor_dynamics_bus - SOURCES BusEnzyme.cpp BusInfinite.cpp - HEADERS ${_install_headers} - LINK_LIBRARIES PUBLIC GridKit::phasor_dynamics_core) + set(_bus_sources + BusEnzyme.cpp + BusInfinite.cpp + BusSignalVoltageOut/BusSignalVoltageOutEnzyme.cpp + BusSignalVoltageIn/BusSignalVoltageIn.cpp) else() - gridkit_add_library( - phasor_dynamics_bus - SOURCES Bus.cpp BusInfinite.cpp - HEADERS ${_install_headers} - LINK_LIBRARIES PUBLIC GridKit::phasor_dynamics_core) + set(_bus_sources + Bus.cpp + BusInfinite.cpp + BusSignalVoltageOut/BusSignalVoltageOut.cpp + BusSignalVoltageIn/BusSignalVoltageIn.cpp) endif() +gridkit_add_library( + phasor_dynamics_bus + SOURCES ${_bus_sources} + HEADERS ${_install_headers} + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal) + gridkit_add_library( phasor_dynamics_bus_dependency_tracking - SOURCES BusDependencyTracking.cpp BusInfiniteDependencyTracking.cpp - LINK_LIBRARIES PUBLIC GridKit::phasor_dynamics_core) + SOURCES BusDependencyTracking.cpp + BusInfiniteDependencyTracking.cpp + BusSignalVoltageOut/BusSignalVoltageOutDependencyTracking.cpp + BusSignalVoltageIn/BusSignalVoltageInDependencyTracking.cpp + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal_dependency_tracking) + +# Headers in subdirectories keep their relative path on install +install(FILES ${_bus_signal_voltage_out_headers} + DESTINATION include/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut) +install(FILES ${_bus_signal_voltage_in_headers} + DESTINATION include/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn) # Link to interface target for all components target_link_libraries( diff --git a/GridKit/Model/PhasorDynamics/SignalIn.hpp b/GridKit/Model/PhasorDynamics/SignalIn.hpp index d996f34c59..09636114e7 100644 --- a/GridKit/Model/PhasorDynamics/SignalIn.hpp +++ b/GridKit/Model/PhasorDynamics/SignalIn.hpp @@ -20,8 +20,8 @@ namespace GridKit using ScalarT = scalar_type; using IdxT = index_type; - /// Read a value from the connected signal node. - ScalarT readSignal() const + /// Read the value of the signal node if connected. + const ScalarT& readSignal() const { assert(this->connected()); return this->signal_node_->read(); diff --git a/GridKit/Model/PhasorDynamics/SignalNode/SignalNode.hpp b/GridKit/Model/PhasorDynamics/SignalNode/SignalNode.hpp index 7c9e8118f1..d8db5f8db4 100644 --- a/GridKit/Model/PhasorDynamics/SignalNode/SignalNode.hpp +++ b/GridKit/Model/PhasorDynamics/SignalNode/SignalNode.hpp @@ -77,7 +77,7 @@ namespace GridKit } [[gnu::always_inline]] - ScalarT read() const noexcept + const ScalarT& read() const noexcept { assert(signal_); return *signal_; diff --git a/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp b/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp new file mode 100644 index 0000000000..f4738709c9 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp @@ -0,0 +1,295 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + using Log = ::GridKit::Utilities::Logger; + + template + class BusSignalVoltageInTests + { + public: + using BusT = PhasorDynamics::BusSignalVoltageIn; + using BusTypeT = typename BusT::BusTypeT; + using SignalT = PhasorDynamics::SignalNode; + using SignalIn = PhasorDynamics::BusSignalVoltageInInputs; + using SignalOut = PhasorDynamics::BusSignalVoltageInOutputs; + + BusSignalVoltageInTests() = default; + ~BusSignalVoltageInTests() = default; + + /// True if verify() throws, as it must for a misconnected bus + template + static bool verifyThrows(const BusLike& bus) + { + try + { + bus.verify(); + } + catch (const std::runtime_error&) + { + return true; + } + return false; + } + + /// Constructor, allocation, and initialization checks + TestOutcome constructor() + { + TestStatus success = true; + + // This test triggers error messages on purpose; silence them. + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::NONE); + + const ScalarT Vr{0.93}; + const ScalarT Vi{-0.27}; + + PhasorDynamics::BusBase* bus = nullptr; + + bus = new BusT(); + bus->allocate(); + bus->initialize(); + success *= (bus->size() == 0); + success *= (bus->BusType() == BusTypeT::SIGNAL_VOLTAGE_IN); + success *= isEqual(bus->Ir(), 0.0); + success *= isEqual(bus->Ii(), 0.0); + // Voltage inlets are mandatory: an unconnected bus fails verification + success *= verifyThrows(*bus); + delete bus; + + // Initial voltage arguments are accepted for interface uniformity but not used + bus = new BusT(Vr, Vi); + bus->allocate(); + bus->initialize(); + success *= verifyThrows(*bus); + delete bus; + + bus = nullptr; + + Log::setVerbosity(previous_verbosity); + + return success.report(__func__); + } + + /// Signal inlets set the bus voltage + TestOutcome voltageInputs() + { + TestStatus success = true; + + // This test triggers error messages on purpose; silence them. + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::NONE); + + ScalarT Vr{0.93}; ///< Voltage on signal vr + ScalarT Vi{-0.27}; ///< Voltage on signal vi + IdxT vr_index{7}; + IdxT vi_index{8}; + + auto vr_node = SignalT({.name = "vr", .signal_id = 0}); + auto vi_node = SignalT({.name = "vi", .signal_id = 1}); + vr_node.link(&Vr, &vr_index); + vi_node.link(&Vi, &vi_index); + + BusT bus; + bus.getPorts().in.template port().connect(&vr_node); + bus.getPorts().in.template port().connect(&vi_node); + bus.allocate(); + bus.initialize(); + success *= (bus.verify() == 0); + + // Voltage is read straight from the signals, no evaluation needed + success *= isEqual(bus.Vr(), Vr); + success *= isEqual(bus.Vi(), Vi); + success *= (&bus.Vr() == &Vr); + success *= (&bus.Vi() == &Vi); + + // Voltage follows the signals + Vr = 1.17; + Vi = 0.41; + success *= isEqual(bus.Vr(), 1.17); + success *= isEqual(bus.Vi(), 0.41); + bus.evaluateResidual(); + success *= isEqual(bus.Vr(), 1.17); + success *= isEqual(bus.Vi(), 0.41); + + // Reading an unconnected voltage inlet is an error, never a default value + BusT plain; + plain.allocate(); + plain.initialize(); + success *= verifyThrows(plain); + bool threw = false; + try + { + [[maybe_unused]] const auto& v = plain.Vr(); + } + catch (const std::runtime_error&) + { + threw = true; + } + success *= threw; + + Log::setVerbosity(previous_verbosity); + + return success.report(__func__); + } + + /// Signal outlets publish the accumulated current injections + TestOutcome currentOutputs() + { + TestStatus success = true; + + // Mandatory voltage inlets + ScalarT Vr{0.93}; + ScalarT Vi{-0.27}; + IdxT vr_index{7}; + IdxT vi_index{8}; + auto vr_node = SignalT({.name = "vr", .signal_id = 0}); + auto vi_node = SignalT({.name = "vi", .signal_id = 1}); + vr_node.link(&Vr, &vr_index); + vi_node.link(&Vi, &vi_index); + + auto ir_node = SignalT({.name = "ir", .signal_id = 2}); + auto ii_node = SignalT({.name = "ii", .signal_id = 3}); + + BusT bus; + bus.getPorts().in.template port().connect(&vr_node); + bus.getPorts().in.template port().connect(&vi_node); + bus.getPorts().out.template port().connect(&ir_node); + bus.getPorts().out.template port().connect(&ii_node); + + success *= ir_node.assigned(); + success *= !ir_node.linked(); + + bus.allocate(); + bus.initialize(); + success *= (bus.verify() == 0); + success *= ir_node.linked(); + success *= ii_node.linked(); + success *= (ir_node.getVariableIndex() == INVALID_INDEX); + success *= (ii_node.getVariableIndex() == INVALID_INDEX); + + bus.evaluateResidual(); + success *= isEqual(ir_node.read(), 0.0); + success *= isEqual(ii_node.read(), 0.0); + + // Two attached components add their injections + bus.Ir() += -3.7; + bus.Ii() += 2.4; + bus.Ir() += 1.3; + bus.Ii() += -0.8; + success *= isEqual(ir_node.read(), -2.4); + success *= isEqual(ii_node.read(), 1.6); + + // Re-evaluating resets the sums + bus.evaluateResidual(); + success *= isEqual(ir_node.read(), 0.0); + success *= isEqual(ii_node.read(), 0.0); + + return success.report(__func__); + } + + /// verify() throws for voltage inlets that are unconnected or unlinked + TestOutcome verifyUnlinked() + { + TestStatus success = true; + + // This test triggers error messages on purpose; silence them. + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::NONE); + + auto vr_node = SignalT({.name = "vr", .signal_id = 0}); + auto vi_node = SignalT({.name = "vi", .signal_id = 1}); + + BusT bus; + bus.getPorts().in.template port().connect(&vr_node); + bus.allocate(); + bus.initialize(); + + // vr connected but unlinked, vi not connected + success *= verifyThrows(bus); + + ScalarT Vr{0.1}; + IdxT vr_index{0}; + vr_node.link(&Vr, &vr_index); + success *= verifyThrows(bus); + + bus.getPorts().in.template port().connect(&vi_node); + success *= verifyThrows(bus); + + ScalarT Vi{0.2}; + IdxT vi_index{1}; + vi_node.link(&Vi, &vi_index); + success *= (bus.verify() == 0); + + Log::setVerbosity(previous_verbosity); + + return success.report(__func__); + } + + /// Bus voltage carries the dependencies of the input signals + TestOutcome dependencyTracking() + { + TestStatus success = true; + + using VariableT = DependencyTracking::Variable; + using DtBusT = PhasorDynamics::BusSignalVoltageIn; + using DtSignalT = PhasorDynamics::SignalNode; + + const size_t vr_var_number{10}; + const size_t vi_var_number{11}; + + VariableT Vr{0.93}; + VariableT Vi{-0.27}; + Vr.setVariableNumber(vr_var_number); + Vi.setVariableNumber(vi_var_number); + IdxT vr_index{5}; + IdxT vi_index{6}; + + auto vr_node = DtSignalT({.name = "vr", .signal_id = 0}); + auto vi_node = DtSignalT({.name = "vi", .signal_id = 1}); + vr_node.link(&Vr, &vr_index); + vi_node.link(&Vi, &vi_index); + + DtBusT bus; + bus.getPorts().in.template port().connect(&vr_node); + bus.getPorts().in.template port().connect(&vi_node); + bus.allocate(); + bus.initialize(); + bus.evaluateResidual(); + bus.evaluateJacobian(); + + success *= isEqual(bus.Vr().getValue(), 0.93); + success *= isEqual(bus.Vi().getValue(), -0.27); + + VariableT::DependencyMap expected_vr{{vr_var_number, 1.0}}; + VariableT::DependencyMap expected_vi{{vi_var_number, 1.0}}; + success *= isEqual(bus.Vr().getDependencies(), expected_vr); + success *= isEqual(bus.Vi().getDependencies(), expected_vi); + + // A component injection depending on the bus voltage propagates + bus.Ir() += 2.0 * bus.Vr(); + VariableT::DependencyMap expected_ir{{vr_var_number, 2.0}}; + success *= isEqual(bus.Ir().getDependencies(), expected_ir); + + return success.report(__func__); + } + }; + + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp b/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp new file mode 100644 index 0000000000..9b8b75baf0 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp @@ -0,0 +1,337 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + using Log = ::GridKit::Utilities::Logger; + + template + class BusSignalVoltageOutTests + { + public: + using BusT = PhasorDynamics::BusSignalVoltageOut; + using BusTypeT = typename BusT::BusTypeT; + using SignalT = PhasorDynamics::SignalNode; + using SignalIn = PhasorDynamics::BusSignalVoltageOutInputs; + using SignalOut = PhasorDynamics::BusSignalVoltageOutOutputs; + + BusSignalVoltageOutTests() = default; + ~BusSignalVoltageOutTests() = default; + + /// True if verify() throws, as it must for a misconnected bus + template + static bool verifyThrows(const BusLike& bus) + { + try + { + bus.verify(); + } + catch (const std::runtime_error&) + { + return true; + } + return false; + } + + /// Constructor, allocation, and initialization checks + TestOutcome constructor() + { + TestStatus success = true; + + const ScalarT Vr{0.93}; + const ScalarT Vi{-0.27}; + + PhasorDynamics::BusBase* bus = nullptr; + + bus = new BusT(); + bus->allocate(); + bus->initialize(); + success *= isEqual(bus->Vr(), 0.0); + success *= isEqual(bus->Vi(), 0.0); + success *= (bus->size() == 2); + success *= (bus->BusType() == BusTypeT::SIGNAL_VOLTAGE_OUT); + delete bus; + + bus = new BusT(Vr, Vi); + bus->allocate(); + bus->initialize(); + success *= isEqual(bus->Vr(), Vr); + success *= isEqual(bus->Vi(), Vi); + delete bus; + + bus = nullptr; + + return success.report(__func__); + } + + /// Signal outlets publish the bus voltage and its variable indices + TestOutcome voltageOutputs() + { + TestStatus success = true; + + const ScalarT Vr{0.93}; + const ScalarT Vi{-0.27}; + const IdxT vr_index{7}; + const IdxT vi_index{8}; + + auto vr_node = SignalT({.name = "vr", .signal_id = 0}); + auto vi_node = SignalT({.name = "vi", .signal_id = 1}); + + // Mandatory current inlets + ScalarT Ir{-3.7}; + ScalarT Ii{2.4}; + IdxT ir_index{5}; + IdxT ii_index{6}; + auto ir_node = SignalT({.name = "ir", .signal_id = 2}); + auto ii_node = SignalT({.name = "ii", .signal_id = 3}); + ir_node.link(&Ir, &ir_index); + ii_node.link(&Ii, &ii_index); + + BusT bus(Vr, Vi); + bus.getPorts().out.template port().connect(&vr_node); + bus.getPorts().out.template port().connect(&vi_node); + bus.getPorts().in.template port().connect(&ir_node); + bus.getPorts().in.template port().connect(&ii_node); + + success *= vr_node.assigned(); + success *= vi_node.assigned(); + success *= !vr_node.linked(); + + bus.allocate(); + bus.setVariableIndex(0, vr_index); + bus.setVariableIndex(1, vi_index); + bus.initialize(); + + success *= (bus.verify() == 0); + success *= vr_node.linked(); + success *= vi_node.linked(); + success *= isEqual(vr_node.read(), Vr); + success *= isEqual(vi_node.read(), Vi); + success *= (vr_node.getVariableIndex() == vr_index); + success *= (vi_node.getVariableIndex() == vi_index); + + // Signal follows the live bus voltage + bus.Vr() = 1.17; + bus.Vi() = 0.41; + success *= isEqual(vr_node.read(), 1.17); + success *= isEqual(vi_node.read(), 0.41); + + return success.report(__func__); + } + + /// Signal inlets add current injections to the residual + TestOutcome residual() + { + TestStatus success = true; + + const ScalarT Vr{0.93}; + const ScalarT Vi{-0.27}; + ScalarT Ir{-3.7}; ///< Current injection on signal ir + ScalarT Ii{2.4}; ///< Current injection on signal ii + IdxT ir_index{5}; + IdxT ii_index{6}; + + // A bus without connected current inlets is rejected by verify() + { + BusT bus(Vr, Vi); + bus.allocate(); + bus.initialize(); + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::NONE); // expected errors + success *= verifyThrows(bus); + Log::setVerbosity(previous_verbosity); + } + + auto ir_node = SignalT({.name = "ir", .signal_id = 2}); + auto ii_node = SignalT({.name = "ii", .signal_id = 3}); + ir_node.link(&Ir, &ir_index); + ii_node.link(&Ii, &ii_index); + + BusT bus(Vr, Vi); + bus.getPorts().in.template port().connect(&ir_node); + bus.getPorts().in.template port().connect(&ii_node); + bus.allocate(); + bus.initialize(); + success *= (bus.verify() == 0); + + bus.evaluateResidual(); + success *= isEqual(bus.Ir(), Ir); + success *= isEqual(bus.Ii(), Ii); + success *= isEqual(bus.getResidual().getData()[0], Ir); + success *= isEqual(bus.getResidual().getData()[1], Ii); + + // A device attached to the bus adds its current after the bus residual + bus.Ir() += 1.3; + bus.Ii() += -0.8; + success *= isEqual(bus.Ir(), Ir + 1.3); + success *= isEqual(bus.Ii(), Ii - 0.8); + + // Re-evaluating resets and re-reads the signals + Ir = 0.55; + Ii = -1.25; + bus.evaluateResidual(); + success *= isEqual(bus.Ir(), Ir); + success *= isEqual(bus.Ii(), Ii); + + return success.report(__func__); + } + + /// verify() throws for current inlets that are unconnected or unlinked + TestOutcome verifyUnlinked() + { + TestStatus success = true; + + // This test triggers error messages on purpose; silence them. + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::NONE); + + auto ir_node = SignalT({.name = "ir", .signal_id = 2}); + auto ii_node = SignalT({.name = "ii", .signal_id = 3}); + + BusT bus(1.0, 0.0); + bus.getPorts().in.template port().connect(&ir_node); + bus.getPorts().in.template port().connect(&ii_node); + bus.allocate(); + bus.initialize(); + + success *= verifyThrows(bus); + + ScalarT Ir{0.1}; + IdxT ir_index{0}; + ir_node.link(&Ir, &ir_index); + success *= verifyThrows(bus); + + ScalarT Ii{0.2}; + IdxT ii_index{1}; + ii_node.link(&Ii, &ii_index); + success *= (bus.verify() == 0); + + Log::setVerbosity(previous_verbosity); + + return success.report(__func__); + } + + /// Residual dependencies on signal variables via dependency tracking + TestOutcome dependencyTracking() + { + TestStatus success = true; + + using VariableT = DependencyTracking::Variable; + using DtBusT = PhasorDynamics::BusSignalVoltageOut; + using DtSignalT = PhasorDynamics::SignalNode; + + const size_t ir_var_number{10}; + const size_t ii_var_number{11}; + + VariableT Ir{-3.7}; + VariableT Ii{2.4}; + Ir.setVariableNumber(ir_var_number); + Ii.setVariableNumber(ii_var_number); + IdxT ir_index{5}; + IdxT ii_index{6}; + + auto ir_node = DtSignalT({.name = "ir", .signal_id = 2}); + auto ii_node = DtSignalT({.name = "ii", .signal_id = 3}); + ir_node.link(&Ir, &ir_index); + ii_node.link(&Ii, &ii_index); + + DtBusT bus(VariableT{0.93}, VariableT{-0.27}); + bus.getPorts().in.template port().connect(&ir_node); + bus.getPorts().in.template port().connect(&ii_node); + bus.allocate(); + bus.setVariableIndex(0, 3); + bus.setVariableIndex(1, 4); + bus.initialize(); + bus.evaluateResidual(); + bus.evaluateJacobian(); + + const auto* f = bus.getResidual().getData(); + + success *= isEqual(f[0].getValue(), -3.7); + success *= isEqual(f[1].getValue(), 2.4); + + VariableT::DependencyMap expected_f0{{ir_var_number, 1.0}}; + VariableT::DependencyMap expected_f1{{ii_var_number, 1.0}}; + success *= isEqual(f[0].getDependencies(), expected_f0); + success *= isEqual(f[1].getDependencies(), expected_f1); + + return success.report(__func__); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + /// Sparse Jacobian has unit entries in the signal columns + TestOutcome jacobian() + { + TestStatus success = true; + + ScalarT Ir{-3.7}; + ScalarT Ii{2.4}; + IdxT ir_index{5}; + IdxT ii_index{6}; + const IdxT var_offset{3}; + + auto ir_node = SignalT({.name = "ir", .signal_id = 2}); + auto ii_node = SignalT({.name = "ii", .signal_id = 3}); + ir_node.link(&Ir, &ir_index); + ii_node.link(&Ii, &ii_index); + + BusT bus(0.93, -0.27); + bus.getPorts().in.template port().connect(&ir_node); + bus.getPorts().in.template port().connect(&ii_node); + bus.allocate(); + for (IdxT i = 0; i < bus.size(); ++i) + { + bus.setVariableIndex(i, i + var_offset); + bus.setResidualIndex(i, i + var_offset); + } + bus.initialize(); + bus.evaluateResidual(); + bus.evaluateJacobian(); + + auto* jac = bus.getCooJacobian(); + success *= (jac != nullptr); + if (jac == nullptr) + { + return success.report(__func__); + } + success *= (jac->getNnz() == 6); + + // Accumulate COO triplets into per-row maps (duplicates are summed) + std::map> rows; + const auto* row_data = jac->getRowData(); + const auto* col_data = jac->getColData(); + const auto* values = jac->getValues(); + for (IdxT k = 0; k < jac->getNnz(); ++k) + { + rows[static_cast(row_data[k])][static_cast(col_data[k])] += values[k]; + } + + std::map expected_row0{{3, 0.0}, {4, 0.0}, {5, 1.0}}; + std::map expected_row1{{3, 0.0}, {4, 0.0}, {6, 1.0}}; + success *= (rows.size() == 2); + success *= isEqual(rows[3], expected_row0); + success *= isEqual(rows[4], expected_row1); + + return success.report(__func__); + } +#endif + }; + + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 6eaa258979..173be7e552 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -17,6 +17,22 @@ target_link_libraries( GridKit::phasor_dynamics_bus_dependency_tracking GridKit::testing) +add_executable(test_phasor_bus_signal_voltage_out runBusSignalVoltageOutTests.cpp) +target_link_libraries( + test_phasor_bus_signal_voltage_out + GridKit::definitions + GridKit::phasor_dynamics_bus + GridKit::phasor_dynamics_bus_dependency_tracking + GridKit::testing) + +add_executable(test_phasor_bus_signal_voltage_in runBusSignalVoltageInTests.cpp) +target_link_libraries( + test_phasor_bus_signal_voltage_in + GridKit::definitions + GridKit::phasor_dynamics_bus + GridKit::phasor_dynamics_bus_dependency_tracking + GridKit::testing) + add_executable(test_phasor_bustosignaladapter runBusToSignalAdapterTests.cpp) target_link_libraries( test_phasor_bustosignaladapter @@ -205,6 +221,8 @@ target_link_libraries( add_test(NAME PhasorDynamicsBusTest COMMAND test_phasor_bus) add_test(NAME PhasorDynamicsBusFaultTest COMMAND test_phasor_bus_fault) +add_test(NAME PhasorDynamicsBusSignalVoltageOutTest COMMAND test_phasor_bus_signal_voltage_out) +add_test(NAME PhasorDynamicsBusSignalVoltageInTest COMMAND test_phasor_bus_signal_voltage_in) add_test(NAME PhasorDynamicsBusToSignalAdapterTest COMMAND test_phasor_bustosignaladapter) add_test(NAME PhasorDynamicsBranchTest COMMAND test_phasor_branch) add_test(NAME PhasorDynamicsGenrouTest COMMAND test_phasor_genrou) @@ -232,6 +250,8 @@ add_test(NAME PhasorDynamicsComponentConnectionTest COMMAND test_phasor_componen install( TARGETS test_phasor_bus test_phasor_bus_fault + test_phasor_bus_signal_voltage_out + test_phasor_bus_signal_voltage_in test_phasor_bustosignaladapter test_phasor_branch test_phasor_loadz diff --git a/tests/UnitTests/PhasorDynamics/runBusSignalVoltageInTests.cpp b/tests/UnitTests/PhasorDynamics/runBusSignalVoltageInTests.cpp new file mode 100644 index 0000000000..50b101e5c3 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runBusSignalVoltageInTests.cpp @@ -0,0 +1,18 @@ +#include "BusSignalVoltageInTests.hpp" + +int main() +{ + using namespace GridKit; + using namespace GridKit::Testing; + + TestingResults result; + BusSignalVoltageInTests test; + + result += test.constructor(); + result += test.voltageInputs(); + result += test.currentOutputs(); + result += test.verifyUnlinked(); + result += test.dependencyTracking(); + + return result.summary(); +} diff --git a/tests/UnitTests/PhasorDynamics/runBusSignalVoltageOutTests.cpp b/tests/UnitTests/PhasorDynamics/runBusSignalVoltageOutTests.cpp new file mode 100644 index 0000000000..57dbac3dd7 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runBusSignalVoltageOutTests.cpp @@ -0,0 +1,21 @@ +#include "BusSignalVoltageOutTests.hpp" + +int main() +{ + using namespace GridKit; + using namespace GridKit::Testing; + + TestingResults result; + BusSignalVoltageOutTests test; + + result += test.constructor(); + result += test.voltageOutputs(); + result += test.residual(); + result += test.verifyUnlinked(); + result += test.dependencyTracking(); +#ifdef GRIDKIT_ENABLE_ENZYME + result += test.jacobian(); +#endif + + return result.summary(); +}