Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions docs/developer-guide/andes-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,14 +635,14 @@ records the lower-level GENROU work already completed or assigned to PR 2.

### Expected GridDyn implementation touchpoints

| Area | Expected files / change |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Canonical machine model | **Done:** `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` is complete at the equation/initialization level and `genrou` is registered in `src/griddyn/genmodels/GenModel.cpp`; generic model `6` remains unchanged. |
| DYR parsing and attachment | **Partial:** GENROU creates the dedicated model and applies parameters in the correct RAW/DYR order. Refactor `src/fileInput/gridDynReadDYR.cpp` into schema-backed adapters and add exact bus-plus-machine-ID lookup support. |
| Native ANDES import | Extend the ANDES JSON reader's dynamic-object dispatch to build the same registered machine/controller classes and control connections used by DYR adapters. |
| Model tests | **Mostly done:** focused equation, Jacobian, initialization, saturation, invalid-parameter, and factory tests are present; add a disturbed-trajectory reference and clone regression as the model is integrated. |
| Reader tests | **Partial:** `test/andesTests/testAndesDyrReader.cpp` is a dedicated RAW/DYR GENROU attachment and initialization test. Add parser edge cases and disturbed trajectories as support expands. |
| Numerical references | **Partial:** the IEEE 14-bus RAW input, minimized GENROU DYR input, and captured ANDES initialization reference are stored under `test/test_files/andes_tests/` and run without importing ANDES. Add time-series references next. |
| Area | Expected files / change |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Canonical machine model | **Done:** `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` is complete at the equation/initialization level and `genrou` is registered in `src/griddyn/genmodels/GenModel.cpp`; generic model `6` remains unchanged. |
| DYR parsing and attachment | **Partial:** GENROU creates the dedicated model and applies parameters in the correct RAW/DYR order. Refactor `src/fileInput/gridDynReadDYR.cpp` into schema-backed adapters and add exact bus-plus-machine-ID lookup support. |
| Native ANDES import | Extend the ANDES JSON reader's dynamic-object dispatch to build the same registered machine/controller classes and control connections used by DYR adapters. |
| Model tests | **Mostly done:** focused equation, Jacobian, initialization, saturation, invalid-parameter, and factory tests are present; add a disturbed-trajectory reference and clone regression as the model is integrated. |
| Reader tests | **Partial:** `test/andesTests/testAndesDyrReader.cpp` is a dedicated RAW/DYR GENROU attachment and initialization test. Add parser edge cases and disturbed trajectories as support expands. |
| Numerical references | **Partial:** the IEEE 14-bus RAW input, minimized GENROU DYR input, captured GENROU initialization reference, and first GENROU+TGOV1 trajectory reference are stored under `test/test_files/andes_tests/` and run without importing ANDES. Add trajectories for the remaining controllers and cleared network disturbances next. |

### Planned GENROU reference cases

Expand All @@ -666,7 +666,7 @@ it also needs native-input mapping, initialization, and a trajectory test.
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial |
| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial |
| `TGOV1` | `GovernorTgov1`; ANDES-equation, DYR-order, limiter, initialization, Jacobian, and isolated trajectory regressions are complete. | Implemented |
| `TGOV1` | `GovernorTgov1`; ANDES-equation, DYR-order, limiter, initialization, Jacobian, and isolated trajectory regressions are complete, including a captured IEEE 14 GENROU+TGOV1 `Alter(pref0)` trajectory. | Implemented |
| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial |
| `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial |
| `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial |
Expand Down
235 changes: 235 additions & 0 deletions test/andesTests/testAndesDyrReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
#include "griddyn/Generator.h"
#include "griddyn/GridBus.h"
#include "griddyn/GridDynSimulation.h"
#include "griddyn/GridSubModel.h"
#include "griddyn/events/Event.h"
#include "griddyn/exciters/ExciterESST3A.h"
#include "griddyn/exciters/ExciterEXST1.h"
#include "griddyn/generators/DynamicGenerator.h"
#include "griddyn/genmodels/GenModelGENROU.h"
#include "griddyn/governors/GovernorIeeeG1.h"
#include "griddyn/governors/GovernorTgov1.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <fstream>
#include <gtest/gtest.h>
Expand All @@ -23,6 +28,7 @@
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace {
constexpr std::string_view andesTestDirectory{GRIDDYN_TEST_DIRECTORY "/andes_tests/"};
Expand All @@ -31,6 +37,83 @@ std::string makeAndesTestPath(std::string_view fileName)
{
return std::string{andesTestDirectory} + std::string{fileName};
}

std::vector<double> runLoadStepCase(const std::vector<std::string_view>& dyrFiles,
double loadStep = 0.5)
{
auto simulation = std::make_unique<griddyn::GridDynSimulation>();
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw"));
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_genrou.dyr"));
for (const auto dyrFile : dyrFiles) {
griddyn::loadFile(simulation.get(), makeAndesTestPath(dyrFile));
}

auto* load = simulation->findByUserID("load", 2);
EXPECT_NE(load, nullptr);
if (load == nullptr) {
return {};
}
const double initialLoad = load->get("p");
auto event = std::make_shared<griddyn::Event>(0.5);
EXPECT_TRUE(event->setTarget(load, "p"));
if (!event->isArmed()) {
return {};
}
event->setValue(initialLoad + loadStep);
simulation->add(event);

EXPECT_EQ(simulation->dynInitialize(), 0);
const auto initialState = simulation->getState();
EXPECT_FALSE(initialState.empty());
std::vector<std::pair<griddyn::GridSubModel*, std::vector<double>>> controllerStates;
for (const auto busId : {1, 2, 3, 6, 8}) {
auto* bus = dynamic_cast<griddyn::GridBus*>(simulation->findByUserID("bus", busId));
if (bus == nullptr) {
continue;
}
auto* generator = bus->getGen(0);
if (generator == nullptr) {
continue;
}
for (const auto controllerName : {"governor", "exciter"}) {
auto* controller =
dynamic_cast<griddyn::GridSubModel*>(generator->find(controllerName));
if (controller != nullptr) {
controllerStates.emplace_back(controller, controller->getStates());
}
}
}
EXPECT_FALSE(controllerStates.empty());
EXPECT_EQ(runResidualCheck(simulation, griddyn::cDaeSolverMode, false), 0);
simulation->run(2.0);
EXPECT_EQ(simulation->getSimulationTime(), 2.0);

const auto finalState = simulation->getState();
EXPECT_EQ(finalState.size(), initialState.size());
double maximumChange = 0.0;
for (std::size_t index = 0; index < finalState.size(); ++index) {
EXPECT_TRUE(std::isfinite(finalState[index]));
maximumChange =
(std::max)(maximumChange, std::abs(finalState[index] - initialState[index]));
}
EXPECT_GT(maximumChange, 1.0e-7);
double maximumControllerChange = 0.0;
for (const auto& [controller, initialControllerState] : controllerStates) {
const auto& finalControllerState = controller->getStates();
EXPECT_EQ(finalControllerState.size(), initialControllerState.size());
if (finalControllerState.size() != initialControllerState.size()) {
continue;
}
for (std::size_t index = 0; index < finalControllerState.size(); ++index) {
EXPECT_TRUE(std::isfinite(finalControllerState[index]));
maximumControllerChange =
(std::max)(maximumControllerChange,
std::abs(finalControllerState[index] - initialControllerState[index]));
}
}
EXPECT_GT(maximumControllerChange, 1.0e-9);
return finalState;
}
} // namespace

TEST(AndesDyrReaderTests, LoadsGenrouAndMatchesIeee14Initialization)
Expand Down Expand Up @@ -277,3 +360,155 @@ TEST(AndesDyrReaderTests, MapsExst1ParametersAndCouplesToGenrou)
EXPECT_EQ(runResidualCheck(simulation, griddyn::cDaeSolverMode, false), 0);
EXPECT_EQ(runJacobianCheck(simulation, griddyn::cDaeSolverMode, false), 0);
}

TEST(AndesDynamicTests, Tgov1RespondsToLoadStep)
{
const auto finalState = runLoadStepCase({"ieee14_tgov1.dyr"});
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, IeeeG1RespondsToLoadStep)
{
const auto finalState = runLoadStepCase({"ieee14_ieeeg1.dyr"});
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, Esst3aRespondsToLoadStep)
{
const auto finalState = runLoadStepCase({"ieee14_esst3a.dyr"});
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, Exst1RespondsToLoadStep)
{
const auto finalState = runLoadStepCase({"ieee14_exst1.dyr"});
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, CombinedControllersRespondToLoadStep)
{
const auto finalState = runLoadStepCase(
{"ieee14_tgov1.dyr", "ieee14_ieeeg1.dyr", "ieee14_esst3a.dyr", "ieee14_exst1.dyr"});
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, CombinedControllersSurviveLargeLoadStep)
{
const auto finalState = runLoadStepCase(
{"ieee14_tgov1.dyr", "ieee14_ieeeg1.dyr", "ieee14_esst3a.dyr", "ieee14_exst1.dyr"}, 2.0);
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, ExciterControllersSurviveLargeLoadStep)
{
const auto finalState = runLoadStepCase({"ieee14_esst3a.dyr", "ieee14_exst1.dyr"}, 2.0);
EXPECT_FALSE(finalState.empty());
}

TEST(AndesDynamicTests, Tgov1TrajectoryMatchesAndesReference)
{
std::ifstream input(makeAndesTestPath("andes_ieee14_tgov1_trajectory_reference.json"));
ASSERT_TRUE(input.is_open());
nlohmann::json reference;
input >> reference;

auto simulation = std::make_unique<griddyn::GridDynSimulation>();
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw"));
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_genrou.dyr"));
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_tgov1.dyr"));

auto* bus = dynamic_cast<griddyn::GridBus*>(simulation->findByUserID("bus", 1));
ASSERT_NE(bus, nullptr);
auto* generator = dynamic_cast<griddyn::DynamicGenerator*>(bus->getGen(0));
ASSERT_NE(generator, nullptr);
auto event = std::make_shared<griddyn::Event>(1.0);
ASSERT_TRUE(event->setTarget(generator, "pset"));
event->setValue(0.8);
simulation->add(event);

ASSERT_EQ(simulation->dynInitialize(), 0);
const auto& times = reference["times"];
const auto& busIds = reference["generator_bus_ids"];
const auto& expectedOmega = reference["genrou_omega"];
const auto omegaTolerance = reference["omega_absolute_tolerance"].get<double>();
ASSERT_EQ(times.size(), expectedOmega.size());

for (std::size_t timeIndex = 0; timeIndex < times.size(); ++timeIndex) {
simulation->run(times[timeIndex].get<double>());
for (std::size_t generatorIndex = 0; generatorIndex < busIds.size(); ++generatorIndex) {
auto* sampleBus = dynamic_cast<griddyn::GridBus*>(
simulation->findByUserID("bus", busIds[generatorIndex].get<index_t>()));
ASSERT_NE(sampleBus, nullptr);
auto* sampleGenerator = dynamic_cast<griddyn::DynamicGenerator*>(sampleBus->getGen(0));
ASSERT_NE(sampleGenerator, nullptr);
auto* genModel = dynamic_cast<griddyn::genmodels::GenModelGENROU*>(
sampleGenerator->find("genmodel"));
ASSERT_NE(genModel, nullptr);
const auto& states = genModel->getStates();
// GENROU stores its two algebraic currents before the differential
// states; omega is therefore local state index 3.
ASSERT_GT(states.size(), 3U);
EXPECT_NEAR(states[3],
expectedOmega[timeIndex][generatorIndex].get<double>(),
omegaTolerance)
<< "time=" << times[timeIndex].get<double>()
<< " bus=" << busIds[generatorIndex].get<index_t>();
}
}
}

TEST(AndesDynamicTests, Tgov1DownwardTrajectoryMatchesAndesReference)
{
// ANDES 2.0.0 reference: TGOV1_1.pref0 is changed from its initialized
// value to 0.6 at t=1.0 s. This exercises the opposite direction of the
// controller step while retaining the same shared RAW/DYR case.
constexpr std::array<double, 5> times{0.0, 0.5, 1.0, 1.5, 2.0};
constexpr std::array<std::array<double, 5>, 5> expectedOmega{{
{{1.0, 1.0, 1.0, 1.0, 1.0}},
{{1.0, 1.0, 1.0, 1.0, 1.0}},
{{1.0, 1.0, 1.0, 1.0, 1.0}},
{{0.9989737769949087,
0.9989924996844121,
0.9990010011199155,
0.9991283354866937,
0.99922546403493}},
{{0.9978834683434779,
0.9981224543226336,
0.9981346723285689,
0.9982087092060992,
0.9982448696384979}},
}};

auto simulation = std::make_unique<griddyn::GridDynSimulation>();
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw"));
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_genrou.dyr"));
griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_tgov1.dyr"));
auto* bus = dynamic_cast<griddyn::GridBus*>(simulation->findByUserID("bus", 1));
ASSERT_NE(bus, nullptr);
auto* generator = dynamic_cast<griddyn::DynamicGenerator*>(bus->getGen(0));
ASSERT_NE(generator, nullptr);
auto event = std::make_shared<griddyn::Event>(1.0);
ASSERT_TRUE(event->setTarget(generator, "pset"));
event->setValue(0.6);
simulation->add(event);
ASSERT_EQ(simulation->dynInitialize(), 0);

for (std::size_t timeIndex = 0; timeIndex < times.size(); ++timeIndex) {
ASSERT_EQ(simulation->run(times[timeIndex]), 0);
for (std::size_t generatorIndex = 0; generatorIndex < expectedOmega[timeIndex].size();
++generatorIndex) {
auto* sampleBus = dynamic_cast<griddyn::GridBus*>(
simulation->findByUserID("bus",
std::array<index_t, 5>{1, 2, 3, 6, 8}[generatorIndex]));
ASSERT_NE(sampleBus, nullptr);
auto* sampleGenerator = dynamic_cast<griddyn::DynamicGenerator*>(sampleBus->getGen(0));
ASSERT_NE(sampleGenerator, nullptr);
auto* genModel = dynamic_cast<griddyn::genmodels::GenModelGENROU*>(
sampleGenerator->find("genmodel"));
ASSERT_NE(genModel, nullptr);
ASSERT_GT(genModel->getStates().size(), 3U);
EXPECT_NEAR(genModel->getStates()[3], expectedOmega[timeIndex][generatorIndex], 0.005)
<< "time=" << times[timeIndex] << " generator=" << generatorIndex;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"andes_version": "2.0.0",
"case": "ieee14.raw + ieee14_genrou.dyr + ieee14_tgov1.dyr",
"disturbance": {
"model": "TGOV1",
"device": "TGOV1_1",
"source": "pref0",
"time": 1.0,
"method": "=",
"amount": 0.8
},
"times": [0.0, 0.5, 1.0, 1.5, 2.0],
"omega_absolute_tolerance": 0.005,
"generator_bus_ids": [1, 2, 3, 6, 8],
"genrou_omega": [
[1.0, 1.0, 1.0, 1.0, 1.0],
[1.0, 1.0, 1.0, 1.0, 1.0],
[1.0, 1.0, 1.0, 1.0, 1.0],
[
0.9999257581611178, 0.9999329128852735, 0.9999337708245503,
0.9999419710186372, 0.9999486153149914
],
[
0.9998560751145729, 0.9998714086888447, 0.9998719045372232,
0.9998778262709337, 0.9998801796876503
]
]
}