diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index 72d5d6eb2..dca8798cc 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -23,10 +23,10 @@ its importer mapping and a numerical regression test are both present. | `PV`, `Slack` | `AcBus` plus `Generator` | Implemented | Active-power and voltage targets are imported. | | `Line` | `AcLine` | Partial | `r`, `x`, `b`, tap, phase shift, and status are imported. ANDES line-rated-voltage (`Vn1`/`Vn2`) base conversion, including its implicit 110 kV default, still needs an equivalent mixed-base implementation. | | `Area` | `GridArea` | Partial | A GridDyn analogue exists; ANDES `Area` is not yet imported. | -| `Shunt` | fixed-admittance `ZipLoad` | Partial | The equivalent exists but needs a direct mapping and test. | +| `Shunt` | fixed-admittance `ZipLoad` | Implemented | Device `Sn`/`Vn` admittance-base conversion, status, conductance, and capacitive susceptance sign are mapped and numerically tested. | | `ShuntSw` | `loads::Svd` | Partial | Both provide switched reactive support; block/control semantics need mapping and numerical tests. | | `ShuntTD` | fixed shunt for power flow | Partial | Its steady-state behavior follows `Shunt`; its time-domain phase-voltage outputs are not represented. | -| `Jumper` | `links::ZBreaker` | Partial | Both merge buses through a zero-impedance connection; no importer mapping/test yet. | +| `Jumper` | `links::ZBreaker` | Implemented | End buses and status are imported; active jumpers merge the bus solutions. Network voltages and angles are numerically tested. ANDES jumper `p`/`q` transfer reporting has no direct `ZBreaker` output. | | `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5` | Partial | Candidate GridDyn models exist; parameter mapping and initialization comparisons remain. | | `Fortescue` | none | No direct analogue | Requires a multi-terminal positive-/negative-/zero-sequence interface model. | | `Node`, `Ground` | `DcBus` | Implemented | Ground is imported as a DC swing reference. | @@ -95,4 +95,6 @@ it also needs native-input mapping, initialization, and a trajectory test. | -------------------------- | ----------------------------------------------------------------------- | | `andes_kundur_vsc_pflow` | 10-bus AC network, DC resistor, PQ/VQ VSC controls, and AC/DC coupling. | | `andes_two_bus_pflow` | Minimal AC Slack/PQ/Line power flow. | +| `andes_shunt_pflow` | Fixed conductance/susceptance shunt with non-system `Sn`/`Vn` bases. | +| `andes_jumper_pflow` | Active and inactive zero-impedance jumpers in a loaded AC network. | | `andes_vsc_resistor_pflow` | Minimal AC/DC `VSCShunt` plus DC resistance power flow. | diff --git a/src/fileInput/gridReadAndes.cpp b/src/fileInput/gridReadAndes.cpp index f4495295b..121004a8d 100644 --- a/src/fileInput/gridReadAndes.cpp +++ b/src/fileInput/gridReadAndes.cpp @@ -13,6 +13,7 @@ #include "griddyn/links/AcLine.h" #include "griddyn/links/DcLink.h" #include "griddyn/links/VSCShunt.h" +#include "griddyn/links/ZBreaker.h" #include "griddyn/loads/ZipLoad.h" #include "griddyn/primary/AcBus.h" #include "griddyn/primary/DcBus.h" @@ -118,6 +119,38 @@ bool loadAndesJson(CoreObject* parentObject, const std::string& fileName) bus->second->add(load); } } + if (document.contains("Shunt") && document["Shunt"].is_array()) { + const auto parentBasePower = parentObject->get("basepower"); + const auto systemBasePower = (parentBasePower > 0.0) ? parentBasePower : 100.0; + for (const auto& record : document["Shunt"]) { + const auto busIndex = indexKey(record, "bus"); + const auto bus = acBuses.find(busIndex); + const auto baseVoltage = acBaseVoltages.find(busIndex); + if ((bus == acBuses.end()) || (baseVoltage == acBaseVoltages.end())) { + continue; + } + + // ANDES marks g and b as y=True parameters. Convert from the + // device base to the system/bus base using Zb/Zn, then map the + // ANDES injection convention P=g*V^2, Q=-b*V^2 to ZipLoad. + const auto deviceBasePower = number(record, "Sn", 100.0); + const auto deviceBaseVoltage = number(record, "Vn", 110.0); + const auto busBaseVoltage = baseVoltage->second; + const auto admittanceScale = + (deviceBasePower > 0.0 && deviceBaseVoltage > 0.0 && busBaseVoltage > 0.0) ? + (deviceBasePower / systemBasePower) * + std::pow(busBaseVoltage / deviceBaseVoltage, 2) : + 1.0; + + auto* shunt = new ZipLoad(objectName(record, "Shunt")); + shunt->set("yp", number(record, "g") * admittanceScale); + shunt->set("yq", -number(record, "b") * admittanceScale); + if (number(record, "u", 1.0) == 0.0) { + shunt->disable(); + } + bus->second->add(shunt); + } + } if (document.contains("PV") && document["PV"].is_array()) { for (const auto& record : document["PV"]) { const auto bus = acBuses.find(indexKey(record, "bus")); @@ -167,6 +200,22 @@ bool loadAndesJson(CoreObject* parentObject, const std::string& fileName) parentObject->add(line); } } + if (document.contains("Jumper") && document["Jumper"].is_array()) { + for (const auto& record : document["Jumper"]) { + const auto first = acBuses.find(indexKey(record, "bus1")); + const auto second = acBuses.find(indexKey(record, "bus2")); + if ((first == acBuses.end()) || (second == acBuses.end())) { + continue; + } + auto* jumper = new links::ZBreaker(objectName(record, "Jumper")); + jumper->updateBus(first->second, 1); + jumper->updateBus(second->second, 2); + if (number(record, "u", 1.0) == 0.0) { + jumper->disable(); + } + parentObject->add(jumper); + } + } std::unordered_map dcBuses; for (const auto& record : document["Node"]) { diff --git a/src/griddyn/links/ZBreaker.cpp b/src/griddyn/links/ZBreaker.cpp index 7b53a1955..e972d4c8e 100644 --- a/src/griddyn/links/ZBreaker.cpp +++ b/src/griddyn/links/ZBreaker.cpp @@ -133,12 +133,16 @@ void ZBreaker::merge() { B1->mergeBus(B2); merged = true; + B1->alert(this, STATE_COUNT_CHANGE); + B2->alert(this, STATE_COUNT_CHANGE); } void ZBreaker::unmerge() { B1->unmergeBus(B2); merged = false; + B1->alert(this, STATE_COUNT_CHANGE); + B2->alert(this, STATE_COUNT_CHANGE); } int ZBreaker::fixRealPower(double /*power*/, diff --git a/test/libraryTests/testJsonReader.cpp b/test/libraryTests/testJsonReader.cpp index fd4bfbd11..3469c6803 100644 --- a/test/libraryTests/testJsonReader.cpp +++ b/test/libraryTests/testJsonReader.cpp @@ -11,6 +11,8 @@ #include "formatInterpreters/jsonReaderElement.h" #include "griddyn/links/DcLink.h" #include "griddyn/links/VSCShunt.h" +#include "griddyn/links/ZBreaker.h" +#include "griddyn/loads/ZipLoad.h" #include "griddyn/primary/AcBus.h" #include "griddyn/primary/DcBus.h" #include @@ -375,6 +377,72 @@ TEST(AndesPowerFlowTests, MatchesCapturedTwoBusReference) } } +TEST(AndesPowerFlowTests, MatchesCapturedShuntReference) +{ + std::ifstream input(makeAndesTestPath("andes_shunt_pflow_reference.json")); + ASSERT_TRUE(input.is_open()); + nlohmann::json reference; + input >> reference; + const auto tolerance = reference.at("tolerance").get(); + + auto simulation = std::make_unique(); + griddyn::loadFile(simulation.get(), makeAndesTestPath("andes_shunt_pflow.json")); + ASSERT_EQ(simulation->powerflow(), 0); + + const std::array busNames{"slack_bus", "shunt_bus"}; + for (std::size_t index = 0; index < busNames.size(); ++index) { + auto* bus = dynamic_cast(simulation->find(busNames[index])); + ASSERT_NE(bus, nullptr); + EXPECT_NEAR(bus->get("voltage"), reference["bus_voltage"][index].get(), tolerance); + EXPECT_NEAR(bus->get("angle"), reference["bus_angle"][index].get(), tolerance); + } + + auto* shunt = dynamic_cast(simulation->find("shunt_bus::fixed_shunt")); + ASSERT_NE(shunt, nullptr); + EXPECT_NEAR(shunt->get("yp"), reference["shunt_yp"].get(), tolerance); + EXPECT_NEAR(shunt->get("yq"), reference["shunt_yq"].get(), tolerance); + EXPECT_NEAR(shunt->getRealPower(), reference["shunt_p"].get(), tolerance); + EXPECT_NEAR(shunt->getReactivePower(), reference["shunt_q"].get(), tolerance); +} + +TEST(AndesPowerFlowTests, MatchesCapturedJumperReference) +{ + std::ifstream input(makeAndesTestPath("andes_jumper_pflow_reference.json")); + ASSERT_TRUE(input.is_open()); + nlohmann::json reference; + input >> reference; + const auto tolerance = reference.at("tolerance").get(); + + auto simulation = std::make_unique(); + griddyn::loadFile(simulation.get(), makeAndesTestPath("andes_jumper_pflow.json")); + ASSERT_EQ(simulation->powerflow(), 0); + + const std::array busNames{"slack_bus", "jumper_bus", "load_bus"}; + for (std::size_t index = 0; index < busNames.size(); ++index) { + auto* bus = dynamic_cast(simulation->find(busNames[index])); + ASSERT_NE(bus, nullptr); + EXPECT_NEAR(bus->get("voltage"), reference["bus_voltage"][index].get(), tolerance); + EXPECT_NEAR(bus->get("angle"), reference["bus_angle"][index].get(), tolerance); + } + + auto* active = dynamic_cast(simulation->find("active_jumper")); + ASSERT_NE(active, nullptr); + EXPECT_TRUE(active->isEnabled()); + ASSERT_NE(active->getBus(1), nullptr); + ASSERT_NE(active->getBus(2), nullptr); + EXPECT_EQ(active->getBus(1)->getName(), "slack_bus"); + EXPECT_EQ(active->getBus(2)->getName(), "jumper_bus"); + EXPECT_NEAR(active->getBus(1)->get("voltage"), active->getBus(2)->get("voltage"), tolerance); + EXPECT_NEAR(active->getBus(1)->get("angle"), active->getBus(2)->get("angle"), tolerance); + + auto* inactive = dynamic_cast(simulation->find("inactive_jumper")); + ASSERT_NE(inactive, nullptr); + EXPECT_FALSE(inactive->isEnabled()); + EXPECT_GT(std::abs(reference["bus_voltage"][0].get() - + reference["bus_voltage"][2].get()), + tolerance); +} + TEST(AndesPowerFlowTests, MatchesCapturedVscResistorReference) { std::ifstream input(makeAndesTestPath("andes_vsc_resistor_pflow_reference.json")); diff --git a/test/test_files/andes_tests/andes_jumper_pflow.json b/test/test_files/andes_tests/andes_jumper_pflow.json new file mode 100644 index 000000000..73df0f9db --- /dev/null +++ b/test/test_files/andes_tests/andes_jumper_pflow.json @@ -0,0 +1,38 @@ +{ + "Bus": [ + { "idx": 1, "name": "slack_bus", "Vn": 230.0, "v0": 1.0, "a0": 0.0 }, + { "idx": 2, "name": "jumper_bus", "Vn": 230.0, "v0": 1.0, "a0": 0.0 }, + { "idx": 3, "name": "load_bus", "Vn": 230.0, "v0": 0.98, "a0": -0.1 } + ], + "Slack": [{ "idx": 1, "bus": 1, "p0": 0.0, "q0": 0.0, "v0": 1.0, "a0": 0.0 }], + "PQ": [{ "idx": "PQ_1", "bus": 3, "p0": 1.0, "q0": 0.3 }], + "Line": [ + { + "idx": "Line_1", + "bus1": 2, + "bus2": 3, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.01, + "x": 0.1, + "b": 0.02 + } + ], + "Jumper": [ + { + "idx": "Jumper_1", + "name": "active_jumper", + "bus1": 1, + "bus2": 2, + "u": 1 + }, + { + "idx": "Jumper_2", + "name": "inactive_jumper", + "bus1": 1, + "bus2": 3, + "u": 0 + } + ], + "Node": [] +} diff --git a/test/test_files/andes_tests/andes_jumper_pflow_reference.json b/test/test_files/andes_tests/andes_jumper_pflow_reference.json new file mode 100644 index 000000000..240e76396 --- /dev/null +++ b/test/test_files/andes_tests/andes_jumper_pflow_reference.json @@ -0,0 +1,10 @@ +{ + "source": "ANDES pflow: andes_jumper_pflow.json (single worker)", + "tolerance": 1e-6, + "bus_voltage": [1.0000000000000588, 1.0000000000001175, 0.9538230766212176], + "bus_angle": [ + 5.872574183726748e-15, 1.1745148367455111e-14, -0.10196800834017775 + ], + "jumper_p": [1.011921860424372, 0.0], + "jumper_q": [0.4001207123321315, 0.0] +} diff --git a/test/test_files/andes_tests/andes_shunt_pflow.json b/test/test_files/andes_tests/andes_shunt_pflow.json new file mode 100644 index 000000000..4cf0d85f3 --- /dev/null +++ b/test/test_files/andes_tests/andes_shunt_pflow.json @@ -0,0 +1,33 @@ +{ + "Bus": [ + { "idx": 1, "name": "slack_bus", "Vn": 230.0, "v0": 1.0, "a0": 0.0 }, + { "idx": 2, "name": "shunt_bus", "Vn": 230.0, "v0": 0.98, "a0": -0.08 } + ], + "Slack": [{ "idx": 1, "bus": 1, "p0": 0.0, "q0": 0.0, "v0": 1.0, "a0": 0.0 }], + "PQ": [{ "idx": "PQ_1", "bus": 2, "p0": 0.8, "q0": 0.3 }], + "Shunt": [ + { + "idx": "Shunt_1", + "name": "fixed_shunt", + "bus": 2, + "Sn": 50.0, + "Vn": 115.0, + "g": 0.02, + "b": 0.1, + "u": 1 + } + ], + "Line": [ + { + "idx": "Line_1", + "bus1": 1, + "bus2": 2, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.01, + "x": 0.1, + "b": 0.02 + } + ], + "Node": [] +} diff --git a/test/test_files/andes_tests/andes_shunt_pflow_reference.json b/test/test_files/andes_tests/andes_shunt_pflow_reference.json new file mode 100644 index 000000000..457b52e08 --- /dev/null +++ b/test/test_files/andes_tests/andes_shunt_pflow_reference.json @@ -0,0 +1,10 @@ +{ + "source": "ANDES pflow: andes_shunt_pflow.json (single worker)", + "tolerance": 1e-6, + "bus_voltage": [1.000000000002938, 0.9776771355574213], + "bus_angle": [2.7532649205730654e-13, -0.08482362610684113], + "shunt_yp": 0.04, + "shunt_yq": -0.2, + "shunt_p": 0.03823410325567057, + "shunt_q": -0.19117051627835288 +}