diff --git a/GridKit/Apps/CMakeLists.txt b/GridKit/Apps/CMakeLists.txt new file mode 100644 index 000000000..ee9cfd875 --- /dev/null +++ b/GridKit/Apps/CMakeLists.txt @@ -0,0 +1,3 @@ +if(TARGET SUNDIALS::idas) + add_subdirectory(PhasorDynamics) +endif() diff --git a/GridKit/Apps/PhasorDynamics/CMakeLists.txt b/GridKit/Apps/PhasorDynamics/CMakeLists.txt new file mode 100644 index 000000000..c55b64591 --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/CMakeLists.txt @@ -0,0 +1,11 @@ +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) + +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..87d7960ff --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/PDSim.cpp @@ -0,0 +1,103 @@ +#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]); + + if (!study.name.empty()) + { + Log::summary() << "Study: " << study.name << std::endl; + } + + // Instantiate system + SystemModel sys(study.model_data); + sys.allocate(); + + 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()); + + ida.initializeSimulation(0.0, false); + + real_type curr_time = 0.0; + for (const auto& cue : study.schedule) + { + // Run to scheduled time + int nout = static_cast(std::round((cue.time - curr_time) / dt)); + ida.runSimulation(cue.time, nout); + + // Execute action + const auto& ev = study.event_map.at(cue.event); + if (ev.type == "bus_fault") + { + sys.getBusFault(study.fault_map.at(cue.event))->setStatus(cue.action == "on"); + } + + ida.initializeSimulation(cue.time, false); + curr_time = cue.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_name.empty() && !study.reference_file.empty()) + { + auto errorSet = compareCSV(study.output.file_name, 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..2f77fffed --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/PDSim.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace fs = ::std::filesystem; + + using json = ::nlohmann::json; + using Log = ::GridKit::Utilities::Logger; + + struct SystemEvent + { + std::string id; + std::string type; // "bus_fault", future: "branch_trip", etc. + json params; // type-specific data + }; + + struct Cue + { + double time; + std::string event; // references SystemEvent::id + std::string action; // "on", "off", etc. + }; + + struct StudyData + { + using MonitorSinkSpec = SystemModelData<>::MonitorSinkSpec; + + int format_version{1}; + std::string name; + std::string description; + fs::path case_file; + double dt; + double tmax; + std::vector events; + std::vector schedule; + MonitorSinkSpec output; + fs::path reference_file; + double error_tol; + SystemModelData<> model_data; + std::map event_map; // event id → event + std::map fault_map; // event id → fault index + }; + + inline void from_json(const json& j, StudyData& c) + { + c.format_version = j.value("format_version", 1); + c.name = j.value("study_name", std::string{}); + c.description = j.value("study_description", std::string{}); + + j.at("case_file").get_to(c.case_file); + j.at("dt").get_to(c.dt); + j.at("tmax").get_to(c.tmax); + + if (j.contains("events")) + { + for (auto& raw_event : j.at("events")) + { + auto& ev = c.events.emplace_back(); + raw_event.at("id").get_to(ev.id); + raw_event.at("type").get_to(ev.type); + ev.params = raw_event.at("params"); + } + } + + if (j.contains("schedule")) + { + for (auto& raw_action : j.at("schedule")) + { + auto& sa = c.schedule.emplace_back(); + raw_action.at("time").get_to(sa.time); + raw_action.at("event").get_to(sa.event); + raw_action.at("action").get_to(sa.action); + } + } + + { + using Format = ::GridKit::Model::VariableMonitorFormat; + if (j.contains("output")) + { + const auto& out = j.at("output"); + c.output.file_name = out.value("file_name", std::string{}); + c.output.delim = out.value("delim", std::string(",")); + + auto fmt_str = out.value("format", std::string("CSV")); + std::transform(fmt_str.begin(), fmt_str.end(), fmt_str.begin(), ::toupper); + + if (fmt_str == "CSV") + c.output.format = Format::CSV; + else if (fmt_str == "JSON") + c.output.format = Format::JSON; + else if (fmt_str == "YAML") + c.output.format = Format::YAML; + else + throw std::runtime_error("Invalid output format: \"" + fmt_str + "\""); + } + else + { + c.output.format = Format::CSV; + } + } + + if (j.contains("reference_file")) + { + j.at("reference_file").get_to(c.reference_file); + } + + c.error_tol = j.value("error_tol", 1.0e-4); + } + + inline std::ifstream openFile(const fs::path& file_path) + { + if (!exists(file_path)) + { + Log::error() << "File not found: " << file_path << std::endl; + throw std::runtime_error("File not found: " + file_path.string()); + } + auto fs = std::ifstream(file_path); + if (!fs) + { + Log::error() << "Failed to open file: " << file_path << std::endl; + throw std::runtime_error("Failed to open file: " + file_path.string()); + } + return fs; + } + + inline StudyData parseStudyData(const fs::path& file_path) + { + auto data = StudyData(json::parse(openFile(file_path))); + + if (data.format_version != 1) + { + throw std::runtime_error( + "Unsupported study file format_version: " + std::to_string(data.format_version)); + } + + auto loc = file_path.parent_path(); + if (!data.case_file.is_absolute()) + { + data.case_file = loc / data.case_file; + } + if (!data.reference_file.empty()) + { + if (!data.reference_file.is_absolute()) + { + data.reference_file = loc / data.reference_file; + } + } + if (!data.output.file_name.empty()) + { + fs::path p(data.output.file_name); + if (!p.is_absolute()) + { + data.output.file_name = (loc / p).string(); + } + } + + data.model_data = parseSystemModelData(data.case_file); + + // Build event lookups and inject event data into model + size_t fault_idx = 0; + for (const auto& ev : data.events) + { + data.event_map[ev.id] = ev; + + if (ev.type == "bus_fault") + { + using BFD = BusFaultData; + BFD bfd; + bfd.parameters[BFD::Parameters::R] = ev.params.value("R", 0.0); + bfd.parameters[BFD::Parameters::X] = ev.params.value("X", 1e-5); + bfd.parameters[BFD::Parameters::state0] = false; + bfd.ports[BFD::Ports::bus] = ev.params.at("bus").get(); + data.model_data.bus_fault.push_back(bfd); + data.fault_map[ev.id] = fault_idx++; + } + } + + // Validate schedule + for (size_t i = 0; i < data.schedule.size(); ++i) + { + const auto& cue = data.schedule[i]; + + if (data.event_map.find(cue.event) == data.event_map.end()) + { + throw std::runtime_error( + "Schedule cue at time " + std::to_string(cue.time) + + " references undefined event: \"" + cue.event + "\""); + } + + if (cue.action != "on" && cue.action != "off") + { + throw std::runtime_error( + "Schedule cue at time " + std::to_string(cue.time) + + " has unrecognized action: \"" + cue.action + "\""); + } + + if (i > 0 && cue.time < data.schedule[i - 1].time) + { + throw std::runtime_error( + "Schedule is not sorted by time: cue at " + + std::to_string(cue.time) + " follows " + + std::to_string(data.schedule[i - 1].time)); + } + } + + data.model_data.monitor_sink.clear(); + data.model_data.monitor_sink.push_back(data.output); + + return data; + } + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Apps/PhasorDynamics/README.md b/GridKit/Apps/PhasorDynamics/README.md new file mode 100644 index 000000000..b39f1eb72 --- /dev/null +++ b/GridKit/Apps/PhasorDynamics/README.md @@ -0,0 +1,140 @@ +# PDSim — Phasor Dynamics Study Runner + +## Overview + +PDSim is a command-line application for running parameterized phasor dynamics +simulations. It reads a JSON study file that specifies the system model, +simulation parameters, disturbance events, and output configuration. + +Event definitions (e.g. bus faults) are specified in +the study file rather than in the system model JSON, keeping the model data +purely descriptive. + +### Usage + +``` +PDSim +``` + +## Study file format + +The study file configures a simulation run. All paths are resolved relative +to the study file's directory. + +### Top-level fields + + Name | Required | Description + -------------------|----------|------------------------------------------------------ + `format_version` | No | Format version integer (default: `1`) + `study_name` | No | Short name for the study + `study_description`| No | Free-text description of the study + `case_file` | Yes | Path to the system model JSON file + `dt` | Yes | Simulation time step in seconds + `tmax` | Yes | Simulation end time in seconds + `events` | No | Array of event definitions (see below). Required when simulation includes disturbance events + `schedule` | No | Ordered array of timed cues (see below). Required when simulation includes disturbance events + `output` | No | Monitor output configuration object (see [Output](#output) below) + `reference_file` | No | Path to reference CSV for validation + `error_tol` | No | Maximum allowed error vs reference (default: `1e-4`) + +### Output + +The study file controls where and how monitored variables are written. +Which variables are monitored is determined by the `mon` fields on buses +and devices in the model JSON. + +The `output` object has the following fields: + + Name | Description + -------------------|------------------------------------------------------ + `file_name` | Output file path. If omitted, output is written to `stdout` + `format` | One of { `"CSV"`, `"JSON"`, `"YAML"` } (case-insensitive, default: `"CSV"`) + `delim` | Delimiter string for CSV output (default: `","`) + +If the `output` key is omitted entirely, the default is CSV output to +`stdout`. + +The CSV format has a header row followed by one row per time step. +The first column is always `Time [s]`, the second is `Solver Status` +(0 = success, positive = warning, negative = error), followed by one +column per monitored variable in the order they appear in the model file. + +> In a future version, `output` should become an array of sink objects to +> support multiple simultaneous outputs (e.g., CSV to file and YAML to +> stdout). + +### Events + +Each event defines a disturbance that can be activated during the simulation. +Events are referenced by `id` in the schedule. + +Each event has the following common fields: + + Name | Description + ---------|------------------------------------------------------ + `id` | Unique string identifier for the event + `type` | Event type string (see supported types below) + `params` | Object containing type-specific parameters + +#### `bus_fault` — fault-to-ground at a bus + + Name | Description + ------|------------------------------------------------------ + `bus` | Bus number to fault + `R` | Fault resistance in per-unit (default: `0.0`) + `X` | Fault reactance in per-unit (default: `1e-5`) + +#### `trip_line` — trip a transmission line or transformer *(not yet implemented)* + + Name | Description + -------|------------------------------------------------------ + `bus1` | Bus number at the first end of the branch + `bus2` | Bus number at the second end of the branch + `id` | Device disambiguation string (required when multiple branches connect the same two buses) + +### Schedule + +The schedule is an ordered list of timed cues that trigger event actions: + + Name | Description + ---------|------------------------------------------------------ + `time` | Simulation time to execute the action in seconds + `event` | References an event `id` + `action` | `"on"` to apply, `"off"` to clear + +Each entry in the schedule array is a **cue** — a timed trigger for an event +action. The simulator runs to each cue's time, executes the action, +reinitializes, and continues. After all cues, it runs to `tmax`. + +### Validation + +When both `output.file_name` and `reference_file` are provided, PDSim compares the +simulation output against the reference and reports whether the maximum error +is within `error_tol`. The process exit code is `0` on pass, `1` on +failure. + +(NOTE: I think in future versions we should generalize the reference and error +specification with 'post-process' or something similar.) + +## Example + +```json +{ + "format_version": 1, + "case_file": "ThreeBusBasic.json", + "dt": 0.00416666666666, + "tmax": 10, + "events": [ + {"id": "fault_1", "type": "bus_fault", "params": {"bus": 2, "R": 0.0, "X": 1e-5}}, + {"id": "trip_1", "type": "trip_line", "params": {"bus1": 1, "bus2": 2, "id": "BR1"}} + ], + "schedule": [ + {"time": 1.0, "event": "fault_1", "action": "on"}, + {"time": 1.1, "event": "fault_1", "action": "off"}, + {"time": 1.1, "event": "trip_1", "action": "on"} + ], + "output": {"file_name": "ThreeBus_fault_results.csv"}, + "reference_file": "ThreeBusBasic.ref.csv", + "error_tol": 1e-4 +} +``` 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/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index e033f1e7a..c74650fee 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -55,25 +55,6 @@ Contained in the `header` key is an object with the following items: `freq_base` | A floating point value indicating the system frequency base in hertz (Hz). This is commonly 60 Hz `va_base` | A floating point value indicating the system power base in volt-amperes (VA). This is commonly 100e6 VA -### Monitors - -Contained in the `monitors` key is an array of objects, each of which describes -an output for monitored variables (those listed in the `mon` field of a -[bus](#buses) or [device](#devices). The following fields are supported: - - Name | Description - -------------------|------------------------------------------------------ - `file_name` | Optional string indicating output file name. If omitted, `stdout` is used. - `format` | One of { "CSV", "JSON", "YAML" } (case-insensitive) - `delim` | Optional string specifying delimiter to use for CSV output (default is `","`). - -__NOTE__: If `monitors` entry is omitted entirely, you will get the default CSV -output to `stdout`. If you wish to change the console output format, use an -entry here without the `file_name` field. For example: -```json - "monitors": [ { "format": "YAML" } ] -``` - ### Buses Contained in the `buses` key is an array of objects, each of which represent @@ -147,11 +128,14 @@ are specified: `GenClassical`| the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega` `Tgov1 ` | the TGOV1 governor model | `pmech`, `speed` | `R`, `T1`, `T2`, `T3`, `Pvmax`, `Pvmin`, `Dt` | `none` `Ieeet1` | the IEEET1 exciter model | `bus`, `speed`, `efd` | `Tr`, `Ka`, `Ta`, `Ke`, `Te`, `Kf`, `Tf`, `Vrmin`, `Vrmax`, `E1`, `E2`, `Se1`, `Se2`, `Ispdlim` | `efd`, `ksat` - `BusFault` | simple impedance-based fault at a bus | `bus`, `status`\* | `state0`, `R`, `X` | `state`, `ir`, `ii` Ports marked with \* are optional and, if missing, will be assumed to be connected to a constant value. This list is subject to change. +> **Note:** Fault events (bus faults, etc.) are defined in the study file +> rather than in the system model. See the PDSim study file format +> documentation. + ## Example File for a 2-Bus System @@ -179,8 +163,7 @@ connected to a constant value. This list is subject to change. { "class": "Branch", "ports": {"bus1":1, "bus2":2}, "id": "BR1", "params": {"R":0.0, "X":0.1, "G":0.0, "B":0.0} }, { "class": "Genrou", "ports": {"bus":1, "speed": 1, "pmech":2, "efd":3}, "id": "DV1", "params": {"p0":1.0, "q0":0.05013, "H":3.0, "D":0.0, "Ra":0.0, "Tdop":7.0, "Tdopp":0.04, "Tqopp":0.05, "Tqop":0.75, "Xd":2.1, "Xdp":0.2, "Xdpp":0.18, "Xq":0.5, "Xqp": 0.0, "Xqpp":0.18, "Xl":0.15, "S10":0.0, "S12":0.0}, "mon": ["delta", "omega"] }, { "class": "Tgov1", "ports": {"bus":1, "speed": 1, "pmech":2}, "id": "DV2", "params": {"R":0.05, "T1":0.5,"T2":2.5, "T3":7.5, "Pvmax":0, "Pvmin":1, "Dt":0}}, - { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.001, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1, "Vrmax":1, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0}}, - { "class": "BusFault", "ports": {"bus":1}, "id": "EVT1", "params": {"state0": false, "R":0.0, "X":1e-3} } + { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.001, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1, "Vrmax":1, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0}} ] } ``` 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..538b65655 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt @@ -6,18 +6,11 @@ target_link_libraries(ThreeBusBasic GridKit::solvers_dyn) install(TARGETS ThreeBusBasic RUNTIME DESTINATION ${_install_path}) -add_executable(ThreeBusBasicJson ThreeBusBasicJson.cpp) -target_link_libraries(ThreeBusBasicJson - GridKit::phasor_dynamics_components - GridKit::solvers_dyn - GridKit::testing) -install(TARGETS ThreeBusBasicJson RUNTIME DESTINATION ${_install_path}) gridkit_example_add_file(ThreeBusBasic.json) gridkit_example_add_file(ThreeBusBasic.ref.csv) add_test(NAME ThreeBusBasic COMMAND ThreeBusBasic) -add_test(NAME ThreeBusBasicJson - COMMAND ThreeBusBasicJson ${CMAKE_CURRENT_BINARY_DIR}/ThreeBusBasic.json) -add_test(NAME ThreeBusBasicJson_no_arg - COMMAND ThreeBusBasicJson + +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.json b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.json index aeb3ab7cd..95ed459ae 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.json +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.json @@ -8,12 +8,6 @@ "freq_base": 60.0, "va_base": 100000000.0 }, - "monitors": [ - { - "file_name": "mon.csv", - "format": "csv" - } - ], "buses": [ { "number": 0, @@ -109,18 +103,6 @@ "X": 0.20330047265361242 } }, - { - "class": "BusFault", - "ports": { - "bus": 2 - }, - "id": "bus_fault_2", - "params": { - "R": 0.0, - "X": 1e-5, - "state0": false - } - }, { "class": "Genrou", "ports": { 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..78f2903ef --- /dev/null +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.study.json @@ -0,0 +1,16 @@ +{ + "format_version": 1, + "case_file": "ThreeBusBasic.json", + "dt": 0.00416666666666, + "tmax": 10, + "events": [ + {"id": "fault_1", "type": "bus_fault", "params": {"bus": 2, "R": 0.0, "X": 1e-5}} + ], + "schedule": [ + {"time": 1, "event": "fault_1", "action": "on"}, + {"time": 1.1, "event": "fault_1", "action": "off"} + ], + "output": {"file_name": "ThreeBus_six_cycle_fault_bus1.csv"}, + "reference_file": "ThreeBusBasic.ref.csv", + "error_tol": 1e-4 +} diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp deleted file mode 100644 index 7f99c6653..000000000 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @file ThreeBusBasicJson.cpp - * @author Adam Birchfield (abirchfield@tamu.edu) - * @author Slaven Peles (peless@ornl.gov) - * @brief Example running a 3-bus system - * - * Simulates a 3-bus system with two Genrou 6th order generator models and - * compares results with data generated for the same system by Poweworld. - * - */ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#define ERROR_TOL 1.0e-4 - -using scalar_type = double; -using real_type = double; -using index_type = size_t; - -// NOTES: -// Write function to compare to CSV files -// compare number of lines and number of items per line - -int main(int argc, const char* argv[]) -{ - using namespace GridKit::PhasorDynamics; - using namespace AnalysisManager::Sundials; - using namespace GridKit::Utilities; - - auto error_allowed = static_cast(1e-4); - - // - // Input file - // - - std::filesystem::path input_file; - if (argc < 2) - { - if (std::filesystem::exists("ThreeBusBasic.json")) - { - input_file = std::filesystem::current_path() / "ThreeBusBasic.json"; - } - else - { - std::cout << "\n" - "ERROR: No input file found or provided.\n" - "\n" - "Usage:\n" - " ThreeBusBasicJson \n" - "\n" - "Please provide a JSON input file as a positional command-line \n" - "argument.\n" - "\n" - "By default this example will look for \"ThreeBusBasic.json\" in the \n" - "current working directory and use that if found.\n" - "\n"; - exit(1); - } - } - else - { - input_file = argv[1]; - } - - std::cout << "Example: ThreeBusBasicJson\n"; - std::cout << "Input file: " << input_file << '\n'; - - // - // Create model data - // - - auto data = parseSystemModelData(input_file); - - // - // Instantiate system - // - - SystemModel sys(data); - sys.allocate(); - - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - - real_type dt = 1.0 / 4.0 / 60.0; - - // Set up simulation - Ida ida(&sys); - ida.configureSimulation(); - - // Run simulation, output each `dt` interval - real_type start = static_cast(clock()); - ida.initializeSimulation(0.0, false); - - // Run for 1s - int nout = static_cast(std::round((1.0 - 0.0) / dt)); - ida.runSimulation(1.0, nout); - - // Introduce fault to ground and run for 0.1s - fault->setStatus(true); - ida.initializeSimulation(1.0, false); - nout = static_cast(std::round((1.1 - 1.0) / dt)); - ida.runSimulation(1.1, nout); - - // Clear fault and run until t = 10s. - fault->setStatus(false); - ida.initializeSimulation(1.1, false); - nout = static_cast(std::round((10.0 - 1.1) / dt)); - ida.runSimulation(10.0, nout); - real_type stop = static_cast(clock()); - - // Stop the variable monitor - sys.stopMonitor(); - - // Generate aggregate errors comparing variable output to reference solution - auto errorSet = - GridKit::Testing::compareCSV("mon.csv", "ThreeBusBasic.ref.csv"); - - // Print the errors - errorSet.display(); - - std::cout << "\n\nComplete in " << (stop - start) / CLOCKS_PER_SEC << " seconds\n"; - - return errorSet.total.max_error < error_allowed ? 0 : 1; -}