From 03946fa94be9e57db4f1db3240e26f56c070fb8d Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Sun, 11 Oct 2026 15:24:38 -0400 Subject: [PATCH 1/3] Add Enzyme vs DependencyTracking system Jacobian test over all PhasorDynamics cases. --- CHANGELOG.md | 2 + GridKit/Utilities/CliArgs/ArgVector.hpp | 1 + cases/PhasorDynamics/Toy/README.md | 2 +- .../Toy/ThreeBusConstantSource.case.json | 16 +- tests/IntegrationTests/CMakeLists.txt | 4 +- .../PhasorDynamics/CMakeLists.txt | 26 +- .../SystemJacobian/CMakeLists.txt | 13 + .../SystemJacobian/SystemJacobianTests.hpp | 281 ++++++++++++++++++ .../SystemJacobian/runSystemJacobianTests.cpp | 56 ++++ 9 files changed, 376 insertions(+), 25 deletions(-) create mode 100644 tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt create mode 100644 tests/IntegrationTests/PhasorDynamics/SystemJacobian/SystemJacobianTests.hpp create mode 100644 tests/IntegrationTests/PhasorDynamics/SystemJacobian/runSystemJacobianTests.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ed853112cd..4d341c72f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Removed `BusToSignalAdapter`. Prefer `BusSignalVoltageOut` or `BusSignalVoltageIn`. - Added `Bus::setFault` to apply or clear a fault to ground directly at a bus. - Removed the `BusFault` class in favor of `Bus::setFault`. +- Added a system Jacobian test comparing Enzyme and DependencyTracking on every case in `cases/PhasorDynamics`. +- Fixed the `ThreeBusConstantSource` case, which still used the removed `BusToSignalAdapter`. ## v0.2 diff --git a/GridKit/Utilities/CliArgs/ArgVector.hpp b/GridKit/Utilities/CliArgs/ArgVector.hpp index ef2617c813..928ba415f7 100644 --- a/GridKit/Utilities/CliArgs/ArgVector.hpp +++ b/GridKit/Utilities/CliArgs/ArgVector.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include #include #include diff --git a/cases/PhasorDynamics/Toy/README.md b/cases/PhasorDynamics/Toy/README.md index d4d7a5bf42..2452bfdb9d 100644 --- a/cases/PhasorDynamics/Toy/README.md +++ b/cases/PhasorDynamics/Toy/README.md @@ -10,5 +10,5 @@ Two- and three-bus cases used by the PhasorDynamics examples and integration tes - [TwoBusTgov1](./TwoBusTgov1.case.json): GENROU machine with a TGOV1 governor. - [ThreeBusBasic](./ThreeBusBasic.case.json): GENROU machines and a constant-impedance load. - [ThreeBusClassical](./ThreeBusClassical.case.json): Classical machines and a constant-impedance load. -- [ThreeBusConstantSource](./ThreeBusConstantSource.case.json): GENROU machine with a constant signal source and a bus-to-signal adapter. +- [ThreeBusConstantSource](./ThreeBusConstantSource.case.json): GENROU machine on a `SignalVoltageOut` bus fed by a constant signal source. - [ThreeBusZipLoad](./ThreeBusZipLoad.case.json): Classical machines and a ZIP load. diff --git a/cases/PhasorDynamics/Toy/ThreeBusConstantSource.case.json b/cases/PhasorDynamics/Toy/ThreeBusConstantSource.case.json index 0d96ca1933..d60ae9179e 100644 --- a/cases/PhasorDynamics/Toy/ThreeBusConstantSource.case.json +++ b/cases/PhasorDynamics/Toy/ThreeBusConstantSource.case.json @@ -34,7 +34,11 @@ }, { "number": 2, - "class": "Bus", + "class": "SignalVoltageOut", + "ports_in": { + "ir": 1, + "ii": 2 + }, "name": "2", "init": { "Vr": 1.0599558398065716, @@ -157,16 +161,6 @@ "speed" ] }, - { - "class": "BusToSignalAdapter", - "ports": { - "bus": 1, - "ir": 1, - "ii": 2 - }, - "id": "adapt3", - "params": {} - }, { "class": "ConstantSignalSource", "ports": { diff --git a/tests/IntegrationTests/CMakeLists.txt b/tests/IntegrationTests/CMakeLists.txt index ee9cfd8753..6c0295831d 100644 --- a/tests/IntegrationTests/CMakeLists.txt +++ b/tests/IntegrationTests/CMakeLists.txt @@ -1,3 +1 @@ -if(TARGET SUNDIALS::idas) - add_subdirectory(PhasorDynamics) -endif() +add_subdirectory(PhasorDynamics) diff --git a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt index 0254ab0c25..b63b447198 100644 --- a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt +++ b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt @@ -1,13 +1,19 @@ -add_executable(test_pd_integration runPDIntegrationTests.cpp) -target_link_libraries( - test_pd_integration - PRIVATE GridKit::phasor_dynamics_systemmodel GridKit::solvers_dyn GridKit::testing) +if(TARGET SUNDIALS::idas) + add_executable(test_pd_integration runPDIntegrationTests.cpp) + target_link_libraries( + test_pd_integration + PRIVATE GridKit::phasor_dynamics_systemmodel GridKit::solvers_dyn GridKit::testing) -add_test( - NAME PhasorDynamicsIntegrationTest - COMMAND test_pd_integration - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/examples/PhasorDynamics/Validation/Toy) + add_test( + NAME PhasorDynamicsIntegrationTest + COMMAND test_pd_integration + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/examples/PhasorDynamics/Validation/Toy) -install(TARGETS test_pd_integration) + install(TARGETS test_pd_integration) -add_subdirectory(TenGen) + add_subdirectory(TenGen) +endif() + +if(GRIDKIT_ENABLE_ENZYME) + add_subdirectory(SystemJacobian) +endif() diff --git a/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt new file mode 100644 index 0000000000..1fb2f98745 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(test_pd_system_jacobian runSystemJacobianTests.cpp) +target_link_libraries( + test_pd_system_jacobian + PRIVATE GridKit::phasor_dynamics_systemmodel + GridKit::phasor_dynamics_systemmodel_dependency_tracking + GridKit::utilities_cli_args GridKit::testing) + +# All cases in cases/PhasorDynamics, one sub-test each +add_test( + NAME PhasorDynamicsIntegrationSystemJacobian + COMMAND test_pd_system_jacobian --cases ${CMAKE_SOURCE_DIR}/cases/PhasorDynamics) + +install(TARGETS test_pd_system_jacobian) diff --git a/tests/IntegrationTests/PhasorDynamics/SystemJacobian/SystemJacobianTests.hpp b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/SystemJacobianTests.hpp new file mode 100644 index 0000000000..d7263560f3 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/SystemJacobianTests.hpp @@ -0,0 +1,281 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + using Log = ::GridKit::Utilities::Logger; + + /** + * @brief Compares and times system Jacobians from Enzyme and DependencyTracking. + */ + template + class SystemJacobianTests + { + private: + using RealT = ScalarT; + using ClockT = std::chrono::steady_clock; + using DurationT = std::chrono::duration; + using JacobianT = std::vector; + using ModelDataT = PhasorDynamics::SystemModelData; + + /// Number of timed evaluations after setup and warm-up + static constexpr int num_evaluations_ = 100; + + // Timing table, one entry per case (times in microseconds per call, setup once) + std::vector case_names_; + std::vector sizes_; + std::vector enzyme_nnz_; + std::vector enzyme_setup_; + std::vector enzyme_residual_; + std::vector enzyme_jacobian_; + std::vector dependency_tracking_nnz_; + std::vector dependency_tracking_setup_; + std::vector dependency_tracking_residual_; + std::vector dependency_tracking_jacobian_; + + public: + /** + * @brief Checks the system Jacobian of the case in `case_file`. + */ + TestOutcome jacobian(const std::filesystem::path& case_file) + { + TestStatus success = true; + + // Looser than the component tests' 10 * eps, for the Math::antiwindup terms + auto tol = 10000 * std::numeric_limits::epsilon(); + + const std::string case_name = case_file.stem().stem().string(); + + // Timing table entry, filled by EnzymeJacobian() and DependencyTrackingJacobian() + case_names_.push_back(case_name); + sizes_.push_back(0); + enzyme_nnz_.push_back(0); + enzyme_setup_.push_back(0.0); + enzyme_residual_.push_back(0.0); + enzyme_jacobian_.push_back(0.0); + dependency_tracking_nnz_.push_back(0); + dependency_tracking_setup_.push_back(0.0); + dependency_tracking_residual_.push_back(0.0); + dependency_tracking_jacobian_.push_back(0.0); + + try + { + auto data = PhasorDynamics::parseSystemModelData(case_file); + + // No monitor output + data.monitor_sink.clear(); + + // Jacobian via Enzyme + JacobianT enzyme_jacobian = EnzymeJacobian(data); + + // Jacobian via DependencyTracking + JacobianT dependency_tracking_jacobian = DependencyTrackingJacobian(data); + + // Compare DependencyTracking dependencies to Enzyme's + success *= (dependency_tracking_jacobian.size() == enzyme_jacobian.size()); + for (IdxT i = 0; i < dependency_tracking_jacobian.size() && i < enzyme_jacobian.size(); ++i) + { + success *= (GridKit::Testing::isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol)); + } + } + catch (const std::exception& error) + { + Log::error() << case_name << ": " << error.what() << '\n'; + success = false; + } + + return success.report(("jacobian " + case_name).c_str()); + } + + /** + * @brief Prints the timings of all checked cases, sorted by number of non-zeros. + */ + void printTimings() const + { + // Case column fits the longest case name + IdxT case_width = 4; + for (const auto& case_name : case_names_) + { + if (case_name.size() > case_width) + { + case_width = case_name.size(); + } + } + case_width += 2; + + auto& output_stream = Log::summary(); + + // Restored at the end, the stream is shared + const auto flags = output_stream.flags(); + const auto precision = output_stream.precision(); + + output_stream << "Times in us per call (setup once); ratio = DependencyTracking / Enzyme.\n"; + output_stream << std::left << std::setw(static_cast(case_width)) << "case" + << std::setw(20) << "method" << std::right + << std::setw(7) << "size" + << std::setw(8) << "nnz" + << std::setw(11) << "setup" << std::setw(8) << "ratio" + << std::setw(10) << "residual" << std::setw(8) << "ratio" + << std::setw(10) << "Jacobian" << std::setw(8) << "ratio" << '\n'; + + output_stream << std::fixed << std::setprecision(1); + + std::vector printed(case_names_.size(), false); + for (IdxT row = 0; row < case_names_.size(); ++row) + { + // Case with the fewest non-zeros among those not printed yet + IdxT i = case_names_.size(); + for (IdxT j = 0; j < case_names_.size(); ++j) + { + if (!printed[j] && (i == case_names_.size() || enzyme_nnz_[j] < enzyme_nnz_[i])) + { + i = j; + } + } + printed[i] = true; + + output_stream << std::left << std::setw(static_cast(case_width)) << case_names_[i] + << std::setw(20) << "Enzyme" << std::right + << std::setw(7) << sizes_[i] + << std::setw(8) << enzyme_nnz_[i] + << std::setw(11) << enzyme_setup_[i] << std::setw(8) << "" + << std::setw(10) << enzyme_residual_[i] << std::setw(8) << "" + << std::setw(10) << enzyme_jacobian_[i] << '\n'; + + output_stream << std::left << std::setw(static_cast(case_width)) << case_names_[i] + << std::setw(20) << "DependencyTracking" << std::right + << std::setw(7) << sizes_[i] + << std::setw(8) << dependency_tracking_nnz_[i] + << std::setw(11) << dependency_tracking_setup_[i] + << std::setw(8) << dependency_tracking_setup_[i] / enzyme_setup_[i] + << std::setw(10) << dependency_tracking_residual_[i] + << std::setw(8) << dependency_tracking_residual_[i] / enzyme_residual_[i] + << std::setw(10) << dependency_tracking_jacobian_[i] + << std::setw(8) << dependency_tracking_jacobian_[i] / enzyme_jacobian_[i] << '\n'; + } + + output_stream.flags(flags); + output_stream.precision(precision); + } + + private: + JacobianT DependencyTrackingJacobian(const ModelDataT& data) + { + PhasorDynamics::SystemModel system(data); + system.updateTime(0.0, 1.0); // Set alpha to 1.0 to verify d/dy' terms + + // Setup: the residual tracks the dependencies the Jacobian is built from + auto start = ClockT::now(); + system.allocate(); + system.initialize(); + system.evaluateResidual(); + system.evaluateJacobian(); + auto stop = ClockT::now(); + + const DurationT setup = stop - start; + + // Warm-up, untimed + system.evaluateResidual(); + system.evaluateJacobian(); + + start = ClockT::now(); + for (int i = 0; i < num_evaluations_; ++i) + { + system.evaluateResidual(); + } + stop = ClockT::now(); + + const DurationT residual = (stop - start) / num_evaluations_; + + start = ClockT::now(); + for (int i = 0; i < num_evaluations_; ++i) + { + system.evaluateJacobian(); + } + stop = ClockT::now(); + + const DurationT jacobian = (stop - start) / num_evaluations_; + + auto* system_jacobian = system.getCsrJacobian(); + + sizes_.back() = system.size(); + dependency_tracking_nnz_.back() = system_jacobian->getNnz(); + dependency_tracking_setup_.back() = setup.count(); + dependency_tracking_residual_.back() = residual.count(); + dependency_tracking_jacobian_.back() = jacobian.count(); + + auto& output_stream = Log::misc(); + output_stream << "Sparse Csr Matrix: System Jacobian with DependencyTracking\n"; + system_jacobian->print(output_stream); + + return GridKit::Testing::MapFromCsr(system_jacobian); + } + + JacobianT EnzymeJacobian(const ModelDataT& data) + { + PhasorDynamics::SystemModel system(data); + system.updateTime(0.0, 1.0); // Set alpha to 1.0 to verify d/dy' terms + + // Setup: allocate() also initializes and evaluates the residual and the first Jacobian + auto start = ClockT::now(); + system.allocate(); + auto stop = ClockT::now(); + + const DurationT setup = stop - start; + + // Warm-up, untimed + system.evaluateResidual(); + system.evaluateJacobian(); + + start = ClockT::now(); + for (int i = 0; i < num_evaluations_; ++i) + { + system.evaluateResidual(); + } + stop = ClockT::now(); + + const DurationT residual = (stop - start) / num_evaluations_; + + start = ClockT::now(); + for (int i = 0; i < num_evaluations_; ++i) + { + system.evaluateJacobian(); + } + stop = ClockT::now(); + + const DurationT jacobian = (stop - start) / num_evaluations_; + + auto* system_jacobian = system.getCsrJacobian(); + + sizes_.back() = system.size(); + enzyme_nnz_.back() = system_jacobian->getNnz(); + enzyme_setup_.back() = setup.count(); + enzyme_residual_.back() = residual.count(); + enzyme_jacobian_.back() = jacobian.count(); + + auto& output_stream = Log::misc(); + output_stream << "Sparse Csr Matrix: System Jacobian with Enzyme\n"; + system_jacobian->print(output_stream); + + return GridKit::Testing::MapFromCsr(system_jacobian); + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/IntegrationTests/PhasorDynamics/SystemJacobian/runSystemJacobianTests.cpp b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/runSystemJacobianTests.cpp new file mode 100644 index 0000000000..f46ed6808a --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/runSystemJacobianTests.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#include + +#include "SystemJacobianTests.hpp" + +int main(int argc, const char* argv[]) +{ + using Log = GridKit::Utilities::Logger; + + // Print timings + Log::raiseVerbosity(Log::SUMMARY); + + GridKit::Utilities::CliArgs args{{.name = {"--cases", "-c"}, + .help = "Case file, or directory searched recursively for *.case.json", + .required = true}}; + args.parseArgs(argc, argv); + + const std::filesystem::path path = args["cases"](); + + std::vector case_files; + if (std::filesystem::is_directory(path)) + { + for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) + { + if (entry.is_regular_file() && entry.path().filename().string().ends_with(".case.json")) + { + case_files.push_back(entry.path()); + } + } + } + else + { + case_files.push_back(path); + } + + if (case_files.empty()) + { + Log::error() << "No case files found\n"; + return 1; + } + + GridKit::Testing::TestingResults result; + GridKit::Testing::SystemJacobianTests test; + + for (const auto& case_file : case_files) + { + result += test.jacobian(case_file); + } + + test.printTimings(); + + return result.summary(); +} From 5cd6ec1f0f34222984c8ad69c6aea33b741106fa Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 14 Oct 2026 10:51:27 -0400 Subject: [PATCH 2/3] Revert CHANGELOG update for now. --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d341c72f7..ed853112cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ - Removed `BusToSignalAdapter`. Prefer `BusSignalVoltageOut` or `BusSignalVoltageIn`. - Added `Bus::setFault` to apply or clear a fault to ground directly at a bus. - Removed the `BusFault` class in favor of `Bus::setFault`. -- Added a system Jacobian test comparing Enzyme and DependencyTracking on every case in `cases/PhasorDynamics`. -- Fixed the `ThreeBusConstantSource` case, which still used the removed `BusToSignalAdapter`. ## v0.2 From 4846058343d51f767476bbcb5ad43750fc9a14ef Mon Sep 17 00:00:00 2001 From: nkoukpaizan Date: Wed, 14 Oct 2026 15:28:13 +0000 Subject: [PATCH 3/3] Apply pre-commit fixes --- tests/IntegrationTests/PhasorDynamics/CMakeLists.txt | 5 ++--- .../PhasorDynamics/SystemJacobian/CMakeLists.txt | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt index b63b447198..4da93fddf7 100644 --- a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt +++ b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt @@ -1,8 +1,7 @@ if(TARGET SUNDIALS::idas) add_executable(test_pd_integration runPDIntegrationTests.cpp) - target_link_libraries( - test_pd_integration - PRIVATE GridKit::phasor_dynamics_systemmodel GridKit::solvers_dyn GridKit::testing) + target_link_libraries(test_pd_integration PRIVATE GridKit::phasor_dynamics_systemmodel + GridKit::solvers_dyn GridKit::testing) add_test( NAME PhasorDynamicsIntegrationTest diff --git a/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt index 1fb2f98745..6d08fa0eb5 100644 --- a/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt +++ b/tests/IntegrationTests/PhasorDynamics/SystemJacobian/CMakeLists.txt @@ -3,11 +3,11 @@ target_link_libraries( test_pd_system_jacobian PRIVATE GridKit::phasor_dynamics_systemmodel GridKit::phasor_dynamics_systemmodel_dependency_tracking - GridKit::utilities_cli_args GridKit::testing) + GridKit::utilities_cli_args + GridKit::testing) # All cases in cases/PhasorDynamics, one sub-test each -add_test( - NAME PhasorDynamicsIntegrationSystemJacobian - COMMAND test_pd_system_jacobian --cases ${CMAKE_SOURCE_DIR}/cases/PhasorDynamics) +add_test(NAME PhasorDynamicsIntegrationSystemJacobian + COMMAND test_pd_system_jacobian --cases ${CMAKE_SOURCE_DIR}/cases/PhasorDynamics) install(TARGETS test_pd_system_jacobian)