From 14c48f2cdd870a7b3d3b24bcea41fa63301cc7e0 Mon Sep 17 00:00:00 2001 From: Philip Fackler Date: Thu, 2 Apr 2026 16:13:14 -0500 Subject: [PATCH 1/2] Initial implementation of generic phasor dynamics app and validation test with ThreeBusBasic case --- GridKit/Apps/CMakeLists.txt | 1 + GridKit/Apps/PhasorDynamics/CMakeLists.txt | 12 ++ GridKit/Apps/PhasorDynamics/PDSim.cpp | 104 ++++++++++++ GridKit/Apps/PhasorDynamics/PDSim.hpp | 151 ++++++++++++++++++ GridKit/CMakeLists.txt | 3 + GridKit/Model/VariableMonitor.hpp | 2 +- GridKit/Testing/Testing.hpp | 10 ++ .../Tiny/ThreeBus/Basic/CMakeLists.txt | 4 + .../ThreeBus/Basic/ThreeBusBasic.study.json | 12 ++ 9 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 GridKit/Apps/CMakeLists.txt create mode 100644 GridKit/Apps/PhasorDynamics/CMakeLists.txt create mode 100644 GridKit/Apps/PhasorDynamics/PDSim.cpp create mode 100644 GridKit/Apps/PhasorDynamics/PDSim.hpp create mode 100644 examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.study.json diff --git a/GridKit/Apps/CMakeLists.txt b/GridKit/Apps/CMakeLists.txt new file mode 100644 index 000000000..6c0295831 --- /dev/null +++ b/GridKit/Apps/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(PhasorDynamics) diff --git a/GridKit/Apps/PhasorDynamics/CMakeLists.txt b/GridKit/Apps/PhasorDynamics/CMakeLists.txt new file mode 100644 index 000000000..7e90135bc --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(PDSim PDSim.cpp) +target_link_libraries(PDSim + PUBLIC + GridKit::phasor_dynamics_components + GridKit::solvers_dyn + GridKit::Utilities + GridKit::testing) +target_include_directories(PDSim PRIVATE + ${GRIDKIT_THIRD_PARTY_DIR}/nlohmann-json/include + ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include) + +install(TARGETS PDSim EXPORT gridkit-targets RUNTIME) diff --git a/GridKit/Apps/PhasorDynamics/PDSim.cpp b/GridKit/Apps/PhasorDynamics/PDSim.cpp new file mode 100644 index 000000000..de71bc76a --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/PDSim.cpp @@ -0,0 +1,104 @@ +#include "PDSim.hpp" + +#include +#include + +#include +#include +#include +#include + +using Log = GridKit::Utilities::Logger; + +using namespace GridKit::PhasorDynamics; +using namespace GridKit::Testing; +using namespace AnalysisManager::Sundials; + +using scalar_type = double; +using real_type = double; +using index_type = size_t; + +int main(int argc, const char* argv[]) +{ + // Study file + if (argc < 2) + { + Log::error() << "No input file provided" << std::endl; + std::cout << "\n" + "Usage:\n" + " pdsim \n" + "\n" + "Please provide a json input file for the study to run.\n" + "\n"; + exit(1); + } + + auto study = parseStudyData(argv[1]); + + // Instantiate system + SystemModel sys(study.model_data); + sys.allocate(); + + // Get access to fault 0 + auto* fault = sys.getBusFault(0); + + real_type dt = study.dt; + + // Set up simulation + Ida ida(&sys); + ida.configureSimulation(); + + // Run simulation, output each `dt` interval + real_type start = static_cast(clock()); + + using EventType = SystemEvent::Type; + ida.initializeSimulation(0.0, false); + + real_type curr_time = 0.0; + for (const auto& event : study.events) + { + // Run to event time + int nout = static_cast(std::round((event.time - curr_time) / dt)); + ida.runSimulation(event.time, nout); + + // Set up run for event (to start at event time) + if (event.type == EventType::FAULT_ON) + { + fault->setStatus(true); + } + else if (event.type == EventType::FAULT_OFF) + { + fault->setStatus(false); + } + ida.initializeSimulation(event.time, false); + curr_time = event.time; + } + + // Run to final time + int nout = static_cast(std::round((study.tmax - curr_time) / dt)); + ida.runSimulation(study.tmax, nout); + + real_type stop = static_cast(clock()); + + // Stop the variable monitor + sys.stopMonitor(); + + // Generate aggregate errors comparing variable output to reference solution + std::string func{"monitor file vs reference file"}; + TestStatus status{func.c_str()}; + if (!study.output_file.empty() && !study.reference_file.empty()) + { + auto errorSet = compareCSV(study.output_file, study.reference_file); + + // Print the errors + errorSet.display(); + + status *= errorSet.total.max_error < study.error_tol; + + status.report(); + } + + std::cout << "\n\nComplete in " << (stop - start) / CLOCKS_PER_SEC << " seconds\n"; + + return status.get(); +} diff --git a/GridKit/Apps/PhasorDynamics/PDSim.hpp b/GridKit/Apps/PhasorDynamics/PDSim.hpp new file mode 100644 index 000000000..de2947efe --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/PDSim.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace fs = ::std::filesystem; + + struct SystemEvent + { + enum class Type + { + FAULT_ON, + FAULT_OFF + }; + + double time; + Type type; + std::size_t element_id; + }; + + struct StudyData + { + fs::path system_model_file; + double dt; + double tmax; + std::vector events; + fs::path output_file; + fs::path reference_file; + double error_tol; + SystemModelData<> model_data; + }; + + using json = ::nlohmann::json; + using Log = ::GridKit::Utilities::Logger; + + void from_json(const json& j, StudyData& c) + { + using namespace magic_enum; + + j.at("system_model_file").get_to(c.system_model_file); + j.at("dt").get_to(c.dt); + j.at("tmax").get_to(c.tmax); + + for (auto& raw_event : j.at("events")) + { + auto& event = c.events.emplace_back(); + raw_event.at("time").get_to(event.time); + raw_event.at("element_id").get_to(event.element_id); + + auto type_str = raw_event.at("type").get(); + using EventType = SystemEvent::Type; + auto type_wrap = enum_cast(type_str, case_insensitive); + if (!type_wrap.has_value()) + { + Log::error() << "Unable to parse event type \"" << type_str << "\"\n"; + } + event.type = type_wrap.value(); + } + + if (j.contains("output_file")) + { + j.at("output_file").get_to(c.output_file); + if (!c.output_file.is_absolute()) + { + // c.output_file = + } + } + + if (j.contains("reference_file")) + { + j.at("reference_file").get_to(c.reference_file); + } + + c.error_tol = j.value("error_tolerance", 1.0e-4); + } + + std::ifstream openFile(const fs::path& file_path) + { + if (!exists(file_path)) + { + Log::error() << "File not found: " << file_path << std::endl; + } + auto fs = std::ifstream(file_path); + if (!fs) + { + Log::error() << "Failed to open file: " << file_path << std::endl; + } + return fs; + } + + StudyData parseStudyData(const fs::path& file_path) + { + auto data = StudyData(json::parse(openFile(file_path))); + + auto loc = file_path.parent_path(); + if (!data.system_model_file.is_absolute()) + { + data.system_model_file = loc / data.system_model_file; + } + if (!data.reference_file.empty()) + { + if (!data.reference_file.is_absolute()) + { + data.reference_file = loc / data.reference_file; + } + } + + auto csv = ::GridKit::Model::VariableMonitorFormat::CSV; + data.model_data = parseSystemModelData(data.system_model_file); + std::string model_output_file; + for (const auto& sink : data.model_data.monitor_sink) + { + if (sink.format == csv && sink.delim == ",") + { + model_output_file = sink.file_name; + } + } + if (model_output_file.empty()) + { + data.model_data.monitor_sink.emplace_back(data.output_file, csv); + } + else + { + if (exists(data.output_file)) + { + if ((!is_symlink(data.output_file)) || (read_symlink(data.output_file) != model_output_file)) + { + Log::error() << "Study output file not usable" << std::endl; + } + } + else + { + fs::create_symlink(model_output_file, data.output_file); + } + } + + return data; + } + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/CMakeLists.txt b/GridKit/CMakeLists.txt index 83cca6579..e098539b3 100644 --- a/GridKit/CMakeLists.txt +++ b/GridKit/CMakeLists.txt @@ -28,6 +28,9 @@ add_subdirectory(Solver) # Testing library add_subdirectory(Testing) +# Create applications +add_subdirectory(Apps) + install( FILES Constants.hpp diff --git a/GridKit/Model/VariableMonitor.hpp b/GridKit/Model/VariableMonitor.hpp index c50654aef..3aa064554 100644 --- a/GridKit/Model/VariableMonitor.hpp +++ b/GridKit/Model/VariableMonitor.hpp @@ -79,7 +79,7 @@ namespace GridKit /// Output format Format format; /// Delimiter (used only with CSV format currently) - std::string delim; + std::string delim{","}; }; virtual ~VariableMonitorBase() diff --git a/GridKit/Testing/Testing.hpp b/GridKit/Testing/Testing.hpp index adbe917bb..81b1f9e12 100644 --- a/GridKit/Testing/Testing.hpp +++ b/GridKit/Testing/Testing.hpp @@ -55,6 +55,16 @@ namespace GridKit return *this; } + operator bool() const + { + return outcome_ == TestOutcome::PASS; + } + + int get() const + { + return outcome_; + } + void skipTest() { outcome_ = TestOutcome::SKIP; diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt index 95e0fc098..613eb48b2 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt @@ -21,3 +21,7 @@ add_test(NAME ThreeBusBasicJson add_test(NAME ThreeBusBasicJson_no_arg COMMAND ThreeBusBasicJson WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +add_test(NAME ThreeBusBasic_using_app + COMMAND PDSim ${CMAKE_CURRENT_SOURCE_DIR}/ThreeBusBasic.study.json + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.study.json b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.study.json new file mode 100644 index 000000000..9c835d3dc --- /dev/null +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.study.json @@ -0,0 +1,12 @@ +{ + "system_model_file": "ThreeBusBasic.json", + "dt": 0.00416666666666, + "tmax": 10, + "events": [ + {"time": 1, "type": "fault_on", "element_id": 0}, + {"time": 1.1, "type": "fault_off", "element_id": 0} + ], + "output_file": "ThreeBus_six_cycle_fault_bus1.csv", + "reference_file": "ThreeBusBasic.ref.csv", + "error_tolerance": 1e-4 +} From be8720781bb62c985f26bbff297e30d1e1d9f572 Mon Sep 17 00:00:00 2001 From: PhilipFackler Date: Thu, 2 Apr 2026 21:15:24 +0000 Subject: [PATCH 2/2] Apply pre-commmit fixes --- GridKit/Apps/PhasorDynamics/PDSim.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GridKit/Apps/PhasorDynamics/PDSim.cpp b/GridKit/Apps/PhasorDynamics/PDSim.cpp index de71bc76a..6a1afeda7 100644 --- a/GridKit/Apps/PhasorDynamics/PDSim.cpp +++ b/GridKit/Apps/PhasorDynamics/PDSim.cpp @@ -85,7 +85,7 @@ int main(int argc, const char* argv[]) // Generate aggregate errors comparing variable output to reference solution std::string func{"monitor file vs reference file"}; - TestStatus status{func.c_str()}; + TestStatus status{func.c_str()}; if (!study.output_file.empty() && !study.reference_file.empty()) { auto errorSet = compareCSV(study.output_file, study.reference_file);