From 0eb93d4cacbc043a2db9cff73b1f586f36b24249 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 24 Aug 2026 06:05:32 -0700 Subject: [PATCH 1/3] add trajectory tests --- docs/developer-guide/andes-compatibility.md | 4 +- test/andesTests/testAndesDyrReader.cpp | 164 ++++++++++++++++++ ...des_ieee14_tgov1_trajectory_reference.json | 22 +++ 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index 7fb4f58aa..82e89547a 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -642,7 +642,7 @@ records the lower-level GENROU work already completed or assigned to PR 2. | 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. | +| 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 @@ -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 | diff --git a/test/andesTests/testAndesDyrReader.cpp b/test/andesTests/testAndesDyrReader.cpp index 7a9e58ffa..7a970e1c4 100644 --- a/test/andesTests/testAndesDyrReader.cpp +++ b/test/andesTests/testAndesDyrReader.cpp @@ -9,12 +9,16 @@ #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 +#include #include #include #include @@ -23,6 +27,7 @@ #include #include #include +#include namespace { constexpr std::string_view andesTestDirectory{GRIDDYN_TEST_DIRECTORY "/andes_tests/"}; @@ -31,6 +36,82 @@ std::string makeAndesTestPath(std::string_view fileName) { return std::string{andesTestDirectory} + std::string{fileName}; } + +std::vector runLoadStepCase(const std::vector& dyrFiles) +{ + auto simulation = std::make_unique(); + 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(0.5); + EXPECT_TRUE(event->setTarget(load, "p")); + if (!event->isArmed()) { + return {}; + } + event->setValue(initialLoad + 0.5); + simulation->add(event); + + EXPECT_EQ(simulation->dynInitialize(), 0); + const auto initialState = simulation->getState(); + EXPECT_FALSE(initialState.empty()); + std::vector>> controllerStates; + for (const auto busId : {1, 2, 3, 6, 8}) { + auto* bus = dynamic_cast(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(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) @@ -277,3 +358,86 @@ 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, 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::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(simulation->findByUserID("bus", 1)); + ASSERT_NE(bus, nullptr); + auto* generator = dynamic_cast(bus->getGen(0)); + ASSERT_NE(generator, nullptr); + auto event = std::make_shared(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(); + ASSERT_EQ(times.size(), expectedOmega.size()); + + for (std::size_t timeIndex = 0; timeIndex < times.size(); ++timeIndex) { + simulation->run(times[timeIndex].get()); + for (std::size_t generatorIndex = 0; generatorIndex < busIds.size(); ++generatorIndex) { + auto* sampleBus = dynamic_cast( + simulation->findByUserID("bus", busIds[generatorIndex].get())); + ASSERT_NE(sampleBus, nullptr); + auto* sampleGenerator = dynamic_cast(sampleBus->getGen(0)); + ASSERT_NE(sampleGenerator, nullptr); + auto* genModel = dynamic_cast( + 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(), + omegaTolerance) + << "time=" << times[timeIndex].get() + << " bus=" << busIds[generatorIndex].get(); + } + } +} diff --git a/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json b/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json new file mode 100644 index 000000000..17ad06523 --- /dev/null +++ b/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json @@ -0,0 +1,22 @@ +{ + "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] + ] +} From 874a4515cd4a72ff41ea13f99276d7e5b7d197e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:14:25 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/developer-guide/andes-compatibility.md | 16 ++++++++-------- .../andes_ieee14_tgov1_trajectory_reference.json | 10 ++++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index 82e89547a..191785467 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -635,13 +635,13 @@ 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. | +| 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 @@ -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, including a captured IEEE 14 GENROU+TGOV1 `Alter(pref0)` trajectory. | 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 | diff --git a/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json b/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json index 17ad06523..1891490c2 100644 --- a/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json +++ b/test/test_files/andes_tests/andes_ieee14_tgov1_trajectory_reference.json @@ -16,7 +16,13 @@ [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] + [ + 0.9999257581611178, 0.9999329128852735, 0.9999337708245503, + 0.9999419710186372, 0.9999486153149914 + ], + [ + 0.9998560751145729, 0.9998714086888447, 0.9998719045372232, + 0.9998778262709337, 0.9998801796876503 + ] ] } From 84a6e7fa335dc48705399f0e1e86c354d1935f87 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 24 Aug 2026 07:46:06 -0700 Subject: [PATCH 3/3] add more dynamic tests --- test/andesTests/testAndesDyrReader.cpp | 75 +++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/test/andesTests/testAndesDyrReader.cpp b/test/andesTests/testAndesDyrReader.cpp index 7a970e1c4..e87d88859 100644 --- a/test/andesTests/testAndesDyrReader.cpp +++ b/test/andesTests/testAndesDyrReader.cpp @@ -18,6 +18,7 @@ #include "griddyn/governors/GovernorIeeeG1.h" #include "griddyn/governors/GovernorTgov1.h" #include +#include #include #include #include @@ -37,7 +38,8 @@ std::string makeAndesTestPath(std::string_view fileName) return std::string{andesTestDirectory} + std::string{fileName}; } -std::vector runLoadStepCase(const std::vector& dyrFiles) +std::vector runLoadStepCase(const std::vector& dyrFiles, + double loadStep = 0.5) { auto simulation = std::make_unique(); griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw")); @@ -57,7 +59,7 @@ std::vector runLoadStepCase(const std::vector& dyrFile if (!event->isArmed()) { return {}; } - event->setValue(initialLoad + 0.5); + event->setValue(initialLoad + loadStep); simulation->add(event); EXPECT_EQ(simulation->dynInitialize(), 0); @@ -390,6 +392,19 @@ TEST(AndesDynamicTests, CombinedControllersRespondToLoadStep) 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")); @@ -441,3 +456,59 @@ TEST(AndesDynamicTests, Tgov1TrajectoryMatchesAndesReference) } } } + +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 times{0.0, 0.5, 1.0, 1.5, 2.0}; + constexpr std::array, 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::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(simulation->findByUserID("bus", 1)); + ASSERT_NE(bus, nullptr); + auto* generator = dynamic_cast(bus->getGen(0)); + ASSERT_NE(generator, nullptr); + auto event = std::make_shared(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( + simulation->findByUserID("bus", + std::array{1, 2, 3, 6, 8}[generatorIndex])); + ASSERT_NE(sampleBus, nullptr); + auto* sampleGenerator = dynamic_cast(sampleBus->getGen(0)); + ASSERT_NE(sampleGenerator, nullptr); + auto* genModel = dynamic_cast( + 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; + } + } +}