From 9967cc509919e0168da8bbedbd9cfbf4789e0ca6 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sat, 25 Apr 2026 07:13:43 -0500 Subject: [PATCH 1/4] Cue and Actions for Components --- .../PhasorDynamics/BusFault/BusFault.hpp | 7 +- .../PhasorDynamics/BusFault/BusFaultImpl.hpp | 14 ++ GridKit/Model/PhasorDynamics/Component.hpp | 21 ++ GridKit/Model/PhasorDynamics/SystemModel.hpp | 49 ++--- application/PhasorDynamics/PDSim.cpp | 36 ++-- application/PhasorDynamics/PDSim.hpp | 196 +++++++++--------- application/PhasorDynamics/README.md | 128 ++++++++++-- examples/PhasorDynamics/Large/Texas/texas.cpp | 13 +- .../Medium/Hawaii/hawaiiJson.cpp | 7 +- .../Medium/NewEngland/newengland.cpp | 7 +- .../TenGen/Classical/TenGenClassical.cpp | 4 +- .../Small/TenGen/Genrou/TenGenGenrou.cpp | 4 +- .../Tiny/ThreeBus/Basic/ThreeBusBasic.cpp | 8 +- .../ThreeBus/Basic/ThreeBusBasic.solver.json | 21 +- .../Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp | 7 +- .../ThreeBus/Classical/ThreeBusClassical.cpp | 8 +- .../Classical/ThreeBusClassicalJson.cpp | 7 +- .../ThreeBus/ZipLoad/ThreeBusZipLoadJson.cpp | 7 +- .../Tiny/TwoBus/Basic/TwoBusBasic.cpp | 8 +- .../Tiny/TwoBus/Basic/TwoBusBasic.solver.json | 19 +- .../Tiny/TwoBus/Basic/TwoBusBasicJson.cpp | 7 +- .../Tiny/TwoBus/Ieeet1/TwoBusIeeet1.cpp | 8 +- .../Tiny/TwoBus/Ieeet1/TwoBusIeeet1Json.cpp | 9 +- .../Tiny/TwoBus/Tgov1/TwoBusTgov1.cpp | 8 +- .../Tiny/TwoBus/Tgov1/TwoBusTgov1Json.cpp | 7 +- .../SystemSingleComponentTests.hpp | 29 ++- 26 files changed, 365 insertions(+), 274 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/BusFault/BusFault.hpp b/GridKit/Model/PhasorDynamics/BusFault/BusFault.hpp index ba0d4784f..fd1130f7e 100644 --- a/GridKit/Model/PhasorDynamics/BusFault/BusFault.hpp +++ b/GridKit/Model/PhasorDynamics/BusFault/BusFault.hpp @@ -64,6 +64,8 @@ namespace GridKit { } + void apply(Action action) override final; + public: void setR(RealT R) { @@ -77,11 +79,6 @@ namespace GridKit setDerivedParams(); } - void setStatus(bool status) - { - status_ = status; - } - const Model::VariableMonitorBase* getMonitor() const override; private: diff --git a/GridKit/Model/PhasorDynamics/BusFault/BusFaultImpl.hpp b/GridKit/Model/PhasorDynamics/BusFault/BusFaultImpl.hpp index 97761a533..9410b7d89 100644 --- a/GridKit/Model/PhasorDynamics/BusFault/BusFaultImpl.hpp +++ b/GridKit/Model/PhasorDynamics/BusFault/BusFaultImpl.hpp @@ -184,6 +184,20 @@ namespace GridKit return monitor_.get(); } + template + void BusFault::apply(Action action) + { + switch (action) + { + case Action::On: + status_ = true; + break; + case Action::Off: + status_ = false; + break; + } + } + /** * @brief Derived parameters * diff --git a/GridKit/Model/PhasorDynamics/Component.hpp b/GridKit/Model/PhasorDynamics/Component.hpp index 53de53b38..95cdc585e 100644 --- a/GridKit/Model/PhasorDynamics/Component.hpp +++ b/GridKit/Model/PhasorDynamics/Component.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -13,6 +15,16 @@ namespace GridKit { using Log = ::GridKit::Utilities::Logger; + /// Project-wide control actions. Today only BusFault uses cues, so this + /// vocabulary is small. When a second action-bearing component arrives, + /// either add its actions here (if they read naturally as shared verbs) + /// or switch to per-component action enums + typed dispatch. + enum class Action + { + On, + Off, + }; + /** * @brief Component model implementation base class. */ @@ -43,6 +55,15 @@ namespace GridKit virtual int verify() const = 0; + /// Cue dispatch. Called by PDSim only at IDA re-init points; never + /// during integration. Default throws — Components without a runtime + /// control surface inherit the default and ignore cues. + /// SystemModel's routing layer wraps the throw with the target id. + virtual void apply(Action) + { + throw std::runtime_error("Component does not accept cues"); + } + virtual IdxT size() override { return size_; diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index 455bb54ad..f50ce66a4 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -2,6 +2,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -308,7 +312,7 @@ namespace GridKit bus_index = faultdata.ports.at(BusFaultData::Ports::bus); } auto* fault = new BusFault(getBus(bus_index), faultdata); - addFault(fault); + addComponent(fault, faultdata.disambiguation_string); } for (const auto& sink : data.monitor_sink) @@ -905,11 +909,8 @@ namespace GridKit /** * @brief Add component * - * Add component at the end of the components array and set GridKit's component ID - * - * @todo: No integer user-facing component_id for now, but we could map GridKit's - * component ID to the disambiguation_string - * + * Add component at the end of the components array and set GridKit's component ID. + * This overload does not register the component for cue routing. */ void addComponent(component_type* component) { @@ -919,18 +920,17 @@ namespace GridKit } /** - * @brief Add fault - * - * The fault is added to the components array, and we keep a map to its - * location, so it can easily be accessed. + * @brief Add component and register it as a cue target under `id`. * + * The id originates from the case JSON (`"id"` field, parsed into + * `data.disambiguation_string`). It is stored only here, in the system's + * routing map — the component itself does not carry it. */ - void addFault(component_type* component) + void addComponent(component_type* component, const std::string& id) { - IdxT gridkit_component_id = static_cast(components_.size()); - IdxT gridkit_fault_id = static_cast(gridkit_fault_indices_.size()); - gridkit_fault_indices_[gridkit_fault_id] = gridkit_component_id; addComponent(component); + if (!id.empty()) + by_id_[id] = component; } /** @@ -967,27 +967,20 @@ namespace GridKit return components_[gridkit_component_id]; } - /** - * @brief Return pointer to a bus fault model - * - * This function is used to provide easier access to setting and - * clearing faults from the SystemModel interface. - * - */ - BusFault* getBusFault(IdxT fault_id) + /// Route a cue to the component registered under `target`. + void cue(const std::string& target, Action action) { - IdxT component_id = gridkit_fault_indices_.at(fault_id); - return dynamic_cast*>(components_[component_id]); + by_id_.at(target)->apply(action); } private: - std::vector buses_; - std::vector signals_; - std::vector components_; + std::vector buses_; + std::vector signals_; + std::vector components_; + std::unordered_map by_id_; ///< Routing map for cues std::map gridkit_bus_indices_; ///< Map between gridkit_bus_id and bus_id std::map gridkit_signal_indices_; ///< Map between gridkit_signal_id and signal_id - std::map gridkit_fault_indices_; ///< Map between fault_id and component_id bool owns_components_{false}; diff --git a/application/PhasorDynamics/PDSim.cpp b/application/PhasorDynamics/PDSim.cpp index 47d67b5bc..749b49a15 100644 --- a/application/PhasorDynamics/PDSim.cpp +++ b/application/PhasorDynamics/PDSim.cpp @@ -26,9 +26,9 @@ int main(int argc, const char* argv[]) Log::error() << "No input file provided" << std::endl; std::cout << "\n" "Usage:\n" - " pdsim \n" + " pdsim \n" "\n" - "Please provide a json input file for the study to run.\n" + "Please provide a JSON solver file for the study to run.\n" "\n"; exit(1); } @@ -48,30 +48,30 @@ int main(int argc, const char* argv[]) // Start timer real_type start = static_cast(clock()); - using EventType = SystemEvent::Type; - // Initilize simultation for first run ida.initializeSimulation(0.0, false); real_type curr_time = 0.0; - for (const auto& event : study.events) + + size_t i = 0; + while (i < study.schedule.size()) { - // Run to event time - int nout = static_cast(std::round((event.time - curr_time) / dt)); - ida.runSimulation(event.time, nout); + real_type t_cue = study.schedule[i].time; - // Set up run for event (to start at event time) - if (event.type == EventType::FAULT_ON) - { - sys.getBusFault(event.element_id)->setStatus(true); - } - else if (event.type == EventType::FAULT_OFF) + // Run to cue time + int nout = static_cast(std::round((t_cue - curr_time) / dt)); + ida.runSimulation(t_cue, nout); + + // Batch simultaneous cues into one re-init so IDA never sees a + // zero-length segment between back-to-back applies at the same time. + while (i < study.schedule.size() && study.schedule[i].time == t_cue) { - sys.getBusFault(event.element_id)->setStatus(false); + sys.cue(study.schedule[i].target, study.schedule[i].action); + ++i; } - // Re-initialize simulation at event time - ida.initializeSimulation(event.time, true); - curr_time = event.time; + // Re-initialize simulation at cue time + ida.initializeSimulation(t_cue, true); + curr_time = t_cue; } // Run to final time diff --git a/application/PhasorDynamics/PDSim.hpp b/application/PhasorDynamics/PDSim.hpp index 45a976a0b..479e05dae 100644 --- a/application/PhasorDynamics/PDSim.hpp +++ b/application/PhasorDynamics/PDSim.hpp @@ -1,12 +1,16 @@ #pragma once +#include #include #include +#include +#include #include -#include #include +#include +#include #include #include @@ -16,135 +20,120 @@ namespace GridKit { namespace fs = ::std::filesystem; - /** - * @brief Describes an event that is used to modify the simulation at the - * given time point - */ - struct SystemEvent + using json = ::nlohmann::json; + using Log = ::GridKit::Utilities::Logger; + + /// One scheduled cue: at `time`, deliver `action` to `target`. + /// Consumed by PDSim — `time` drives integration, `target`+`action` are + /// forwarded to `SystemModel::cue`. Components themselves never see this. + struct Cue { - /// Type of event determines action performed - enum class Type - { - FAULT_ON, - FAULT_OFF - }; - - /// Time event takes place - double time; - /// Event type - Type type; - /// ID of element used in event (e.g., bus fault id) - std::size_t element_id; + double time{0.0}; + std::string target; + Action action{Action::Off}; }; - /** - * @brief Data defined in JSON file for parameterized study - */ struct StudyData { - /// path to system model JSON file - fs::path system_model_file; - /// time step size - double dt; - /// max time - double tmax; - /// set of system events - std::vector events; - /// path to output file - fs::path output_file; - /// path to reference file for validation - fs::path reference_file; - /// Error tolerance (between output file and reference file) - double error_tol; - /// Instance of model data - SystemModelData<> model_data; + int format_version{1}; + fs::path case_file; + double dt{0.0}; + double tmax{0.0}; + std::vector schedule; + fs::path output_file; + fs::path reference_file; + double error_tol{1.0e-4}; + SystemModelData<> model_data; }; - using json = ::nlohmann::json; - using Log = ::GridKit::Utilities::Logger; - - /** - * @brief JSON parser implemntation for `StudyData` - */ - 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 (j.contains("reference_file")) - { - j.at("reference_file").get_to(c.reference_file); - } - - c.error_tol = j.value("error_tolerance", 1.0e-4); - } - - /** - * @brief Check for existence and successful input file open - */ - std::ifstream openFile(const fs::path& file_path) + 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; } - /** - * @brief Wrapper function to parse `StudyData` from JSON and perform - * follow-up configuration - */ - StudyData parseStudyData(const fs::path& file_path) + inline StudyData parseStudyData(const fs::path& file_path) { - auto data = StudyData(json::parse(openFile(file_path))); + auto raw = json::parse(openFile(file_path)); + + StudyData data; + + data.format_version = raw.value("format_version", 1); + if (data.format_version != 1) + { + throw std::runtime_error( + "Unsupported solver file format_version: " + + std::to_string(data.format_version)); + } + raw.at("case_file").get_to(data.case_file); auto loc = file_path.parent_path(); - if (!data.system_model_file.is_absolute()) + if (!data.case_file.is_absolute()) { - data.system_model_file = loc / data.system_model_file; + data.case_file = loc / data.case_file; } - if (!data.reference_file.empty()) + + raw.at("dt").get_to(data.dt); + raw.at("tmax").get_to(data.tmax); + + if (raw.contains("output_file")) + { + raw.at("output_file").get_to(data.output_file); + } + if (raw.contains("reference_file")) { + raw.at("reference_file").get_to(data.reference_file); if (!data.reference_file.is_absolute()) { data.reference_file = loc / data.reference_file; } } + data.error_tol = raw.value("error_tolerance", 1.0e-4); - auto csv = ::GridKit::Model::VariableMonitorFormat::CSV; - data.model_data = parseSystemModelData(data.system_model_file); + data.model_data = parseSystemModelData(data.case_file); + + if (raw.contains("schedule")) + { + double last_time = -std::numeric_limits::infinity(); + for (const auto& raw_cue : raw.at("schedule")) + { + Cue cue; + raw_cue.at("time").get_to(cue.time); + raw_cue.at("target").get_to(cue.target); + + std::string action_str; + raw_cue.at("action").get_to(action_str); + if (action_str == "on") + cue.action = Action::On; + else if (action_str == "off") + cue.action = Action::Off; + else + throw std::runtime_error( + "Solver file: unknown action '" + action_str + "' (valid: on, off)"); + + assert(cue.time <= data.tmax); + if (cue.time < last_time) + { + throw std::runtime_error( + "Solver file: schedule is not monotone non-decreasing in time"); + } + last_time = cue.time; + data.schedule.push_back(std::move(cue)); + } + } + + auto csv = ::GridKit::Model::VariableMonitorFormat::CSV; std::string model_output_file; - // Find output file (CSV) specified in model input file for (const auto& sink : data.model_data.monitor_sink) { if (sink.format == csv && sink.delim == ",") @@ -155,8 +144,10 @@ namespace GridKit if (model_output_file.empty()) { - // Add study output file to model if one did not already exist - data.model_data.monitor_sink.emplace_back(data.output_file, csv); + if (!data.output_file.empty()) + { + data.model_data.monitor_sink.emplace_back(data.output_file.string(), csv); + } } else { @@ -164,13 +155,12 @@ namespace GridKit { data.output_file = model_output_file; } - else + else if (data.output_file.string() != model_output_file) { - // If model file already specifies a CSV output file, then the study - // output file must be a symlink to the model output file if (exists(data.output_file)) { - if ((!is_symlink(data.output_file)) || (read_symlink(data.output_file) != model_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; } diff --git a/application/PhasorDynamics/README.md b/application/PhasorDynamics/README.md index c3da80264..478f1a811 100644 --- a/application/PhasorDynamics/README.md +++ b/application/PhasorDynamics/README.md @@ -1,25 +1,115 @@ -# Input file for GridKit phasor dynamics application +# PDSim — Phasor Dynamics Study Runner -## Root elements +## Overview - Name | Value - ---------------------|------------------------------------------------------- - `system_model_file` | Path to the system model file[^1] - `dt` | A floating point value for time step size - `tmax` | A floating point value for max time - `events` | An array of event groups (see [Events](#events) below) - `output_file` | Path to output (CSV) file - `reference_file` | A string containing the name of the case - `error_tolerance` | A string containing the name of the case +PDSim is a command-line application for running parameterized phasor dynamics +simulations. It reads a JSON solver file that specifies the case (system +model) to load, the simulation parameters, an optional schedule of runtime +cues, and output configuration. -[^1]: See system model [case format](../../Model/PhasorDynamics/INPUT_FORMAT.md) +Components that respond to runtime control (e.g. `BusFault`) are declared in +the case file as ordinary devices. The solver file's `schedule` then drives +their state changes by referencing each component's `id` from the case file. -## Events +### Usage -Each event group describes a system event that occurs at a given time point +``` +PDSim +``` - Name | Value - --------------------|------------------------------------------------------- - `time` | A floating point value for time event occurs - `type` | Event type (one of { "fault_on", "fault_off" }) - `element_id` | An integer value referencing the element associated with the event (e.g., bus fault id) +## Solver file format + +The solver file configures a simulation run. All paths are resolved relative +to the solver file's directory. + +### Top-level fields + + Name | Required | Description + -------------------|----------|------------------------------------------------------ + `format_version` | No | Format version integer (default: `1`) + `case_file` | Yes | Path to the case (system model) JSON file + `dt` | Yes | Simulation time step in seconds + `tmax` | Yes | Simulation end time in seconds + `schedule` | No | Ordered array of timed cues (see below) + `output_file` | No | Path to monitor output file (CSV). If omitted, writes to a default sink declared in the case file + `reference_file` | No | Path to reference CSV for validation + `error_tolerance` | No | Maximum allowed error vs reference (default: `1e-4`) + +### Output + +Which variables are monitored — and in what format — is configured by the +case file (the `mon` fields on buses and devices, and the monitor sink +declarations). The solver file's `output_file` only chooses the destination +path for the study's CSV sink; everything else about the output (format, +delimiter, monitored variables) is a model property. + +### Schedule + +The schedule is an ordered list of timed **cues**. Each cue addresses a +device declared in the case file by its `id` and asks it to perform an +action at the given simulation time. + + Name | Description + ---------|------------------------------------------------------ + `time` | Simulation time to fire the cue, in seconds (must be ≤ `tmax`) + `target` | Device `id` from the case file (e.g. a `BusFault` device's `id`) + `action` | Action verb interpreted by the target device's class + +The schedule must be non-decreasing in time; ties are allowed for +simultaneous cues (e.g. trip line A and B at the same instant). PDSim +runs to each distinct cue time, applies every cue scheduled at that +instant, re-initializes for a consistent IC, and continues. After the +last cue, it runs to `tmax`. + +#### Devices that accept cues + + Class | Action vocabulary | Effect + ------------|-------------------|---------------------------------------------- + `BusFault` | `on`, `off` | Engage or clear the fault impedance at the bus + +A device is cue-targetable only if its `id` is referenced by the schedule. +Devices whose class does not accept cues (or unknown actions) cause +PDSim to fail with a clear error. + +### Validation + +When both `output_file` and `reference_file` are provided, PDSim compares +the simulation output against the reference and reports whether the +maximum error is within `error_tolerance`. The process exit code is `0` +on pass, `1` on failure. + +## Example + +Solver file: + +```json +{ + "format_version": 1, + "case_file": "ThreeBusBasic.case.json", + "dt": 0.00416666666666, + "tmax": 10, + "schedule": [ + { "time": 1.0, "target": "fault_1", "action": "on" }, + { "time": 1.1, "target": "fault_1", "action": "off" } + ], + "output_file": "ThreeBus_six_cycle_fault_bus1.csv", + "reference_file": "ThreeBusBasic.ref.csv", + "error_tolerance": 1e-4 +} +``` + +The corresponding case file declares the `BusFault` device whose `id` is +referenced as `target`: + +```json +{ + "devices": [ + { + "class": "BusFault", + "id": "fault_1", + "ports": { "bus": 2 }, + "params": { "R": 0.0, "X": 1e-5, "state0": false } + } + ] +} +``` diff --git a/examples/PhasorDynamics/Large/Texas/texas.cpp b/examples/PhasorDynamics/Large/Texas/texas.cpp index 4d9b43856..6e8d62786 100644 --- a/examples/PhasorDynamics/Large/Texas/texas.cpp +++ b/examples/PhasorDynamics/Large/Texas/texas.cpp @@ -79,15 +79,6 @@ int main(int argc, const char* argv[]) real_type start = static_cast(clock()); std::string phase = "setup"; - // Get access to fault 0. - auto* fault = sys.getBusFault(0); - if (fault == nullptr) - { - std::cerr << "ERROR: Texas input is missing BusFault component at index 0.\n"; - sys.stopMonitor(); - return 2; - } - try { phase = "pre-fault"; @@ -98,14 +89,14 @@ int main(int argc, const char* argv[]) phase = "fault"; std::cout << "[fault] t = 10.0 -> 10.15\n"; - fault->setStatus(true); + sys.cue("fault_1", Action::On); ida.initializeSimulation(10.0, false); nout = static_cast(std::round((10.15 - 10.0) / dt)); ida.runSimulation(10.15, nout); phase = "post-fault"; std::cout << "[post-fault] t = 10.15 -> 20.0\n"; - fault->setStatus(false); + sys.cue("fault_1", Action::Off); ida.initializeSimulation(10.15, false); nout = static_cast(std::round((20.0 - 10.15) / dt)); ida.runSimulation(20.0, nout); diff --git a/examples/PhasorDynamics/Medium/Hawaii/hawaiiJson.cpp b/examples/PhasorDynamics/Medium/Hawaii/hawaiiJson.cpp index a3018eadc..28db83f8d 100644 --- a/examples/PhasorDynamics/Medium/Hawaii/hawaiiJson.cpp +++ b/examples/PhasorDynamics/Medium/Hawaii/hawaiiJson.cpp @@ -70,9 +70,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - // NOTE Now we try and run the case. // Fails for now but left here for future @@ -92,13 +89,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("fault_1", Action::On); ida.initializeSimulation(1.0, true); 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); + sys.cue("fault_1", Action::Off); ida.initializeSimulation(1.1, true); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout); diff --git a/examples/PhasorDynamics/Medium/NewEngland/newengland.cpp b/examples/PhasorDynamics/Medium/NewEngland/newengland.cpp index 3414fc844..3014fa366 100644 --- a/examples/PhasorDynamics/Medium/NewEngland/newengland.cpp +++ b/examples/PhasorDynamics/Medium/NewEngland/newengland.cpp @@ -71,9 +71,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle. real_type dt = 1.0 / 4.0 / 60.0; @@ -89,13 +86,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("fault_1", Action::On); 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); + sys.cue("fault_1", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout); diff --git a/examples/PhasorDynamics/Small/TenGen/Classical/TenGenClassical.cpp b/examples/PhasorDynamics/Small/TenGen/Classical/TenGenClassical.cpp index 16e648a71..9be408b85 100644 --- a/examples/PhasorDynamics/Small/TenGen/Classical/TenGenClassical.cpp +++ b/examples/PhasorDynamics/Small/TenGen/Classical/TenGenClassical.cpp @@ -169,13 +169,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault.setStatus(1); + fault.apply(Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault.setStatus(0); + fault.apply(Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Small/TenGen/Genrou/TenGenGenrou.cpp b/examples/PhasorDynamics/Small/TenGen/Genrou/TenGenGenrou.cpp index 5f7b05e8b..7f6882f34 100644 --- a/examples/PhasorDynamics/Small/TenGen/Genrou/TenGenGenrou.cpp +++ b/examples/PhasorDynamics/Small/TenGen/Genrou/TenGenGenrou.cpp @@ -155,13 +155,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault.setStatus(1); + fault.apply(Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault.setStatus(0); + fault.apply(Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.cpp index 8f5c7f083..44179bf6d 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.cpp +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.cpp @@ -193,6 +193,7 @@ int main() data.bus_fault[0].parameters[BusFaultParameters::R] = 0.0; data.bus_fault[0].parameters[BusFaultParameters::X] = 1e-5; data.bus_fault[0].parameters[BusFaultParameters::state0] = false; + data.bus_fault[0].disambiguation_string = "fault_0"; // // Instantiate system @@ -201,9 +202,6 @@ int main() SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - real_type dt = 1.0 / 4.0 / 60.0; std::vector output; @@ -238,13 +236,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("fault_0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault->setStatus(false); + sys.cue("fault_0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.solver.json b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.solver.json index 37d0398ec..c0add2497 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.solver.json +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasic.solver.json @@ -1,12 +1,15 @@ { - "system_model_file": "ThreeBusBasic.case.json", - "dt": 0.00416666666666, - "tmax": 10, - "events": [ - {"time": 1, "type": "fault_on", "element_id": 0}, - {"time": 1.1, "type": "fault_off", "element_id": 0} + "format_version": 1, + "case_file": "ThreeBusBasic.case.json", + "dt": 0.00416666666666, + "tmax": 10, + + "schedule": [ + { "time": 1.0, "target": "bus_fault_2", "action": "on" }, + { "time": 1.1, "target": "bus_fault_2", "action": "off" } ], - "output_file": "ThreeBus_six_cycle_fault_bus1.csv", - "reference_file": "ThreeBusBasic.ref.csv", - "error_tolerance": 1e-4 + + "output_file": "ThreeBus_six_cycle_fault_bus1.csv", + "reference_file": "ThreeBusBasic.ref.csv", + "error_tolerance": 1e-4 } diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp index f31f47ec1..cf4b42767 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicJson.cpp @@ -89,9 +89,6 @@ int main(int argc, const char* argv[]) 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 @@ -107,13 +104,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("bus_fault_2", Action::On); 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); + sys.cue("bus_fault_2", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout); diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassical.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassical.cpp index 71fc02638..15d2d1322 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassical.cpp +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassical.cpp @@ -170,6 +170,7 @@ int main() data.bus_fault[0].parameters[BusFaultParameters::R] = 0.0; data.bus_fault[0].parameters[BusFaultParameters::X] = 1e-5; data.bus_fault[0].parameters[BusFaultParameters::state0] = false; + data.bus_fault[0].disambiguation_string = "fault_0"; // // Instantiate system @@ -178,9 +179,6 @@ int main() SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - real_type dt = 1.0 / 4.0 / 60.0; std::vector output; @@ -209,13 +207,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("fault_0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault->setStatus(false); + sys.cue("fault_0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassicalJson.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassicalJson.cpp index 9ddb6c15e..e8fdf4031 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassicalJson.cpp +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Classical/ThreeBusClassicalJson.cpp @@ -125,9 +125,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - real_type dt = 1.0 / 4.0 / 60.0; std::vector output; @@ -156,13 +153,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("bus_fault_2", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault->setStatus(false); + sys.cue("bus_fault_2", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/ZipLoad/ThreeBusZipLoadJson.cpp b/examples/PhasorDynamics/Tiny/ThreeBus/ZipLoad/ThreeBusZipLoadJson.cpp index 3caf9d8bf..02c651c86 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/ZipLoad/ThreeBusZipLoadJson.cpp +++ b/examples/PhasorDynamics/Tiny/ThreeBus/ZipLoad/ThreeBusZipLoadJson.cpp @@ -123,9 +123,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to fault 0 - auto* fault = sys.getBusFault(0); - real_type dt = 1.0 / 4.0 / 60.0; std::vector output; @@ -154,13 +151,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout, output_cb); // Introduce fault to ground and run for 0.1s - fault->setStatus(true); + sys.cue("bus_fault_2", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear fault and run until t = 10s. - fault->setStatus(false); + sys.cue("bus_fault_2", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.cpp index 43fe05c59..193d17317 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.cpp @@ -89,6 +89,7 @@ int main() data.bus_fault[0].parameters[BusFaultParameters::R] = 0.0; data.bus_fault[0].parameters[BusFaultParameters::X] = 1e-3; data.bus_fault[0].parameters[BusFaultParameters::state0] = false; + data.bus_fault[0].disambiguation_string = "fault_0"; // // Instantiate system model @@ -97,9 +98,6 @@ int main() SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -154,13 +152,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - fault->setStatus(true); + sys.cue("fault_0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - fault->setStatus(false); + sys.cue("fault_0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.solver.json b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.solver.json index ef55cd111..93d71a032 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.solver.json +++ b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasic.solver.json @@ -1,11 +1,14 @@ { - "system_model_file": "TwoBusBasic.case.json", - "dt": 0.00416666666666, - "tmax": 10, - "events": [ - {"time": 1, "type": "fault_on", "element_id": 0}, - {"time": 1.1, "type": "fault_off", "element_id": 0} + "format_version": 1, + "case_file": "TwoBusBasic.case.json", + "dt": 0.00416666666666, + "tmax": 10, + + "schedule": [ + { "time": 1.0, "target": "0", "action": "on" }, + { "time": 1.1, "target": "0", "action": "off" } ], - "reference_file": "TwoBusBasic.ref.csv", - "error_tolerance": 2.01e-4 + + "reference_file": "TwoBusBasic.ref.csv", + "error_tolerance": 2.01e-4 } diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasicJson.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasicJson.cpp index b1f5cbd06..37e3a7608 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasicJson.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Basic/TwoBusBasicJson.cpp @@ -77,9 +77,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -134,13 +131,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - fault->setStatus(true); + sys.cue("0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - fault->setStatus(false); + sys.cue("0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1.cpp index 392a80dc6..77718ecfa 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1.cpp @@ -78,6 +78,7 @@ int main() data.bus_fault[0].parameters[BusFaultParameters::R] = 0.0; data.bus_fault[0].parameters[BusFaultParameters::X] = 1e-3; data.bus_fault[0].parameters[BusFaultParameters::state0] = false; + data.bus_fault[0].disambiguation_string = "fault_0"; // Set generator data data.genrou.resize(1); @@ -144,9 +145,6 @@ int main() SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -210,13 +208,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - fault->setStatus(true); + sys.cue("fault_0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - fault->setStatus(false); + sys.cue("fault_0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1Json.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1Json.cpp index 623395822..e21f1a0d5 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1Json.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Ieeet1/TwoBusIeeet1Json.cpp @@ -77,9 +77,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -143,15 +140,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - // fault.setStatus(true); - fault->setStatus(true); + sys.cue("0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - // fault.setStatus(false); - fault->setStatus(false); + sys.cue("0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1.cpp index b50164656..5919155bc 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1.cpp @@ -77,6 +77,7 @@ int main() data.bus_fault[0].parameters[BusFaultParameters::R] = 0.0; data.bus_fault[0].parameters[BusFaultParameters::X] = 1e-3; data.bus_fault[0].parameters[BusFaultParameters::state0] = false; + data.bus_fault[0].disambiguation_string = "fault_0"; // Set generator data data.genrou.resize(1); @@ -118,9 +119,6 @@ int main() SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -175,13 +173,13 @@ int main() ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - fault->setStatus(true); + sys.cue("fault_0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - fault->setStatus(false); + sys.cue("fault_0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1Json.cpp b/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1Json.cpp index 27aefa1fd..c02787462 100644 --- a/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1Json.cpp +++ b/examples/PhasorDynamics/Tiny/TwoBus/Tgov1/TwoBusTgov1Json.cpp @@ -81,9 +81,6 @@ int main(int argc, const char* argv[]) SystemModel sys(data); sys.allocate(); - // Get access to the fault - auto* fault = sys.getBusFault(0); - // Set time step to 1/4 of a 60Hz cycle real_type dt = 1.0 / 4.0 / 60.0; @@ -138,13 +135,13 @@ int main(int argc, const char* argv[]) ida.runSimulation(1.0, nout, output_cb); // Introduce fault and run for the next 0.1s - fault->setStatus(true); + sys.cue("0", Action::On); ida.initializeSimulation(1.0, false); nout = static_cast(std::round((1.1 - 1.0) / dt)); ida.runSimulation(1.1, nout, output_cb); // Clear the fault and run until t = 10s. - fault->setStatus(false); + sys.cue("0", Action::Off); ida.initializeSimulation(1.1, false); nout = static_cast(std::round((10.0 - 1.1) / dt)); ida.runSimulation(10.0, nout, output_cb); diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 62a9b3069..6dc0b7fd3 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -78,8 +79,10 @@ namespace GridKit PhasorDynamics::BusInfinite bus; system->addBus(&bus); - PhasorDynamics::BusFault fault(&bus); - system->addFault(&fault); + PhasorDynamics::BusFaultData fault_data; + fault_data.disambiguation_string = "fault_0"; + PhasorDynamics::BusFault fault(&bus, fault_data); + system->addComponent(&fault, fault_data.disambiguation_string); success *= system->allocate() == 0; success *= system->initialize() == 0; @@ -88,6 +91,28 @@ namespace GridKit success *= system->size() == 0; success *= system->size() == fault.size(); + // Cue routes to the matching child via SystemModel::cue. + try + { + system->cue("fault_0", PhasorDynamics::Action::On); + } + catch (...) + { + success *= false; + } + + // Unknown target throws. + bool threw = false; + try + { + system->cue("no_such", PhasorDynamics::Action::On); + } + catch (const std::exception&) + { + threw = true; + } + success *= threw; + delete system; system = nullptr; From 37fe394d9781131bf680ed64096cb96c8f7d1ca9 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sat, 25 Apr 2026 09:52:00 -0500 Subject: [PATCH 2/4] BranchTrip Cue Implementation --- .../Model/PhasorDynamics/Branch/Branch.hpp | 11 ++++ .../PhasorDynamics/Branch/BranchData.hpp | 9 +-- .../PhasorDynamics/Branch/BranchImpl.hpp | 37 ++++++++--- GridKit/Model/PhasorDynamics/Component.hpp | 7 +-- GridKit/Model/PhasorDynamics/INPUT_FORMAT.md | 2 +- GridKit/Model/PhasorDynamics/SystemModel.hpp | 2 +- application/PhasorDynamics/README.md | 3 +- .../Tiny/ThreeBus/Basic/CMakeLists.txt | 5 ++ .../Basic/ThreeBusBasicBranchTrip.solver.json | 12 ++++ .../SystemSingleComponentTests.hpp | 63 +++++++++++++++++++ .../runSystemSingleComponentTests.cpp | 1 + 11 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json diff --git a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp index 5355b0b8a..fcd41f754 100644 --- a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp @@ -106,6 +106,16 @@ namespace GridKit setDerivedParams(); } + /// True when the branch is in service (closed). The underlying + /// `status_` is a smooth multiplier (1.0 closed / 0.0 open) so that + /// `evaluateBusResidualNN` kernels see a constant expression structure + bool status() const + { + return status_ > RealT{0.5}; + } + + void apply(Action action) override final; + const Model::VariableMonitorBase* getMonitor() const override; private: @@ -166,6 +176,7 @@ namespace GridKit RealT X_{0.0}; RealT G_{0.0}; RealT B_{0.0}; + RealT status_{1.0}; ///< Multiplier on residual contributions: 1.0 = closed, 0.0 = open IdxT bus1_id_{0}; IdxT bus2_id_{0}; diff --git a/GridKit/Model/PhasorDynamics/Branch/BranchData.hpp b/GridKit/Model/PhasorDynamics/Branch/BranchData.hpp index 98528c3d1..01f5d9ac0 100644 --- a/GridKit/Model/PhasorDynamics/Branch/BranchData.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/BranchData.hpp @@ -15,10 +15,11 @@ namespace GridKit /// Initial parameters for a branch enum class BranchParameters { - R, ///< Line series resistance - X, ///< Line series reactance - G, ///< Line shunt conductance - B, ///< Line shunt charging + R, ///< Line series resistance + X, ///< Line series reactance + G, ///< Line shunt conductance + B, ///< Line shunt charging + state0, ///< Initial branch status (true = closed/in-service, default true) }; /// Ports for a branch diff --git a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp index bcb0c268d..43a1d6045 100644 --- a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp @@ -97,6 +97,20 @@ namespace GridKit // std::cout << "Destroy Branch..." << std::endl; } + template + void Branch::apply(Action action) + { + switch (action) + { + case Action::On: + status_ = RealT{1.0}; + break; + case Action::Off: + status_ = RealT{0.0}; + break; + } + } + /** * @brief Set the component ID */ @@ -155,8 +169,8 @@ namespace GridKit ScalarT Vi1 = wb[1]; ScalarT Ir1 = -(g_ + 0.5 * G_) * Vr1 + (b_ + 0.5 * B_) * Vi1; ScalarT Ii1 = -(b_ + 0.5 * B_) * Vr1 - (g_ + 0.5 * G_) * Vi1; - h[0] = Ir1; - h[1] = Ii1; + h[0] = status_ * Ir1; + h[1] = status_ * Ii1; return 0; } @@ -176,8 +190,8 @@ namespace GridKit ScalarT Vi2 = wb[1]; ScalarT Ir1 = g_ * Vr2 - b_ * Vi2; ScalarT Ii1 = b_ * Vr2 + g_ * Vi2; - h[0] = Ir1; - h[1] = Ii1; + h[0] = status_ * Ir1; + h[1] = status_ * Ii1; return 0; } @@ -197,8 +211,8 @@ namespace GridKit ScalarT Vi1 = wb[1]; ScalarT Ir2 = g_ * Vr1 - b_ * Vi1; ScalarT Ii2 = b_ * Vr1 + g_ * Vi1; - h[0] = Ir2; - h[1] = Ii2; + h[0] = status_ * Ir2; + h[1] = status_ * Ii2; return 0; } @@ -218,8 +232,8 @@ namespace GridKit ScalarT Vi2 = wb[1]; ScalarT Ir2 = -(g_ + 0.5 * G_) * Vr2 + (b_ + 0.5 * B_) * Vi2; ScalarT Ii2 = -(b_ + 0.5 * B_) * Vr2 - (g_ + 0.5 * G_) * Vi2; - h[0] = Ir2; - h[1] = Ii2; + h[0] = status_ * Ir2; + h[1] = status_ * Ii2; return 0; } @@ -275,6 +289,13 @@ namespace GridKit B_ = std::get(data.parameters.at(model_data_type::Parameters::B)); } + if (data.parameters.contains(model_data_type::Parameters::state0)) + { + status_ = std::get(data.parameters.at(model_data_type::Parameters::state0)) + ? RealT{1.0} + : RealT{0.0}; + } + if (data.ports.contains(model_data_type::Ports::bus1)) { bus1_id_ = data.ports.at(model_data_type::Ports::bus1); diff --git a/GridKit/Model/PhasorDynamics/Component.hpp b/GridKit/Model/PhasorDynamics/Component.hpp index 95cdc585e..1d9079e79 100644 --- a/GridKit/Model/PhasorDynamics/Component.hpp +++ b/GridKit/Model/PhasorDynamics/Component.hpp @@ -15,10 +15,9 @@ namespace GridKit { using Log = ::GridKit::Utilities::Logger; - /// Project-wide control actions. Today only BusFault uses cues, so this - /// vocabulary is small. When a second action-bearing component arrives, - /// either add its actions here (if they read naturally as shared verbs) - /// or switch to per-component action enums + typed dispatch. + /// Project-wide control actions shared by cue-targetable components such + /// as BusFault and Branch. If future runtime controls stop fitting these + /// verbs naturally, switch to per-component action enums + typed dispatch. enum class Action { On, diff --git a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index ec01498f2..560e638b5 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -141,7 +141,7 @@ are specified: Device class | Description | Ports | Initialization parameters | Variables available to monitor --------------|------------------------------------------------------|----------------------------------|---------------------------- | ------------------------- - `Branch` | a basic algebraic pi model for a line or transformer | `bus1`, `bus2` | `R`, `X`, `G`, `B` | `ir1`, `ii1`, `im1`, `p1`, `q1`, `ir2`, `ii2`, `im2`, `p2`, `q2` + `Branch` | a basic algebraic pi model for a line or transformer | `bus1`, `bus2` | `R`, `X`, `G`, `B`, `state0` | `ir1`, `ii1`, `im1`, `p1`, `q1`, `ir2`, `ii2`, `im2`, `p2`, `q2` `Load` | a basic static impedence load model | `bus` | `R`, `X` | `p`, `q` `Genrou` | 6th order machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqopp`, `Tqop`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xqp`, `Xqpp`, `Xl`, `S10`, `S12`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed` `GenClassical`| the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega` diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index f50ce66a4..9515c796f 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -125,7 +125,7 @@ namespace GridKit } auto* branch = new Branch(getBus(bus1_index), getBus(bus2_index), branchdata); - addComponent(branch); + addComponent(branch, branchdata.disambiguation_string); } // Add loads diff --git a/application/PhasorDynamics/README.md b/application/PhasorDynamics/README.md index 478f1a811..8d4267143 100644 --- a/application/PhasorDynamics/README.md +++ b/application/PhasorDynamics/README.md @@ -7,7 +7,7 @@ simulations. It reads a JSON solver file that specifies the case (system model) to load, the simulation parameters, an optional schedule of runtime cues, and output configuration. -Components that respond to runtime control (e.g. `BusFault`) are declared in +Components that respond to runtime control (e.g. `BusFault`, `Branch`) are declared in the case file as ordinary devices. The solver file's `schedule` then drives their state changes by referencing each component's `id` from the case file. @@ -66,6 +66,7 @@ last cue, it runs to `tmax`. Class | Action vocabulary | Effect ------------|-------------------|---------------------------------------------- `BusFault` | `on`, `off` | Engage or clear the fault impedance at the bus + `Branch` | `on`, `off` | Close or open the branch (in service / tripped) A device is cue-targetable only if its `id` is referenced by the schedule. Devices whose class does not accept cues (or unknown actions) cause diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt index 81266df9d..1cf3dbd95 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt @@ -14,6 +14,7 @@ target_link_libraries(ThreeBusBasicJson install(TARGETS ThreeBusBasicJson RUNTIME DESTINATION ${_install_path}) gridkit_example_add_file(ThreeBusBasic.case.json) gridkit_example_add_file(ThreeBusBasic.ref.csv) +gridkit_example_add_file(ThreeBusBasicBranchTrip.ref.csv) add_test(NAME ThreeBusBasic COMMAND ThreeBusBasic) add_test(NAME ThreeBusBasicJson @@ -23,6 +24,10 @@ add_test(NAME ThreeBusBasicJson_no_arg WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) gridkit_example_add_file(ThreeBusBasic.solver.json) +gridkit_example_add_file(ThreeBusBasicBranchTrip.solver.json) add_test(NAME ThreeBusBasic_using_app COMMAND PDSim ThreeBusBasic.solver.json WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +add_test(NAME ThreeBusBasicBranchTrip_using_app + COMMAND PDSim ThreeBusBasicBranchTrip.solver.json + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json new file mode 100644 index 000000000..457cb34dc --- /dev/null +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json @@ -0,0 +1,12 @@ +{ + "format_version": 1, + "case_file": "ThreeBusBasic.case.json", + "dt": 0.00416666666666, + "tmax": 10, + "schedule": [ + { "time": 1.0, "target": "BR_1_2", "action": "off" } + ], + "output_file": "ThreeBusBasicBranchTrip.csv", + "reference_file": "ThreeBusBasicBranchTrip.ref.csv", + "error_tolerance": 1e-4 +} diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 6dc0b7fd3..c5ae7dea9 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -49,6 +49,69 @@ namespace GridKit return success.report(__func__); } + TestOutcome branchTrip() + { + TestStatus success = true; + + PhasorDynamics::SystemModel* system = new PhasorDynamics::SystemModel(); + + PhasorDynamics::BusInfinite bus1, bus2; + system->addBus(&bus1); + system->addBus(&bus2); + + // Branch data with realistic R/X/G/B so setDerivedParams stays finite. + PhasorDynamics::BranchData branch_data; + branch_data.disambiguation_string = "br_0"; + branch_data.parameters[PhasorDynamics::BranchParameters::R] = 0.05; + branch_data.parameters[PhasorDynamics::BranchParameters::X] = 0.21; + branch_data.parameters[PhasorDynamics::BranchParameters::G] = 0.0; + branch_data.parameters[PhasorDynamics::BranchParameters::B] = 0.1; + + PhasorDynamics::Branch branch(&bus1, &bus2, branch_data); + system->addComponent(&branch, branch_data.disambiguation_string); + + success *= system->allocate() == 0; + success *= system->initialize() == 0; + + // Default: branch is closed (in service) + success *= branch.status() == true; + + // Open via cue + try + { + system->cue("br_0", PhasorDynamics::Action::Off); + } + catch (...) + { + success *= false; + } + success *= branch.status() == false; + + // Close via cue + try + { + system->cue("br_0", PhasorDynamics::Action::On); + } + catch (...) + { + success *= false; + } + success *= branch.status() == true; + + // state0 = false -> branch starts open without a cue. + PhasorDynamics::BranchData open_data; + open_data.parameters[PhasorDynamics::BranchParameters::R] = 0.05; + open_data.parameters[PhasorDynamics::BranchParameters::X] = 0.21; + open_data.parameters[PhasorDynamics::BranchParameters::state0] = false; + PhasorDynamics::Branch open_branch(&bus1, &bus2, open_data); + success *= open_branch.status() == false; + + delete system; + system = nullptr; + + return success.report(__func__); + } + TestOutcome bus() { TestStatus success = true; diff --git a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp index 4f4a3c093..287918203 100644 --- a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp @@ -9,6 +9,7 @@ int main() GridKit::Testing::SystemSingleComponentTests test; result += test.branch(); + result += test.branchTrip(); result += test.bus(); result += test.busFault(); result += test.ieeet1(); From 3f29c342288849be5bf1be350fe58a6c58215a02 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sat, 25 Apr 2026 11:38:41 -0500 Subject: [PATCH 3/4] csv for validation not git tracked --- examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt index 1cf3dbd95..1f74e6da4 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/CMakeLists.txt @@ -14,7 +14,6 @@ target_link_libraries(ThreeBusBasicJson install(TARGETS ThreeBusBasicJson RUNTIME DESTINATION ${_install_path}) gridkit_example_add_file(ThreeBusBasic.case.json) gridkit_example_add_file(ThreeBusBasic.ref.csv) -gridkit_example_add_file(ThreeBusBasicBranchTrip.ref.csv) add_test(NAME ThreeBusBasic COMMAND ThreeBusBasic) add_test(NAME ThreeBusBasicJson From f0efd5ee274192765d9613b2f167a5e94102fe8a Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sat, 25 Apr 2026 11:49:05 -0500 Subject: [PATCH 4/4] removing ref file from tracked solver --- .../Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json index 457cb34dc..145a8be0b 100644 --- a/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json +++ b/examples/PhasorDynamics/Tiny/ThreeBus/Basic/ThreeBusBasicBranchTrip.solver.json @@ -6,7 +6,5 @@ "schedule": [ { "time": 1.0, "target": "BR_1_2", "action": "off" } ], - "output_file": "ThreeBusBasicBranchTrip.csv", - "reference_file": "ThreeBusBasicBranchTrip.ref.csv", - "error_tolerance": 1e-4 + "output_file": "ThreeBusBasicBranchTrip.csv" }