From 98face3bc3bd8f187b09a6c29144d680234d01db Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 20 Aug 2026 08:20:03 -0700 Subject: [PATCH 01/12] add dynamic genrou model --- docs/developer-guide/andes-compatibility.md | 530 +++++++++++++++++- src/fileInput/gridDynReadDYR.cpp | 7 +- src/griddyn/genmodels/GenModel.cpp | 2 + src/griddyn/genmodels/GenModel4.cpp | 1 + src/griddyn/genmodels/GenModelGENROU.cpp | 457 ++++++++------- src/griddyn/genmodels/GenModelGENROU.h | 151 +++++ src/utilities/Saturation.cpp | 143 ++++- src/utilities/Saturation.h | 43 +- test/CMakeLists.txt | 12 + test/andesTests/testAndesDyrReader.cpp | 81 +++ test/componentTests/testGenModels.cpp | 171 ++++++ test/libraryTests/testSaturation.cpp | 72 +++ .../andes_ieee14_genrou_reference.json | 28 + test/test_files/andes_tests/ieee14.raw | 93 +++ test/test_files/andes_tests/ieee14_genrou.dyr | 15 + 15 files changed, 1563 insertions(+), 243 deletions(-) create mode 100644 test/andesTests/testAndesDyrReader.cpp create mode 100644 test/libraryTests/testSaturation.cpp create mode 100644 test/test_files/andes_tests/andes_ieee14_genrou_reference.json create mode 100644 test/test_files/andes_tests/ieee14.raw create mode 100644 test/test_files/andes_tests/ieee14_genrou.dyr diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index dca8798cc..b72a80277 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -39,6 +39,15 @@ The present numerical suite covers power flow. Dynamic compatibility requires matching model states, initialization, events, limits, and trajectories; not only the final power-flow point. +There are two separate compatibility axes: + +1. **PSS/e RAW plus DYR input:** reproduce the model conversion performed by + ANDES's `psse-dyr.yaml`, including the few conversions that ANDES currently + labels as approximations. +2. **Native ANDES input:** construct the same GridDyn dynamic submodels and + control connections when the source is ANDES JSON/XLSX. Native support is + not implied by a working DYR adapter. + | Work item | Status | Completion evidence | | ---------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | | DC `L`/`C`/combined branch dynamics | Planned | ANDES and GridDyn trajectories from the same initialized DC case. | @@ -48,6 +57,490 @@ only the final power-flow point. | Fortescue interface | Planned | New GridDyn component and unbalanced/interface regression cases. | | Remaining ANDES dynamic families | Planned | Maintain a model-by-model inventory before adding importer mappings. | +## PSS/e DYR compatibility baseline + +The local ANDES DYR conversion table recognizes the 41 record names below. +GridDyn's current DYR reader recognizes only `GENROU`, `ESDC1A`, `EXDC2`, +`TGOV1`, and `SEXS`. Recognition alone is not compatibility: the existing +adapters still have attachment, parameter, equation, and validation gaps. + +| PSS/e DYR record(s) | ANDES destination | Existing GridDyn candidate | Current compatibility and required action | +| ------------------- | ----------------- | -------------------------- | ----------------------------------------- | +| `GENCLS` | `GENCLS` | `GenModelClassical` | **Partial.** Add a DYR adapter, verify machine/system-base conversion, initialization, and rotor trajectory. | +| `GENSAL` | `GENROU` | No verified exact equivalent | **Planned.** ANDES explicitly marks this as an approximation and fills GENROU q-axis values from d-axis data. Decide whether compatibility mode should reproduce that conversion before considering a distinct salient-pole model. | +| `GENROU` | `GENROU` | Registered `GenModelGENROU`; DYR uses the dedicated model | **Partial.** The adapter and dedicated model match all eight initialized machine states, field voltage, and mechanical power for five IEEE 14-bus machines, including quadratic saturation. Exact alphanumeric machine-ID lookup, arbitrary machine-base cases, native ANDES import, and disturbed-trajectory validation remain. | +| `IEEEVC` | `IEEEVC` | None | **No direct analogue.** Add a voltage-compensator input to the exciter path, including `rc`/`xc` terminal-current compensation. | +| `SEXS` | `SEXS` | `ExciterSEXS` | **Partial.** The adapter exists; verify lead-lag convention, limits, initialization, and trajectories. | +| `ESDC1A` | `ESDC1A` | `ExciterDC1A` / `ExciterIEEEtype1` | **Partial.** The current adapter ignores `TR`, switch behavior, and `E1`/`SE1`/`E2`/`SE2`; its fallback to type 1 when `TB` is zero must be compared with ANDES. | +| `EXDC2` | `EXDC2` | `ExciterDC2A` | **Partial.** The adapter exists but ignores `TR`, switch behavior, and saturation points. Confirm whether `DC2A` equations exactly represent ANDES `EXDC2`. | +| `IEEET1`, `IEEET3`, `ESDC2A` | Same named ANDES models | `ExciterIEEEtype1`, `ExciterIEEEtype2`, and `ExciterDC2A` are only candidates | **Planned.** Perform equation and limiter audits before selecting an analogue; similar names are insufficient. | +| `ESST1A`, `ESAC1A`, `AC8B`, `EXAC1`, `EXAC2`, `EXAC4`, `IEEEX1`, `EXST1`, `ESST3A`, `ESST4B` | Same named ANDES models | None exact | **No direct analogue.** Implement model-specific exciter blocks and DYR schemas, then add initialization and trajectory tests. | +| `ESAC6A`, `SCRX`, `EXPIC1` | `SEXS` | `ExciterSEXS` | **Planned compatibility approximations.** ANDES marks these conversions as TODO/approximate and currently discards most source parameters. Reproduce this only as an explicit compatibility mode and emit a diagnostic; do not describe it as exact model support. | +| `TGOV1` | `TGOV1` | `GovernorTgov1` | **Partial.** The adapter exists, but currently reads `T2`, `T3`, and `Dt` from the wrong token positions. Correct the schema and then audit limits, initialization, and trajectories. | +| `HYGOV` | `HYGOV` | `GovernorHydro` is a candidate | **Planned.** Compare equations, water-column dynamics, gate/rate limits, and parameter units before mapping it. | +| `IEESGO`, `IEEEG1` | Same named ANDES models | `GovernorReheat` and steam-governor classes are only candidates | **Planned.** No exact equivalence has been established; audit block diagrams before choosing reuse versus new models. | +| `GAST` | `GAST` | None exact | **No direct analogue.** Add a gas-turbine governor implementation and DYR adapter. | +| `GGOV1` | `TGOV1` | `GovernorTgov1` | **Planned compatibility approximation.** ANDES currently retains only `R` when converting this record. If reproduced, emit a diagnostic and keep exact GGOV1 support as a separate task. | +| `IEEEST`, `ST2CUT` | Same named ANDES models | Base `Stabilizer` only | **No direct analogue.** The base class is not a usable PSS implementation; add the two models and their generator/exciter signal connections. | +| `REGCA1`, `REECA1`, `REECB1`, `REPCA1` | Same named ANDES models | `GenModelInverter` is not equivalent | **No direct analogue.** Add the coordinated renewable generator, electrical-control, and plant-control chain rather than flattening these records into the generic inverter. | +| `WTDTA1`, `WTARA1`, `WTPTA1`, `WTTQA1` | Same named ANDES models | None | **No direct analogue.** Add drive-train, aerodynamic, pitch, and torque-control submodels with their shared interfaces. | +| `Toggle`, `Fault` | ANDES event models | GridDyn event/action and fault mechanisms | **Partial conceptually.** Define DYR record schemas and translate target resolution, timing, status changes, and fault clearing semantics; no adapter exists. | + +### DYR reader infrastructure plan + +- Replace the model-name `if` chain and unchecked numeric vector with a + table-driven record registry. Each adapter should declare token count, + parameter names, supported variants, and its exact/approximate status. +- Parse quoted machine identifiers, continuation records, comments, commas, + and the terminating slash without assuming that every field is numeric. +- Resolve a machine by RAW bus number plus exact PSS/e machine ID. The current + `stoi(ID)` followed by `getGen(ID - 1)` is a positional lookup and cannot + safely handle quoted or alphanumeric IDs. +- Validate bus, generator, factory result, token count, and numeric conversion + before attaching a model. Unsupported, approximate, malformed, duplicate, + and conflicting controller records need structured diagnostics rather than + an `stdout` message followed by continued loading. +- Ensure the target is a `DynamicGenerator`, preserve the RAW machine base and + stator data needed by the DYR model, and define how a second machine, + exciter, governor, or stabilizer record on the same generator is handled. +- Use the same model-construction functions from the DYR reader and native + ANDES reader so the two input paths cannot drift to different equations. +- Add parser/attachment tests for quoted and alphanumeric IDs, multiple + machines at one bus, continuation lines, comments, malformed records, + unsupported models, and model conflicts. + +## Six-PR IEEE 14 dynamic compatibility execution plan + +This is the authoritative implementation order for fully exercising +`GenModelGENROU` with the complete ANDES IEEE 14-bus RAW/DYR case. It is +written as a handoff contract: after any PR is merged, a new developer or LLM +should be able to start the next PR using only this document, the GridDyn +repository, and the frozen ANDES source identified below. Do not silently move +work between PRs. If a prerequisite defect is found, fix it in the PR that owns +that subsystem and record the change here. + +This six-PR sequence covers the PSS/e RAW/DYR path and the models required by +this case. Native ANDES JSON/XLSX dynamic-model import and unrelated ANDES +models remain separate roadmap work; they are not hidden requirements for +these merge gates. + +### Frozen comparison source and target behavior + +- ANDES source repository: `C:\Users\phlpt\Documents\andes` in the original + development environment. +- ANDES version and commit: `v2.0.0`, + `eda5163c9ee8d19945a1dd5d1771fec5da608c27`. +- Full inputs: `andes/cases/ieee14/ieee14.raw` and + `andes/cases/ieee14/ieee14.dyr`. +- Upstream input SHA-256 values: + - RAW: `539c35fd57f72079206d5b92b3eb5be2e0fbdbd6dc9037a2e0b99f9c6a774eb3` + - DYR: `1f97507a01df1c73c897092fb928a460d4dc5e5ba2263c2bac2cd63b8e97a888` +- DYR conversion schema: `andes/io/psse-dyr.yaml`. +- Existing ANDES regression: `tests/integration/test_known_good.py`, with its + two-second reference in `tests/pkl/ieee14_2s.pkl`. The upstream + test checks final GENROU `omega`, `tm`, and `vf` at two decimal places. The + GridDyn regression must retain those checks but capture more signals and use + explicit per-signal absolute and relative tolerances. Its ANDES TDS settings + are `tf=2`, fixed `tstep=1/30`, nonlinear tolerance `1e-4`, `fixt=1`, and + `shrinkt=0`; use the same settings when deliberately regenerating the + comparison reference. +- Relevant ANDES equations: + - `andes/models/synchronous/genrou.py` + - `andes/models/governor/tgov1.py` + - `andes/models/governor/ieeeg1.py` + - `andes/models/exciter/esst3a.py` + - `andes/models/exciter/exst1.py` + - `andes/models/pss/st2cut.py` + - `andes/models/pss/ieeest.py` + +The target run is a converged power flow followed by dynamic initialization, +an equilibrium interval, opening `Line_1` at 1.0 seconds, reclosing it at 1.1 +seconds, and continuing through 2.0 seconds. The five controller chains are: + +| Generator bus | Machine | Exciter | Governor | Stabilizer | +| ------------- | ------- | ------- | -------- | ---------- | +| 1 | `GENROU` | `ESST3A` | `TGOV1` | `ST2CUT` | +| 2 | `GENROU` | `EXST1` | `IEEEG1` | `ST2CUT` | +| 3 | `GENROU` | `ESST3A` | `IEEEG1` | `IEEEST` | +| 6 | `GENROU` | `ESST3A` | `TGOV1` | none | +| 8 | `GENROU` | `ESST3A` | `TGOV1` | none | + +### PR status and dependency order + +| PR | Deliverable | Status | Depends on | +| -- | ----------- | ------ | ---------- | +| 1 | Complete GENROU equations, saturation utility, DYR mapping, and five-machine initialization reference | Current changes; treat as complete after merge | none | +| 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | +| 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Planned | PR 2 | +| 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Planned | PR 3 | +| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | Planned | PR 4 | +| 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | + +PRs must merge in this order. Each PR must pass without relying on production +code from a later PR. Minimized fixtures and test-only probe models are +preferred when a downstream controller is not yet available. + +### PR 1: GENROU model and DYR initialization baseline + +This is the current change set. Once merged, do not reopen the generic +`GenModel6` implementation to add GENROU behavior; `GenModelGENROU` is the +canonical PSS/e/ANDES-compatible class. + +Required contents: + +- Complete sixth-order GENROU algebraic, differential, initialization, output, + and analytic Jacobian equations with GridDyn's documented dq sign mapping. +- Reusable cutoff-scaled quadratic saturation in `utilities::Saturation`. +- Factory registration under `genrou`; generic model `6` remains unchanged. +- `GENROU` DYR records construct `GenModelGENROU`. Attach the model before + applying DYR reactances so RAW stator resistance is retained without the RAW + source reactance overwriting DYR `Xd`. +- Focused unsaturated/saturated initialization, derivative, residual, + algebraic, Jacobian, invalid-parameter, factory, and saturation tests. +- A dedicated `AndesCompatibilityTests` target and an IEEE 14 test that loads + the RAW plus the five extracted GENROU DYR records, asserts the model type, + and compares all eight initialized states, field voltage, and mechanical + power with ANDES at `1e-6`. + +Primary GridDyn artifacts after merge: + +- `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` +- `src/utilities/Saturation.{h,cpp}` +- `src/fileInput/gridDynReadDYR.cpp` +- `test/componentTests/testGenModels.cpp` +- `test/libraryTests/testSaturation.cpp` +- `test/andesTests/testAndesDyrReader.cpp` +- `test/test_files/andes_tests/ieee14.raw` +- `test/test_files/andes_tests/ieee14_genrou.dyr` +- `test/test_files/andes_tests/andes_ieee14_genrou_reference.json` + +Merge gate: `LibraryTests`, `GeneratorComponentTests`, and +`AndesCompatibilityTests` pass. The current implementation has passed all +three targets. Full network/controller trajectory parity is intentionally not +part of PR 1. + +### PR 2: Reader foundation, power-flow parity, and GENROU-only trajectory + +Fresh-context task statement: make the real IEEE 14 RAW/DYR generator identity +and bases reliable, make GridDyn's RAW-only operating point agree with ANDES, +and then prove that the five GENROU models remain in equilibrium and respond +correctly without any controller models. + +Required production work: + +- Refactor `src/fileInput/gridDynReadDYR.cpp` into validated record adapters or + a small schema-backed registry. Preserve continuation-line handling while + validating token count and numeric fields. +- Resolve generators by RAW bus number plus the exact quoted PSS/e machine ID; + remove the current `stoi(ID)` plus `getGen(ID - 1)` positional assumption. +- Diagnose missing buses/generators, duplicate machine models, malformed + records, unsupported records, and factory failures without null dereferences. +- Verify transfer of RAW `MBASE`, stator resistance, rated voltage, and status + into the dynamic generator/model. Add a non-100-MVA machine-base regression. +- Correct the IEEE 14 RAW power-flow mismatch. Audit PV voltage targets, + reactive-limit behavior, transformer taps/shunts, and generator setpoints; + do not hide a modeling mismatch by loosening tolerances. + +Independent tests: + +- DYR parser/attachment cases for quoted and alphanumeric IDs, two generators + on one bus, continuation lines, malformed records, and duplicate GENROU. +- RAW-only IEEE 14 comparison of every bus voltage/angle and generator P/Q + against captured ANDES values with reviewed per-signal tolerances. +- Load the RAW plus `ieee14_genrou.dyr`, perform GridDyn power flow and dynamic + initialization without supplying captured terminal inputs, and compare the + initialized GENROU states to ANDES. +- A no-disturbance run with bounded drift and a separate small field-voltage or + mechanical-power perturbation compared to a captured ANDES time series. + +Likely touchpoints include `src/fileInput/gridDynReadRAW.cpp`, +`src/fileInput/gridDynReadDYR.cpp`, generator identity/storage classes, and +`test/andesTests/`. Do not implement governors, exciters, stabilizers, or +`Toggle` in this PR. + +Merge gate: the controller-free case reaches dynamic completion, initialization +matches ANDES, the equilibrium case does not drift beyond its declared +tolerance, and the perturbed GENROU trajectory agrees at common sample times. + +### PR 3: Controller signal contract, PSS routing, and TGOV1 + +Fresh-context task statement: establish the reusable signal connections needed +by the missing IEEE 14 controllers, then make the three existing TGOV1 records +numerically compatible with ANDES. + +Required production work: + +- Define named, documented generator/controller signals for terminal voltage, + rotor speed, `Id`, `Iq`, `Vd`, `Vq`, electrical power/torque, mechanical + power, and `XadIfd`. Specify dq signs and machine/system-base scaling. +- Expose signal values and state/Jacobian locations from GENROU through + `DynamicGenerator`; avoid model-specific downcasts in controller classes. +- Extend the exciter input contract to include field-current/machine feedback + needed by `ESST3A` and `EXST1`. +- Define selectable stabilizer inputs and route the stabilizer output `Vss` + into the exciter reference summing junction. The current code initializes a + PSS but does not use its output in `generateSubModelInputs`. +- Correct `TGOV1` DYR mapping. The schema is `R, T1, VMAX, VMIN, T2, T3, Dt`; + the existing adapter incorrectly takes `T2` from the `VMIN` position. +- Audit `GovernorTgov1` droop, damping, limit/root behavior, initialization, + units, and machine-base conversion against ANDES rather than assuming the + class name establishes equivalence. + +Independent tests: + +- A probe submodel verifies every new signal's value, sign, base, and Jacobian + location at initialization and a perturbed GENROU state. +- A test stabilizer produces a known `Vss`; verify it changes the exciter + summing input and ultimately GENROU field voltage with the correct Jacobian. +- DYR parameter/attachment tests for all three IEEE 14 `TGOV1` records. +- Isolated TGOV1 initialization, limit transitions, and speed/load-step + trajectory against ANDES, followed by a GENROU+TGOV1 equilibrium test. + +Likely touchpoints include `GenModel`, `GenModelGENROU`, `DynamicGenerator`, +`Exciter`, `Stabilizer`, `GovernorTgov1`, the DYR adapters, and component plus +ANDES compatibility tests. Do not implement the two exciters or three remaining +governor/PSS classes in this PR. + +### PR 4: ESST3A and EXST1 excitation systems + +Fresh-context task statement: implement the two excitation-system models used +by all five IEEE 14 generators, using the PR 3 signal contract and exact ANDES +equations. + +`ESST3A` must include its voltage transducer, terminal-current compensation, +`VE`, piecewise `FEX`, `XadIfd` feedback, `VB` calculation, regulator and +feedback lags, lead-lag compensation, anti-windup, and input/output limits. +Map all DYR fields in the order declared by `psse-dyr.yaml`. It is used at +buses 1, 3, 6, and 8. + +`EXST1` must include its voltage transducer, input limiter, lead-lag, +regulator, washout feedback, `XadIfd`-dependent output bounds, and output +limiter. It is used at bus 2. Do not approximate either model with an existing +generic exciter merely because some blocks are similar; reuse GridDyn block or +saturation utilities only when their equations and limiter semantics match. + +Each model must provide: + +- A normal GridDyn class, factory registration, clone support, parameter + validation, state names, Doxygen equations, and citations to the frozen + ANDES source and an applicable standard/manual. +- Complete DYR mapping with an attachment/parameter regression. +- Captured ANDES initialization values, a perturbed equation/derivative test, + analytic-versus-finite-difference Jacobian checks, and explicit limiter/root + transition tests. +- A minimized GENROU+exciter voltage-reference-step trajectory. ESST3A and + EXST1 tests must pass independently of each other. + +Cross-simulator fixtures belong in `test/andesTests/` and +`test/test_files/andes_tests/`; generic equation tests may live with exciter +component tests. Do not add IEEEG1 or stabilizer implementations in this PR. + +Merge gate: all five IEEE 14 generators can attach and initialize the correct +exciter type in a controller subset case, and both minimized trajectories +match ANDES within declared tolerances. + +### PR 5: IEEEG1, ST2CUT, and IEEEST + +Fresh-context task statement: complete the remaining governor and stabilizer +models so every controller record in the IEEE 14 DYR has an exact GridDyn +destination. + +Required models: + +- `IEEEG1`, used at buses 2 and 3: implement speed droop, valve-position and + rate limits, turbine-stage time constants, eight power fractions, and the + optional second-machine input defined by the DYR schema. +- `ST2CUT`, used at buses 1 and 2: implement both selectable inputs, local or + remote buses, transducers, washout/lag selection, three lead-lag stages, + output limits, and voltage gating. +- `IEEEST`, used at bus 3: implement selectable input, second-order filters, + two lead-lag stages, gain, washout/lag, output limiting, and voltage gating. + +Do not map `IEEEG1` to `GovernorReheat` or either stabilizer to the base +`Stabilizer` unless an equation-by-equation audit proves exact equivalence. +The actual IEEE 14 `ST2CUT` records use rotor-speed input, while its `IEEEST` +record uses generator electrical-power input; complete model support should +also test the other documented modes so the implementation is not +case-specific. + +Each model has the same completion requirements as PR 4: class/factory/clone, +full DYR schema, Doxygen equations and sources, initialization and perturbed +equations, Jacobian, limit/root transitions, and a minimized coupled ANDES +trajectory. Add explicit remote-bus and machine/system-base tests where the +schema permits them. + +Merge gate: a no-event IEEE 14 controller subset loads all five governors and +all three stabilizers with no unsupported-record diagnostics; each controller +also passes its independent minimized numerical comparison. Do not add the +line `Toggle` or final full-case regression in this PR. + +### PR 6: Toggle events and complete IEEE 14 regression + +Fresh-context task statement: translate the two ANDES DYR `Toggle` records and +add the permanent end-to-end regression proving that the complete unmodified +IEEE 14 RAW/DYR case behaves like ANDES. + +Required production work: + +- Add a validated `Toggle` DYR adapter for the schema `model, dev, t`. +- Resolve the target by stable imported model/device identity, not incidental + container position. For this case, toggle `Line_1` at exactly 1.0 and 1.1 + seconds. +- Use GridDyn's event queue and topology-change notifications so opening and + reclosing a line causes the required solver/Jacobian reinitialization. +- Unsupported DYR records must produce structured diagnostics. This exact case + must load with no unsupported or silently ignored records. + +Independent event test: in a minimized network, open and reclose one line and +verify trigger times, link status, topology/solver refresh, and pre-/post-event +power transfer. This test must not depend on the IEEE 14 numerical reference. + +Full-case tests, in increasing order: + +1. Load the unmodified RAW/DYR and assert the exact five machine/controller + chains listed above. +2. Compare power-flow and every selected controller/machine initialization + value with ANDES. +3. Run without events over the same interval and verify bounded equilibrium + drift. +4. Run the original events through 2.0 seconds and compare common-time samples + before, at, between, and after the events. + +At minimum capture bus voltage/angle, line status/flow, GENROU `delta`, +`omega`, all transient/subtransient states, `vf`, `tm`, `Pe`, and `Qe`, plus +selected state/output signals from each governor, exciter, and stabilizer. +Store units, state names, sample times, ANDES commit, input hashes, disturbance +definition, and per-signal absolute/relative tolerances with the reference. +Event times are exact and are not relaxed for solver differences. + +Merge gate: the unmodified case completes through 2.0 seconds, reproduces both +line status changes, retains the upstream final `omega`/`tm`/`vf` checks, and +passes the stronger GridDyn time-series comparisons. Register the test as a +regular CTest target; use a slower label only if measured runtime justifies it. + +### Fresh-context restart and validation checklist + +For any new task working on PR 2 through PR 6: + +1. Read this six-PR section and the detailed compatibility tables below it. +2. Confirm every earlier PR is present in the branch and run its focused test + targets before editing. +3. Inspect the working tree and preserve unrelated user changes. Work only on + the next unmerged PR's scope. +4. Read the corresponding frozen ANDES model source and `psse-dyr.yaml` + section completely. Capture reference values deliberately; never import or + execute ANDES from the regular GridDyn C++ test. +5. Keep ANDES comparison tests out of the generic element-reader fixture + folder. Use `test/andesTests/` and + `test/test_files/andes_tests/`; keep reusable component equation tests with + their normal GridDyn component test target. +6. Compare initialization before trajectories. A successful solve alone is + not evidence of compatibility. +7. Run formatting, `git diff --check`, the new focused tests, and all earlier + affected test targets. Update the PR status and evidence in this document + before merging. + +The normal Windows build entry point is: + +```text +cmake --build build --config Debug --parallel 4 +ctest --test-dir build -C Debug --output-on-failure +``` + +Only if MSBuild reports the duplicate `Path`/`PATH` `MSB6001` error, use the +repository workaround for the same target: + +```text +cmd /v:on /c "set PATH=& set Path=& ""C:\Program Files\CMake\bin\cmake.exe"" --build build --config Debug --parallel 4" +``` + +## GENROU first-target plan + +### Current state + +- `GenModelGENROU` is registered as `genrou` and implements the ANDES gamma + coefficients, algebraic current equations, six differential equations, + electrical torque including stator copper loss, quadratic saturation, + initialization, and analytic Jacobian in GridDyn's established dq sign + convention. +- Focused tests compare unsaturated and saturated initialization against + captured values from the first GENROU machine in ANDES's + `kundur/kundur_full.json` case. A perturbed-state test compares all six + differential equations against direct ANDES-equation reference values. + Residual, derivative, algebraic, and Jacobian finite-difference checks + exercise the saturated model, and invalid reactance ordering is rejected + during initialization. +- The DYR reader creates the registered `GenModelGENROU` and applies the DYR + machine parameters after attachment so RAW stator data is retained without + allowing the RAW source reactance to overwrite `Xd`. A dedicated + `AndesCompatibilityTests` regression loads the ANDES IEEE 14-bus RAW file + and five GENROU records, then compares all eight initialized machine states, + field voltage, and mechanical power with captured ANDES v2.0.0 values at + `1e-6`. The test supplies the captured ANDES terminal operating points to + isolate DYR/model compatibility from the remaining RAW AC-network and + controller gaps. Exact machine identity/base handling and disturbed + trajectory parity remain required before compatibility is called complete. + +### Parameter contract + +| PSS/e GENROU value | ANDES value | Intended GridDyn value / note | +| ------------------ | ----------- | ----------------------------- | +| `Td10`, `Td20` | `Td10`, `Td20` | `Tdop`, `Tdopp` | +| `Tq10`, `Tq20` | `Tq10`, `Tq20` | `Tqop`, `Tqopp` | +| `H` | `M = 2 H` | Store `H`; the perturbed derivative reference verifies equivalence to ANDES's `M` convention. | +| `D` | `D` | `D`; the perturbed derivative reference verifies the speed-deviation convention on the machine base. | +| `Xd`, `Xq` | `xd`, `xq` | `Xd`, `Xq` on the machine base. | +| `Xd1`, `Xq1` | `xd1`, `xq1` | `Xdp`, `Xqp` on the machine base. | +| `Xd2` | `xd2`, and `xq2 = xd2` | `Xdpp = Xqpp = Xd2`; this is the PSS/e GENROU single-subtransient-reactance convention. | +| `Xl` | `xl` | `Xl`; use it in the same gamma-factor formulation as ANDES, not as an extra reactance added twice. | +| `S10`, `S12` | `S10`, `S12` | Fit and apply the same quadratic saturation characteristic used by ANDES. | +| Not in DYR record | `ra`, `Sn`, `Vn`, static generator link | Obtain stator resistance, machine MVA base, rated voltage, and machine identity from the paired RAW generator. | + +### GENROU implementation status + +The six-PR plan above is the authoritative merge sequence. This checklist only +records the lower-level GENROU work already completed or assigned to PR 2. + +1. **Done:** Create equation-level reference notes from ANDES `GENROU`, including its + `gd1`, `gq1`, `gd2`, `gq2`, and `gqd` coefficients, air-gap flux magnitude, + quadratic saturation, algebraic current equations, and all initialization + equations. +2. **Done:** Complete `GenModelGENROU`, register it under an unambiguous + `genrou` factory name, and leave generic model `6` behavior unchanged for + existing users. +3. **Done:** Implement every state, residual, derivative, Jacobian, initialization, and + output equation consistently. Add singular-parameter checks for invalid + reactance differences and time constants. +4. **Partial:** The `GENROU` DYR adapter now creates `GenModelGENROU`, preserves + RAW stator data, and maps all PSS/e GENROU fields. Complete exact quoted or + alphanumeric bus-plus-machine-ID lookup, verify non-unity machine-base + conversion through the RAW path, and have native ANDES input construct the + same registered class. +5. Validate in layers: parameter/attachment tests, t=0 state and algebraic + comparison, no-disturbance equilibrium, then disturbed trajectories. Only + after generator-only agreement add governor, exciter, and PSS controls. + +### Expected GridDyn implementation touchpoints + +| Area | Expected files / change | +| ---- | ----------------------- | +| Canonical machine model | **Done:** `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` is complete at the equation/initialization level and `genrou` is registered in `src/griddyn/genmodels/GenModel.cpp`; generic model `6` remains unchanged. | +| DYR parsing and attachment | **Partial:** GENROU creates the dedicated model and applies parameters in the correct RAW/DYR order. Refactor `src/fileInput/gridDynReadDYR.cpp` into schema-backed adapters and add exact bus-plus-machine-ID lookup support. | +| Native ANDES import | Extend the ANDES JSON reader's dynamic-object dispatch to build the same registered machine/controller classes and control connections used by DYR adapters. | +| Model tests | **Mostly done:** focused equation, Jacobian, initialization, saturation, invalid-parameter, and factory tests are present; add a disturbed-trajectory reference and clone regression as the model is integrated. | +| Reader tests | **Partial:** `test/andesTests/testAndesDyrReader.cpp` is a dedicated RAW/DYR GENROU attachment and initialization test. Add parser edge cases and disturbed trajectories as support expands. | +| Numerical references | **Partial:** the IEEE 14-bus RAW input, minimized GENROU DYR input, and captured ANDES initialization reference are stored under `test/test_files/andes_tests/` and run without importing ANDES. Add time-series references next. | + +### Planned GENROU reference cases + +| Case | Purpose | Required comparisons | +| ---- | ------- | -------------------- | +| Single-machine/infinite-bus `GENROU` | Isolate machine equations and bases. | Initial `delta`, `omega`, d/q currents, transient and subtransient states, field voltage, mechanical/electrical power, terminal P/Q. | +| Same case with nonzero `S10`/`S12` | Exercise saturation independently. | Air-gap flux, saturation output, field-current term, and resulting trajectory. | +| `GENROU` + `TGOV1` | Add mechanical control after machine parity. | Speed, valve/governor states, mechanical power, and limiter activity after a small load step. | +| `GENROU` + `SEXS`, then `ESDC1A`/`EXDC2` | Add excitation controls one at a time. | Terminal voltage, field voltage, exciter states, reactive power, and limiter activity after a voltage-reference step. | +| Multi-machine RAW/DYR case | Verify identity, bases, and network coupling. | Correct model attachment plus selected machine angles, speeds, bus voltages, P/Q, and relative-angle trajectories through a cleared disturbance. | + ## Dynamic-model inventory This inventory is based on the 100 registered models in the local ANDES @@ -59,20 +552,21 @@ it also needs native-input mapping, initialization, and a trajectory test. | ANDES model(s) | GridDyn mapping / next action | Status | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------- | | `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial | -| `GENROU` | `GenModelGENROU`; PSS/e DYR reader has a mapping, but native ANDES mapping and numerical validation are absent. | Partial | -| `TGOV1` | GridDyn governor; PSS/e DYR reader has a mapping, but native ANDES mapping and numerical validation are absent. | Partial | -| `EXDC2` | GridDyn DC exciter; PSS/e DYR reader has a mapping, but native ANDES mapping and numerical validation are absent. | Partial | +| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial | +| `TGOV1` | `GovernorTgov1`; correct the DYR token mapping, then audit equations, limits, initialization, and trajectories. | Partial | +| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial | | `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial | | `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial | | `ACE`, `ACEc`, `COI` | Map area-control and center-of-inertia services. | Untriaged | -| `PLBVFU1`, `IEEEVC` | Map playback and voltage-compensation controls. | Untriaged | -| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEEEG1`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Governor-family inventory and model-by-model equivalence review. | Untriaged | +| `PLBVFU1`, `IEEEVC` | No direct voltage-compensator/playback analogue; define exciter input and playback interfaces. | No direct analogue | +| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEEEG1`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Existing hydro/reheat/steam classes are candidates only; each needs an equation audit or a new model. | Planned | | `SHAFT5` | Multi-mass shaft model; map states and mechanical interfaces. | Untriaged | -| `IEEEX1`, `ESDC1A`, `ESDC2A`, `EXST1`, `ESST3A`, `SEXS`, `IEEET1`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `IEEET3`, `ESAC1A`, `ESST1A`, `ESAC5A` | Exciter-family inventory; review the remaining classes individually. | Untriaged | -| `IEEEST`, `ST2CUT` | Power-system stabilizer models; identify GridDyn analogue or implement. | Untriaged | +| `ESDC1A`, `ESDC2A`, `SEXS`, `IEEET1`, `IEEET3` | Existing DC/IEEE/SEXS exciters are candidates; complete model-specific audits and DYR/native mappings. | Partial | +| `IEEEX1`, `EXST1`, `ESST3A`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | +| `IEEEST`, `ST2CUT` | Base `Stabilizer` is not a functional equivalent; implement both models and signal connections. | No direct analogue | | `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | -| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Renewable/inverter-controller family; map plant, electrical, and frequency-control blocks. | Untriaged | -| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Wind-turbine drive-train and control models. | Untriaged | +| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | +| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | | `PVD1`, `ESD1`, `EV1`, `EV2`, `DGPRCT1`, `DGPRCTExt` | Distributed energy-resource and protection models. | Untriaged | | `Fault`, `Alter`, `TimeSeries`, `Toggle` | Event/action semantics and time-series input mapping. | Untriaged | | `Summary`, `Output` | Reporting configuration; define output-channel mapping after model compatibility. | Untriaged | @@ -89,6 +583,23 @@ it also needs native-input mapping, initialization, and a trajectory test. - Compare selected voltages, angles, injections, and flows. A successful solve by itself is not sufficient evidence of compatibility. +For dynamic cases, capture an ANDES reference time series from the same +RAW/DYR pair (or the same native ANDES case) and retain the ANDES version, +input checksum, disturbance definition, output names, units, sampling times, +and tolerance metadata beside the values. Compare in the following order: + +1. **Initialization:** selected state and algebraic values at `t = 0`, before + accepting any trajectory comparison. +2. **Equilibrium:** a no-disturbance run with bounded state/output drift. +3. **Trajectory:** state and output samples at common times around a small + reference/load step and a cleared network disturbance. + +Dynamic tolerances must be per signal and include absolute and relative terms; +angle, speed, voltage, power, and controller-state scales are different. Event +times must be compared separately. Solver differences can justify reviewed +tolerance changes, but cannot justify a phase shift, wrong limiter transition, +wrong model attachment, or a different post-event equilibrium. + ## Current reference cases | Case | Coverage | @@ -98,3 +609,4 @@ it also needs native-input mapping, initialization, and a trajectory test. | `andes_shunt_pflow` | Fixed conductance/susceptance shunt with non-system `Sn`/`Vn` bases. | | `andes_jumper_pflow` | Active and inactive zero-impedance jumpers in a loaded AC network. | | `andes_vsc_resistor_pflow` | Minimal AC/DC `VSCShunt` plus DC resistance power flow. | +| `andes_ieee14_genrou` | Five PSS/e DYR GENROU attachments and initialized machine states/inputs. | diff --git a/src/fileInput/gridDynReadDYR.cpp b/src/fileInput/gridDynReadDYR.cpp index 70ed66667..01afa1704 100644 --- a/src/fileInput/gridDynReadDYR.cpp +++ b/src/fileInput/gridDynReadDYR.cpp @@ -92,7 +92,10 @@ namespace { auto params = gmlc::utilities::str2vector(tokens, kNullVal); auto cof = CoreObjectFactory::instance(); - auto* genModel = static_cast(cof->createObject("genmodel", "6")); + auto* genModel = static_cast(cof->createObject("genmodel", "genrou")); + // Attach first so the RAW source resistance/reactance is transferred to + // the model before the DYR machine parameters replace the source Xd. + gen->add(genModel); genModel->set("tdop", params[3]); genModel->set("tdopp", params[4]); genModel->set("tqop", params[5]); @@ -108,8 +111,6 @@ namespace { genModel->set("xl", params[14]); genModel->set("s1", params[15]); genModel->set("s12", params[16]); - - gen->add(genModel); } void loadESDC1A(CoreObject* parentObject, stringVec& tokens) diff --git a/src/griddyn/genmodels/GenModel.cpp b/src/griddyn/genmodels/GenModel.cpp index cb7217b0d..a6308dd31 100644 --- a/src/griddyn/genmodels/GenModel.cpp +++ b/src/griddyn/genmodels/GenModel.cpp @@ -66,6 +66,8 @@ static ChildTypeFactory sixthOrderGenModelType2Factory( "genmodel", std::to_array({"6.2", "sixtype2", "sixthordertype2", "VItype2", "VI.2"})); +static ChildTypeFactory + genrouModelFactory("genmodel", std::to_array({"genrou"})); static ChildTypeFactory eighthOrderGenModelFactory( "genmodel", std::to_array({"8", "eight", "eighthorder", "VIII"})); diff --git a/src/griddyn/genmodels/GenModel4.cpp b/src/griddyn/genmodels/GenModel4.cpp index f4e18e7d0..e1676a71c 100644 --- a/src/griddyn/genmodels/GenModel4.cpp +++ b/src/griddyn/genmodels/GenModel4.cpp @@ -24,6 +24,7 @@ CoreObject* GenModel4::clone(CoreObject* obj) const gd->Tqop = Tqop; gd->S10 = S10; gd->S12 = S12; + gd->sat = sat; return gd; } diff --git a/src/griddyn/genmodels/GenModelGENROU.cpp b/src/griddyn/genmodels/GenModelGENROU.cpp index 739c2d8b0..ece32cf38 100644 --- a/src/griddyn/genmodels/GenModelGENROU.cpp +++ b/src/griddyn/genmodels/GenModelGENROU.cpp @@ -8,22 +8,59 @@ #include "../Generator.h" #include "../GridBus.h" +#include "core/CoreExceptions.h" #include "core/CoreObjectTemplates.hpp" #include "gmlc/utilities/vectorOps.hpp" #include "utilities/MatrixData.hpp" #include -#include -#include +#include #include namespace griddyn::genmodels { +namespace { + /** Coefficients used to eliminate ANDES's auxiliary flux algebraic states. + * + * ANDES exposes psi2d and psi2q as algebraic variables. GridDyn evaluates + * their closed-form expressions directly, reducing the nonlinear system + * without changing the machine equations documented on GenModelGENROU. + */ + struct GenrouCoefficients { + double gd1; + double gq1; + double gd2; + double gq2; + double gqd; + }; + + GenrouCoefficients computeCoefficients(double xd, + double xq, + double xdp, + double xqp, + double xdpp, + double xqpp, + double xl) + { + const double xdDifference = xdp - xl; + const double xqDifference = xqp - xl; + return {(xdpp - xl) / xdDifference, + (xqpp - xl) / xqDifference, + (xdp - xdpp) / (xdDifference * xdDifference), + (xqp - xqpp) / (xqDifference * xqDifference), + (xq - xl) / (xd - xl)}; + } + +} // namespace + GenModelGENROU::GenModelGENROU(const std::string& objName): GenModel5(objName) { - // default values Xdpp = 0.2; Xqpp = 0.2; Xqp = 0.30; D = 0.03; + S10 = 0.0; + S12 = 1.0; + sat.setType(utilities::Saturation::SaturationType::CUTOFF_SCALED_QUADRATIC); + sat.setParam(S10, S12); } CoreObject* GenModelGENROU::clone(CoreObject* obj) const @@ -37,44 +74,71 @@ CoreObject* GenModelGENROU::clone(CoreObject* obj) const void GenModelGENROU::dynObjectInitializeA(CoreTime /*time0*/, std::uint32_t /*flags*/) { + if ((H <= 0.0) || (Tdop <= 0.0) || (Tdopp <= 0.0) || (Tqop <= 0.0) || (Tqopp <= 0.0) || + (Xd <= Xdp) || (Xdp < Xdpp) || (Xdpp < Xl) || (Xq <= Xqp) || (Xqp < Xqpp) || (Xqpp < Xl)) { + throw InvalidParameterValue("GENROU reactances, inertia, or time constants"); + } + offsets.local().local.diffSize = 6; offsets.local().local.algSize = 2; - offsets.local().local.jacSize = 37; + offsets.local().local.jacSize = 50; } -// initial conditions + void GenModelGENROU::dynObjectInitializeB(const IOdata& inputs, const IOdata& desiredOutput, IOdata& fieldSet) { - computeInitialAngleAndCurrent(inputs, desiredOutput, Rs, Xq); - double* gm = m_state.data(); - - double D1 = (Xdpp) / (Xdp); - double Q1 = (Xqpp) / (Xqp); - - double D2 = (Xdp - Xdpp) / (Xdp); - double Q2 = (Xqp - Xqpp) / (Xqp); - - gm[4] = -(Xq - Xqp) * gm[1]; - - // psiq - - gm[7] = -gm[4] - (Xqp)*gm[1]; - - gm[6] = ((Xdpp + Xl) * gm[0] + (Xdpp)*gm[0] - Vq - Rs * gm[1]) / (D2 - D1); - - gm[5] = gm[6] + (Xd)*gm[0]; - // record Pm = Pset - // this should be close to P from above - double Pmt = D1 * gm[5] * gm[0] - D2 * gm[6] * gm[0] - Q1 * gm[4] * gm[0] + Q2 * gm[7] * gm[0]; - - // exciter - assign Ef - double Eft = gm[5] - (Xd - Xdp) * gm[0]; + // This complex initialization follows ANDES GENROU and its cited OpenIPSL + // reference. The explicit sign changes below translate the rotated ANDES + // quantities into the GridDyn dq convention documented in the header. + const double voltageMagnitude = inputs[VOLTAGE_IN_LOCATION]; + const double voltageAngle = inputs[ANGLE_IN_LOCATION]; + const std::complex terminalVoltage = std::polar(voltageMagnitude, voltageAngle); + const std::complex terminalPower(desiredOutput[POUT_LOCATION], + -desiredOutput[QOUT_LOCATION]); + const std::complex terminalCurrent = terminalPower / std::conj(terminalVoltage); + const std::complex statorImpedance(Rs, Xdpp); + const std::complex subtransientFlux = + terminalVoltage + terminalCurrent * statorImpedance; + + const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); + const double fluxMagnitude = std::abs(subtransientFlux); + const double saturation = sat.compute(fluxMagnitude); + const double currentMagnitude = std::abs(terminalCurrent); + const double fluxCurrentAngle = std::arg(subtransientFlux) - std::arg(terminalCurrent); + const double angleNumerator = currentMagnitude * (Xqpp - Xq) * std::cos(fluxCurrentAngle); + const double angleDenominator = currentMagnitude * (Xqpp - Xq) * std::sin(fluxCurrentAngle) - + fluxMagnitude * (1.0 + saturation * coefficients.gqd); + const double rotorAngle = + std::atan(angleNumerator / angleDenominator) + std::arg(subtransientFlux); + + const std::complex dqRotation = std::polar(1.0, -rotorAngle); + const std::complex fluxDq = subtransientFlux * dqRotation; + const std::complex currentDq = std::conj(terminalCurrent * dqRotation); + const double andesId = std::imag(currentDq); + const double iq = std::real(currentDq); + const double idCurrent = -andesId; + const double psi2d = std::real(fluxDq); + const double psi2q = std::imag(fluxDq); // negative of the ANDES q-axis flux convention - // double g4 = gm[6] + (Xqp - Xqpp + qrat * (Xq - Xqp)) * gm[1]; - // preset the inputs that should be initialized - fieldSet[2] = Eft; - fieldSet[3] = Pmt; + double* gm = m_state.data(); + gm[0] = idCurrent; + gm[1] = iq; + gm[2] = rotorAngle; + gm[3] = 1.0; + + Vd = -voltageMagnitude * std::sin(rotorAngle - voltageAngle); + Vq = voltageMagnitude * std::cos(rotorAngle - voltageAngle); + + const double fieldVoltage = (1.0 + saturation) * psi2d + (Xd - Xdpp) * andesId; + gm[4] = -(Xq - Xqp) * iq - saturation * coefficients.gqd * psi2q; + gm[5] = (Xd - Xdp) * idCurrent - saturation * psi2d + fieldVoltage; + gm[6] = -(Xq - Xl) * iq - saturation * coefficients.gqd * psi2q; + gm[7] = (Xd - Xl) * idCurrent - saturation * psi2d + fieldVoltage; + + const double mechanicalPower = (Vd + Rs * idCurrent) * idCurrent + (Vq + Rs * iq) * iq; + fieldSet[genModelEftInLocation] = fieldVoltage; + fieldSet[genModelPmechInLocation] = mechanicalPower; } void GenModelGENROU::algebraicUpdate(const IOdata& inputs, @@ -85,14 +149,14 @@ void GenModelGENROU::algebraicUpdate(const IOdata& inputs, { auto Loc = offsets.getLocations(sD, update, sMode, this); updateLocalCache(inputs, sD, sMode); - gmlc::utilities::solve2x2(Rs, - (Xqpp + Xl), - -(Xdpp + Xl), - Rs, - Loc.diffStateLoc[4] - Vd, - Loc.diffStateLoc[5] - Vq, - Loc.destLoc[0], - Loc.destLoc[1]); + const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); + const double psi2q = + coefficients.gq1 * Loc.diffStateLoc[2] + (1.0 - coefficients.gq1) * Loc.diffStateLoc[4]; + const double psi2d = + coefficients.gd1 * Loc.diffStateLoc[3] + (1.0 - coefficients.gd1) * Loc.diffStateLoc[5]; + + gmlc::utilities::solve2x2( + Rs, Xqpp, -Xdpp, Rs, psi2q - Vd, psi2d - Vq, Loc.destLoc[0], Loc.destLoc[1]); m_output = -(Loc.destLoc[1] * Vq + Loc.destLoc[0] * Vd); } @@ -101,61 +165,38 @@ void GenModelGENROU::derivative(const IOdata& inputs, double deriv[], const SolverMode& sMode) { + if (isAlgebraicOnly(sMode)) { + return; + } auto Loc = offsets.getLocations(sD, deriv, sMode, this); - - // double V = inputs[VOLTAGE_IN_LOCATION]; - const double* gm = Loc.algStateLoc; - const double* gmd = Loc.diffStateLoc; - const double* gmp = Loc.dstateLoc; - - // double *rva = Loc.destLoc; - double* rvd = Loc.destDiffLoc; - // double angle = gmd[0] - inputs[ANGLE_IN_LOCATION]; - // double Vq = V * cos (angle); - // double Vd = -V*sin (angle); - - // Get the exciter field - double Eft = inputs[genModelEftInLocation]; - double Pmt = inputs[genModelPmechInLocation]; - - double D1 = (Xdpp) / (Xdp); - double Q1 = (Xqpp) / (Xqp); - - double D2 = (Xdp - Xdpp) / (Xdp); - double Q2 = (Xqp - Xqpp) / (Xqp); - - // delta - rvd[0] = systemBaseFrequency * (gmd[1] - 1.0); - // Edp and Eqp - rvd[2] = - (-gmd[2] - (Xq - Xqp) * (gm[1] - (Q2 / (Xqp)) * (gmd[5] + (Xqp)*gm[1] + gmd[2]))) / Tqop; - - rvd[3] = - (-gmd[3] - (Xd - Xdp) * (gm[0] - (D2 / (Xdp)) * (gmd[4] + (Xdp)*gm[0] + gmd[3])) + Eft) / + const double* alg = Loc.algStateLoc; + const double* state = Loc.diffStateLoc; + double* dstate = Loc.destDiffLoc; + const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); + + const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; + const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + const double fluxMagnitude = std::hypot(psi2d, psi2q); + const double saturation = sat.compute(fluxMagnitude); + + const double fieldVoltage = inputs[genModelEftInLocation]; + const double mechanicalPower = inputs[genModelPmechInLocation]; + const double electricalTorque = (Vd + Rs * alg[0]) * alg[0] + (Vq + Rs * alg[1]) * alg[1]; + + dstate[0] = systemBaseFrequency * (state[1] - 1.0); + dstate[1] = 0.5 * (mechanicalPower - electricalTorque - D * (state[1] - 1.0)) / H; + dstate[2] = + (-state[2] - + (Xq - Xqp) * (coefficients.gq2 * (state[2] - state[4]) + coefficients.gq1 * alg[1]) - + saturation * coefficients.gqd * psi2q) / + Tqop; + dstate[3] = + (fieldVoltage - state[3] + + (Xd - Xdp) * (coefficients.gd1 * alg[0] + coefficients.gd2 * (state[5] - state[3])) - + saturation * psi2d) / Tdop; - // psid - rvd[4] = (-gmd[4] + gmd[3] - (Xdp)*gm[0]) / Tdopp; - - rvd[5] = (-gmd[5] - gmd[2] - (Xqp)*gm[1]) / Tqopp; - // omega - - double Pe = - D1 * gmd[3] * gm[1] + D2 * gmd[4] * gm[1] - Q1 * gmd[2] * gm[0] + Q2 * gmd[5] * gm[0]; - - rvd[1] = 0.5 * (Pmt - Pe - D * (gmd[1] - 1.0)) / H; - // Edp and Eqp - if (std::abs(gmp[1]) > 0.0001) { - std::println("[{}]t={} gmp[1]={} Pdiff={} A={}, B={}, C={} Xdp={:3.3f}, Xqp={:3.3f}", - static_cast(getID()), - static_cast(sD.time), - gmp[1], - Pmt - Pe, - gmd[4] * gm[0], - gmd[5] * gm[1], - (Xdpp - Xqpp) * gm[0] * gm[1], - Xdp, - Xqp); - } + dstate[4] = (-state[4] + state[2] - (Xqp - Xl) * alg[1]) / Tqopp; + dstate[5] = (-state[5] + state[3] + (Xdp - Xl) * alg[0]) / Tdopp; } void GenModelGENROU::residual(const IOdata& inputs, @@ -164,34 +205,25 @@ void GenModelGENROU::residual(const IOdata& inputs, const SolverMode& sMode) { auto Loc = offsets.getLocations(sD, resid, sMode, this); - - const double* gm = Loc.algStateLoc; - const double* gmd = Loc.diffStateLoc; - const double* gmp = Loc.dstateLoc; - double* rva = Loc.destLoc; - double* rvd = Loc.destDiffLoc; + const double* alg = Loc.algStateLoc; + const double* state = Loc.diffStateLoc; + const double* dstate = Loc.dstateLoc; + double* algResidual = Loc.destLoc; + double* stateResidual = Loc.destDiffLoc; updateLocalCache(inputs, sD, sMode); - double D1 = (Xdpp) / (Xdp); - double Q1 = (Xqpp) / (Xqp); - - double D2 = (Xdp - Xdpp) / (Xdp); - double Q2 = (Xqp - Xqpp) / (Xqp); - // Id and Iq + const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); if (hasAlgebraic(sMode)) { - rva[0] = Vd + Rs * gm[0] + (Xqpp + Xl) * gm[1] - Q1 * gmd[2] + Q2 * gmd[5]; - rva[1] = Vq + Rs * gm[1] - (Xdpp + Xl) * gm[0] - D1 * gmd[3] + D2 * gmd[4]; + const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; + const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + algResidual[0] = Vd + Rs * alg[0] + Xqpp * alg[1] - psi2q; + algResidual[1] = Vq + Rs * alg[1] - Xdpp * alg[0] - psi2d; } if (hasDifferential(sMode)) { derivative(inputs, sD, resid, sMode); - // Get the exciter field - // delta - rvd[0] -= gmp[0]; - rvd[1] -= gmp[1]; - rvd[2] -= gmp[2]; - rvd[3] -= gmp[3]; - rvd[4] -= gmp[4]; - rvd[5] -= gmp[5]; + for (index_t index = 0; index < 6; ++index) { + stateResidual[index] -= dstate[index]; + } } } @@ -202,132 +234,141 @@ void GenModelGENROU::jacobianElements(const IOdata& inputs, const SolverMode& sMode) { auto Loc = offsets.getLocations(sD, sMode, this); - - double V = inputs[VOLTAGE_IN_LOCATION]; - // double theta = inputs[ANGLE_IN_LOCATION]; - const double* gm = Loc.algStateLoc; - const double* gmd = Loc.diffStateLoc; - // const double *gmp = Loc.dstateLoc; - + const double* alg = Loc.algStateLoc; + const double* state = Loc.diffStateLoc; + const auto refAlg = Loc.algOffset; + const auto refDiff = Loc.diffOffset; + const auto voltageLoc = inputLocs[VOLTAGE_IN_LOCATION]; + const auto angleLoc = inputLocs[ANGLE_IN_LOCATION]; updateLocalCache(inputs, sD, sMode); - auto refAlg = Loc.algOffset; - auto refDiff = Loc.diffOffset; - - auto VLoc = inputLocs[VOLTAGE_IN_LOCATION]; - auto TLoc = inputLocs[ANGLE_IN_LOCATION]; - - // Id Differential - double D1 = (Xdpp) / (Xdp); - double Q1 = (Xqpp) / (Xqp); - - double D2 = (Xdp - Xdpp) / (Xdp); - double Q2 = (Xqp - Xqpp) / (Xqp); - + const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); if (hasAlgebraic(sMode)) { - // P - if (TLoc != kNullLocation) { - md.assign(refAlg, TLoc, Vq); - md.assign(refAlg + 1, TLoc, -Vd); + if (angleLoc != kNullLocation) { + md.assign(refAlg, angleLoc, Vq); + md.assign(refAlg + 1, angleLoc, -Vd); } - - // Q - if (VLoc != kNullLocation) { - md.assign(refAlg, VLoc, Vd / V); - md.assign(refAlg + 1, VLoc, Vq / V); + if (voltageLoc != kNullLocation) { + md.assign(refAlg, voltageLoc, Vd / inputs[VOLTAGE_IN_LOCATION]); + md.assign(refAlg + 1, voltageLoc, Vq / inputs[VOLTAGE_IN_LOCATION]); } - md.assign(refAlg, refAlg, Rs); - md.assign(refAlg, refAlg + 1, (Xqpp + Xl)); - - md.assign(refAlg + 1, refAlg, -(Xdpp + Xl)); + md.assign(refAlg, refAlg + 1, Xqpp); + md.assign(refAlg + 1, refAlg, -Xdpp); md.assign(refAlg + 1, refAlg + 1, Rs); + if (isAlgebraicOnly(sMode)) { return; } - - // rva[0] = Vd + Rs * gm[0] + (Xqpp)* gm[1] - qrat*gmd[2] + (Xqp - Xqpp) / - // (Xqp)*gmd[5]; - // rva[1] = Vq + Rs * gm[1] - (Xdpp)* gm[0] - drat*gmd[3] + (Xdp - Xdpp) / - // (Xdp)*gmd[4]; - md.assign(refAlg, refDiff, -Vq); - md.assign(refAlg, refDiff + 2, -Q1); - md.assign(refAlg, refDiff + 5, Q2); - // Iq Differential - + md.assign(refAlg, refDiff + 2, -coefficients.gq1); + md.assign(refAlg, refDiff + 4, -(1.0 - coefficients.gq1)); md.assign(refAlg + 1, refDiff, Vd); - md.assign(refAlg + 1, refDiff + 3, -D1); - md.assign(refAlg + 1, refDiff + 4, D2); + md.assign(refAlg + 1, refDiff + 3, -coefficients.gd1); + md.assign(refAlg + 1, refDiff + 5, -(1.0 - coefficients.gd1)); } - // delta + + if (!hasDifferential(sMode)) { + return; + } + md.assign(refDiff, refDiff, -sD.cj); md.assign(refDiff, refDiff + 1, systemBaseFrequency); - // double Pe =D1*gmd[3] * gm[1] + D2*gmd[4] * gm[1] - Q1*gmd[2] * gm[0] + - // Q2*gmd[5] * gm[0]; - - // rvd[1] = 0.5 * (Pmt - Pe - D * (gmd[1] - 1.0)) / H; - // omega - double kVal = -0.5 / H; + const double inverseTwoH = 0.5 / H; if (hasAlgebraic(sMode)) { - md.assign(refDiff + 1, refAlg, -0.5 * (-Q1 * gmd[2] + Q2 * gmd[5]) / H); - md.assign(refDiff + 1, refAlg + 1, -0.5 * (D1 * gmd[3] + D2 * gmd[4]) / H); + md.assign(refDiff + 1, refAlg, -inverseTwoH * (Vd + 2.0 * Rs * alg[0])); + md.assign(refDiff + 1, refAlg + 1, -inverseTwoH * (Vq + 2.0 * Rs * alg[1])); + } + md.assign(refDiff + 1, refDiff, inverseTwoH * (Vq * alg[0] - Vd * alg[1])); + md.assign(refDiff + 1, refDiff + 1, -inverseTwoH * D - sD.cj); + md.assignCheckCol(refDiff + 1, inputLocs[genModelPmechInLocation], inverseTwoH); + if (angleLoc != kNullLocation) { + md.assign(refDiff + 1, angleLoc, -inverseTwoH * (Vq * alg[0] - Vd * alg[1])); + } + if (voltageLoc != kNullLocation) { + md.assign(refDiff + 1, + voltageLoc, + -inverseTwoH * (Vd * alg[0] + Vq * alg[1]) / inputs[VOLTAGE_IN_LOCATION]); } - md.assign(refDiff + 1, refDiff + 1, -0.5 * D / H - sD.cj); - md.assign(refDiff + 1, refDiff + 2, -0.5 * (-Q1 * gm[0]) / H); - md.assign(refDiff + 1, refDiff + 3, -0.5 * (D1 * gm[1]) / H); - md.assign(refDiff + 1, refDiff + 4, -0.5 * D2 * gm[1] / H); - md.assign(refDiff + 1, refDiff + 5, -0.5 * Q2 * gm[0] / H); - - md.assignCheckCol(refDiff + 1, inputLocs[genModelPmechInLocation], -kVal); // governor: Pm - - // Edp and Eqp - // rvd[2] = (-gmd[2] - (Xq - Xqp) - // *(gm[1]-(Q2/(Xqp))*(gmd[5]+(Xqp)*gm[1]+gmd[2]))) / Tqop; - - // rvd[3] = (-gmd[3] - (Xd - Xdp) *(gm[0] - (D2/(Xdp))*(gmd[4] + (Xdp)*gm[0] + - // gmd[3]))+Eft) / Tdop; + const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; + const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + const double fluxMagnitude = std::hypot(psi2d, psi2q); + const auto saturation = sat.evaluate(fluxMagnitude); + double dSaturationD = 0.0; + double dSaturationQ = 0.0; + if (fluxMagnitude > 0.0) { + dSaturationD = saturation.derivative * psi2d / fluxMagnitude; + dSaturationQ = saturation.derivative * psi2q / fluxMagnitude; + } + const double dQSatD = coefficients.gqd * psi2q * dSaturationD; + const double dQSatQ = coefficients.gqd * (saturation.value + psi2q * dSaturationQ); + const double dDSatD = saturation.value + psi2d * dSaturationD; + const double dDSatQ = psi2d * dSaturationQ; - // Edp if (hasAlgebraic(sMode)) { - md.assign(refDiff + 2, refAlg + 1, (-(Xq - Xqp) * (1.0 - Q2)) / Tqop); + md.assign(refDiff + 2, refAlg + 1, -(Xq - Xqp) * coefficients.gq1 / Tqop); } - md.assign(refDiff + 2, refDiff + 5, Q2 * (Xq - Xqp) / (Xqp) / Tqop); - md.assign(refDiff + 2, refDiff + 2, (-1.0 + Q2 * (Xq - Xqp) / (Xqp)) / Tqop - sD.cj); + md.assign(refDiff + 2, + refDiff + 2, + (-1.0 - (Xq - Xqp) * coefficients.gq2 - dQSatQ * coefficients.gq1) / Tqop - sD.cj); + md.assign(refDiff + 2, + refDiff + 4, + ((Xq - Xqp) * coefficients.gq2 - dQSatQ * (1.0 - coefficients.gq1)) / Tqop); + md.assign(refDiff + 2, refDiff + 3, -dQSatD * coefficients.gd1 / Tqop); + md.assign(refDiff + 2, refDiff + 5, -dQSatD * (1.0 - coefficients.gd1) / Tqop); - // Eqp if (hasAlgebraic(sMode)) { - md.assign(refDiff + 3, refAlg, (-(Xd - Xdp) * (1.0 - D2)) / Tdop); + md.assign(refDiff + 3, refAlg, (Xd - Xdp) * coefficients.gd1 / Tdop); } - md.assign(refDiff + 3, refDiff + 4, D2 * (Xd - Xdp) / (Xdp) / Tdop); - md.assign(refDiff + 3, refDiff + 3, (-1.0 + D2 * (Xd - Xdp) / (Xdp)) / Tdop - sD.cj); + md.assign(refDiff + 3, + refDiff + 3, + (-1.0 - (Xd - Xdp) * coefficients.gd2 - dDSatD * coefficients.gd1) / Tdop - sD.cj); + md.assign(refDiff + 3, + refDiff + 5, + ((Xd - Xdp) * coefficients.gd2 - dDSatD * (1.0 - coefficients.gd1)) / Tdop); + md.assign(refDiff + 3, refDiff + 2, -dDSatQ * coefficients.gq1 / Tdop); + md.assign(refDiff + 3, refDiff + 4, -dDSatQ * (1.0 - coefficients.gq1) / Tdop); + md.assignCheckCol(refDiff + 3, inputLocs[genModelEftInLocation], 1.0 / Tdop); - md.assignCheckCol(refDiff + 3, inputLocs[genModelEftInLocation], - 1.0 / Tdop); // exciter: Ef - - // psid - // rvd[4] = (-gmd[4] + gmd[3] - (Xdp)*gm[0]) / Tdopp; if (hasAlgebraic(sMode)) { - md.assign(refDiff + 4, refAlg, -(Xdp) / Tdopp); + md.assign(refDiff + 4, refAlg + 1, -(Xqp - Xl) / Tqopp); } - md.assign(refDiff + 4, refDiff + 3, 1.0 / Tdopp); - md.assign(refDiff + 4, refDiff + 4, -1.0 / Tdopp - sD.cj); - - // psiq rvd[5] = (-gmd[5] - gmd[2] - (Xqp)*gm[1]) / Tqopp; + md.assign(refDiff + 4, refDiff + 2, 1.0 / Tqopp); + md.assign(refDiff + 4, refDiff + 4, -1.0 / Tqopp - sD.cj); if (hasAlgebraic(sMode)) { - md.assign(refDiff + 5, refAlg + 1, -(Xqp) / Tqopp); + md.assign(refDiff + 5, refAlg, (Xdp - Xl) / Tdopp); + } + md.assign(refDiff + 5, refDiff + 3, 1.0 / Tdopp); + md.assign(refDiff + 5, refDiff + 5, -1.0 / Tdopp - sD.cj); +} + +void GenModelGENROU::set(std::string_view param, std::string_view val) +{ + GenModel5::set(param, val); +} + +void GenModelGENROU::set(std::string_view param, double val, units::unit unitType) +{ + if ((param == "s1") || (param == "s10")) { + S10 = val; + sat.setParam(S10, S12); + } else if (param == "s12") { + S12 = val; + sat.setParam(S10, S12); + } else if (param == "xpp") { + Xdpp = val; + Xqpp = val; + } else { + GenModel5::set(param, val, unitType); } - md.assign(refDiff + 5, refDiff + 2, -1.0 / Tqopp); - md.assign(refDiff + 5, refDiff + 5, -1.0 / Tqopp - sD.cj); } -static const stringVec - genModelGenRouNames{"id", "iq", "delta", "freq", "edp", "eqp", "psid", "psiq"}; +static const stringVec genModelGenrouNames{"id", "iq", "delta", "freq", "e1d", "e1q", "e2q", "e2d"}; stringVec GenModelGENROU::localStateNames() const { - return genModelGenRouNames; + return genModelGenrouNames; } } // namespace griddyn::genmodels diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index 569c10387..ed8e02a5f 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -10,6 +10,153 @@ #include namespace griddyn::genmodels { +/** + * @brief Sixth-order PSS/E GENROU round-rotor synchronous-machine model. + * + * @details + * This class implements the differential-algebraic GENROU formulation used by + * ANDES, translated to GridDyn's established dq-axis sign convention. All + * electrical quantities are per-unit on the machine base. The algebraic states + * are \f$[I_d,I_q]\f$ and the differential states are + * \f$[\delta,\omega,e'_{d},e'_{q},e''_{q},e''_{d}]\f$. The input vector supplies + * terminal voltage magnitude \f$V\f$, terminal angle \f$\theta\f$, field + * voltage \f$E_f\f$, and mechanical power \f$P_m\f$. + * + * @par Rotor-frame voltage and coefficient definitions + * The terminal-voltage components are + * \f[ + * V_d=-V\sin(\delta-\theta), \qquad + * V_q= V\cos(\delta-\theta). + * \f] + * The transient/subtransient coefficients follow the ANDES notation: + * \f[ + * \begin{aligned} + * \gamma_{d1} &= \frac{x''_d-x_l}{x'_d-x_l}, & + * \gamma_{q1} &= \frac{x''_q-x_l}{x'_q-x_l}, \\ + * \gamma_{d2} &= \frac{x'_d-x''_d}{(x'_d-x_l)^2}, & + * \gamma_{q2} &= \frac{x'_q-x''_q}{(x'_q-x_l)^2}, \\ + * \gamma_{qd} &= \frac{x_q-x_l}{x_d-x_l}. + * \end{aligned} + * \f] + * GridDyn parameter names \c Xdp, \c Xqp, \c Xdpp, and \c Xqpp denote + * \f$x'_d\f$, \f$x'_q\f$, \f$x''_d\f$, and \f$x''_q\f$, respectively. + * + * @par Air-gap flux and stator algebraic equations + * The subtransient air-gap flux components and magnitude are + * \f[ + * \begin{aligned} + * \psi''_q &= \gamma_{q1}e'_d+(1-\gamma_{q1})e''_q,\\ + * \psi''_d &= \gamma_{d1}e'_q+(1-\gamma_{d1})e''_d,\\ + * \psi'' &= \sqrt{(\psi''_d)^2+(\psi''_q)^2}. + * \end{aligned} + * \f] + * The two algebraic residuals are + * \f[ + * \begin{aligned} + * 0 &= V_d+r_a I_d+x''_q I_q-\psi''_q,\\ + * 0 &= V_q+r_a I_q-x''_d I_d-\psi''_d. + * \end{aligned} + * \f] + * Electrical torque, including stator copper loss, is + * \f[ + * T_e=(V_d+r_a I_d)I_d+(V_q+r_a I_q)I_q. + * \f] + * + * @par Differential equations + * With synchronous electrical speed \f$\omega_b=2\pi f_b\f$, inertia \f$H\f$, + * damping \f$D\f$, and saturation \f$S_e\f$, the implemented equations are + * \f[ + * \begin{aligned} + * \dot\delta &= \omega_b(\omega-1),\\ + * \dot\omega &= \frac{P_m-T_e-D(\omega-1)}{2H},\\ + * T'_{q0}\dot e'_d &= -e'_d + * -(x_q-x'_q)\left[\gamma_{q2}(e'_d-e''_q)+\gamma_{q1}I_q\right] + * -S_e\gamma_{qd}\psi''_q,\\ + * T'_{d0}\dot e'_q &= E_f-e'_q + * +(x_d-x'_d)\left[\gamma_{d1}I_d+\gamma_{d2}(e''_d-e'_q)\right] + * -S_e\psi''_d,\\ + * T''_{q0}\dot e''_q &= -e''_q+e'_d-(x'_q-x_l)I_q,\\ + * T''_{d0}\dot e''_d &= -e''_d+e'_q+(x'_d-x_l)I_d. + * \end{aligned} + * \f] + * The GridDyn parameters \c Tqop, \c Tdop, \c Tqopp, and \c Tdopp correspond + * to \f$T'_{q0}\f$, \f$T'_{d0}\f$, \f$T''_{q0}\f$, and \f$T''_{d0}\f$. + * + * @par Quadratic saturation + * For positive saturation points \f$S_{1.0}\f$ and \f$S_{1.2}\f$, define + * \f[ + * r=\sqrt{\frac{S_{1.0}}{1.2S_{1.2}}},\qquad + * A=\frac{1-1.2r}{1-r},\qquad + * B=\frac{S_{1.0}}{(1-A)^2}. + * \f] + * The ANDES ExcQuadSat characteristic is then + * \f[ + * S_e(\psi'')= + * \begin{cases} + * 0, & \psi'' < A,\\ + * \displaystyle\frac{B(\psi''-A)^2}{\psi''}, & \psi''\ge A. + * \end{cases} + * \f] + * Saturation is disabled when either configured saturation value is not + * positive. The default \f$S_{1.0}=0\f$, \f$S_{1.2}=1\f$ is the ANDES + * no-saturation convention. + * + * @par Initialization + * Given terminal power \f$P+jQ\f$, initialization first forms + * \f[ + * \underline V=Ve^{j\theta},\quad + * \underline I=\frac{P-jQ}{\underline V^*},\quad + * \underline\psi''=\underline V+(r_a+jx''_d)\underline I. + * \f] + * If \f$\chi=\arg(\underline\psi'')-\arg(\underline I)\f$, + * \f$a=|\underline\psi''|(1+S_e\gamma_{qd})\f$, and + * \f$b=|\underline I|(x''_q-x_q)\f$, the initial rotor angle is + * \f[ + * \delta_0=\tan^{-1}\!\left(\frac{b\cos\chi}{b\sin\chi-a}\right) + * +\arg(\underline\psi''). + * \f] + * The complex flux and current are then rotated into the rotor frame. The + * remaining states are initialized from the steady-state forms of the six + * equations above, with + * \f[ + * E_{f0}=(1+S_e)\psi''_{d0}-(x_d-x''_d)I_{d0},\qquad P_{m0}=T_{e0}. + * \f] + * + * @par GridDyn and ANDES sign conventions + * To retain compatibility with existing GridDyn generator models, the + * translation uses + * \f[ + * \begin{array}{c|rrrrrr} + * &I_d&V_d&e'_d&e''_q&\psi''_q&\delta\\ \hline + * \text{GridDyn}/\text{ANDES}&-1&-1&-1&-1&-1&+1 + * \end{array} + * \f] + * while \f$I_q,V_q,e'_q,e''_d,\psi''_d\f$, and \f$\omega\f$ have the same + * signs. This mapping changes representation only; it does not change terminal + * power, torque, or rotor motion. + * + * @par Equation sources + * - ANDES + * v2.0 GENROU equations and initialization + * - ANDES + * v2.0 synchronous-generator base equations + * - OpenIPSL + * PSS/E GENROU initialization reference + * - ANDES + * GENROU verification against PSS/E, OpenIPSL, and TSAT + * - H. Cui, F. Li, and K. Tomsovic, "Hybrid Symbolic-Numeric Framework for + * Power System Modeling and Analysis," IEEE Transactions on Power Systems, + * 36(2), 1373-1384, 2021, + * doi:10.1109/TPWRS.2020.3017019. + * + * @note PSS/E requires the GENROU d- and q-axis subtransient reactances to be + * equal. The class retains separate \c Xdpp and \c Xqpp storage to fit the + * GridDyn architecture; the \c xpp setter assigns both values. + */ class GenModelGENROU: public GenModel5 { protected: public: @@ -20,6 +167,10 @@ class GenModelGENROU: public GenModel5 { const IOdata& desiredOutput, IOdata& fieldSet) override; + virtual void set(std::string_view param, std::string_view val) override; + virtual void + set(std::string_view param, double val, units::unit unitType = units::defunit) override; + virtual stringVec localStateNames() const override; // dynamics virtual void residual(const IOdata& inputs, diff --git a/src/utilities/Saturation.cpp b/src/utilities/Saturation.cpp index 7e1fc816c..3689f399c 100644 --- a/src/utilities/Saturation.cpp +++ b/src/utilities/Saturation.cpp @@ -18,8 +18,44 @@ Saturation::Saturation(SaturationType sT): type(sT) Saturation::Saturation(const std::string& satType): type(SaturationType::NONE) { setType(satType); +} + +Saturation::Saturation(const Saturation& other): + s10(other.s10), s12(other.s12), A(other.A), B(other.B), type(other.type) +{ loadFunctions(); - computeParam(); +} + +Saturation::Saturation(Saturation&& other) noexcept: + s10(other.s10), s12(other.s12), A(other.A), B(other.B), type(other.type) +{ + loadFunctions(); +} + +Saturation& Saturation::operator=(const Saturation& other) +{ + if (this != &other) { + s10 = other.s10; + s12 = other.s12; + A = other.A; + B = other.B; + type = other.type; + loadFunctions(); + } + return *this; +} + +Saturation& Saturation::operator=(Saturation&& other) noexcept +{ + if (this != &other) { + s10 = other.s10; + s12 = other.s12; + A = other.A; + B = other.B; + type = other.type; + loadFunctions(); + } + return *this; } void Saturation::setType(const std::string& stype) @@ -30,6 +66,8 @@ void Saturation::setType(const std::string& stype) type = SaturationType::QUADRATIC; } else if (stype == "scaled_quadratic") { type = SaturationType::SCALED_QUADRATIC; + } else if ((stype == "cutoff_scaled_quadratic") || (stype == "clamped_scaled_quadratic")) { + type = SaturationType::CUTOFF_SCALED_QUADRATIC; } else if (stype == "exponential") { type = SaturationType::EXPONENTIAL; } else if (stype == "linear") { @@ -54,10 +92,23 @@ void Saturation::setParam(double V1, double S1, double V2, double S2) A = -(V2 * ssv - V1) / (V1 - ssv); B = S1 / ((V1 - A) * (V1 - A)); } break; - case SaturationType::SCALED_QUADRATIC: { + case SaturationType::SCALED_QUADRATIC: + case SaturationType::CUTOFF_SCALED_QUADRATIC: { + if ((V1 <= 0.0) || (V2 <= 0.0) || (S1 <= 0.0) || (S2 <= 0.0)) { + A = 0.0; + B = 0.0; + break; + } double ssv = sqrt((S1 * V1) / (S2 * V2)); - A = -(V2 * ssv - V1) / (V1 - ssv); - B = S1 / ((V1 - A) * (V1 - A)); + const double fitDenominator = V1 - ssv; + if (std::abs(fitDenominator) < 1e-12) { + A = 0.0; + B = 0.0; + break; + } + A = -(V2 * ssv - V1) / fitDenominator; + const double distance = V1 - A; + B = (std::abs(distance) < 1e-12) ? 0.0 : S1 / (distance * distance); } break; case SaturationType::EXPONENTIAL: A = log(S1 / S2) / log(V1 / V2); @@ -100,9 +151,13 @@ double Saturation::deriv(double val) const { return derivFunc(val); } +Saturation::Evaluation Saturation::evaluate(double val) const +{ + return {satFunc(val), derivFunc(val)}; +} double Saturation::inv(double val) const { - if (val < 0.00001) { + if ((val < 0.00001) || (B == 0.0)) { return 0.5; } double ret = 0.5; @@ -110,9 +165,10 @@ double Saturation::inv(double val) const case SaturationType::QUADRATIC: ret = sqrt(val / B) + A; break; - case SaturationType::SCALED_QUADRATIC: { + case SaturationType::SCALED_QUADRATIC: + case SaturationType::CUTOFF_SCALED_QUADRATIC: { double temp = (2 * A + val / B); - ret = (temp + sqrt(temp - 4 * A * A)) / 2; + ret = (temp + sqrt(temp * temp - 4 * A * A)) / 2; break; } case SaturationType::EXPONENTIAL: @@ -132,24 +188,54 @@ void Saturation::computeParam() { switch (type) { case SaturationType::QUADRATIC: { + if ((s10 <= 0.0) || (s12 <= 0.0)) { + A = 0.0; + B = 0.0; + break; + } double ssv = sqrt(s10 / s12); - A = -(1.2 * ssv - 1.0) / (1.0 - ssv); - B = s10 / ((1.0 - A) * (1.0 - A)); + const double fitDenominator = 1.0 - ssv; + if (std::abs(fitDenominator) < 1e-12) { + A = 0.0; + B = 0.0; + break; + } + A = -(1.2 * ssv - 1.0) / fitDenominator; + const double distance = 1.0 - A; + B = (std::abs(distance) < 1e-12) ? 0.0 : s10 / (distance * distance); break; } - case SaturationType::SCALED_QUADRATIC: { + case SaturationType::SCALED_QUADRATIC: + case SaturationType::CUTOFF_SCALED_QUADRATIC: { + if ((s10 <= 0.0) || (s12 <= 0.0)) { + A = 0.0; + B = 0.0; + break; + } double ssv = sqrt((s10 * 1.0) / (s12 * 1.2)); - A = -(1.2 * ssv - 1.0) / (1.0 - ssv); - B = s10 / ((1.0 - A) * (1.0 - A)); + const double fitDenominator = 1.0 - ssv; + if (std::abs(fitDenominator) < 1e-12) { + A = 0.0; + B = 0.0; + break; + } + A = -(1.2 * ssv - 1.0) / fitDenominator; + const double distance = 1.0 - A; + B = (std::abs(distance) < 1e-12) ? 0.0 : s10 / (distance * distance); break; } case SaturationType::EXPONENTIAL: - A = -log(s10 / s12) / log(1.2); - B = s10; + if ((s10 <= 0.0) || (s12 <= 0.0)) { + A = 0.0; + B = 0.0; + } else { + A = -log(s10 / s12) / log(1.2); + B = s10; + } break; case SaturationType::LINEAR: B = (s12 - s10) / 0.2; - A = 1.0 - s10 / B; + A = (std::abs(B) < 1e-12) ? 0.0 : 1.0 - s10 / B; break; case SaturationType::NONE: default: @@ -167,15 +253,36 @@ void Saturation::loadFunctions() derivFunc = [this](double val) { return (2 * B * (val - A)); }; break; case SaturationType::SCALED_QUADRATIC: - satFunc = [this](double val) { return (B * (val - A) * (val - A) / val); }; + satFunc = [this](double val) { + return ((B == 0.0) || (val == 0.0)) ? 0.0 : (B * (val - A) * (val - A) / val); + }; derivFunc = [this](double val) { + if ((B == 0.0) || (val == 0.0)) { + return 0.0; + } double v1 = (val - A); return (B * (2 * val * v1 - v1 * v1) / (val * val)); }; break; + case SaturationType::CUTOFF_SCALED_QUADRATIC: + satFunc = [this](double val) { + if ((B == 0.0) || (val <= 0.0) || (val < A)) { + return 0.0; + } + const double distance = val - A; + return B * distance * distance / val; + }; + derivFunc = [this](double val) { + if ((B == 0.0) || (val <= 0.0) || (val < A)) { + return 0.0; + } + const double distance = val - A; + return B * distance * (val + A) / (val * val); + }; + break; case SaturationType::EXPONENTIAL: - satFunc = [this](double val) { return (B * pow(val, A)); }; - derivFunc = [this](double val) { return (A * B * pow(val, A - 1)); }; + satFunc = [this](double val) { return (B == 0.0) ? 0.0 : (B * pow(val, A)); }; + derivFunc = [this](double val) { return (B == 0.0) ? 0.0 : (A * B * pow(val, A - 1)); }; break; case SaturationType::LINEAR: satFunc = [this](double val) { return (val <= A) ? 0 : (B * (val - A)); }; diff --git a/src/utilities/Saturation.h b/src/utilities/Saturation.h index ada388368..7abf2990e 100644 --- a/src/utilities/Saturation.h +++ b/src/utilities/Saturation.h @@ -9,15 +9,38 @@ #include #include namespace utilities { -/** @brief class implementing a saturation model - *@details 4 mathematical models are available including: quadratic, scaled_quadratic, exponential, - *and linear +/** @brief Class implementing several saturation characteristics. + * + * @details Saturation points supplied with setParam(double, double) are the + * values at inputs 1.0 and 1.2. The cutoff-scaled-quadratic characteristic is + * the form used by PSS/E synchronous-machine models such as GENROU: + * \f[ + * S(x)=\begin{cases} + * 0,&x +#include +#include +#include +#include +#include +#include + +namespace { +constexpr std::string_view andesTestDirectory{GRIDDYN_TEST_DIRECTORY "/andes_tests/"}; + +std::string makeAndesTestPath(std::string_view fileName) +{ + return std::string{andesTestDirectory} + std::string{fileName}; +} +} // namespace + +TEST(AndesDyrReaderTests, LoadsGenrouAndMatchesIeee14Initialization) +{ + std::ifstream input(makeAndesTestPath("andes_ieee14_genrou_reference.json")); + ASSERT_TRUE(input.is_open()); + nlohmann::json reference; + input >> reference; + + auto simulation = std::make_unique(); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw")); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_genrou.dyr")); + + const auto tolerance = reference["tolerance"].get(); + for (std::size_t index = 0; index < reference["generator_bus_ids"].size(); ++index) { + const auto busId = reference["generator_bus_ids"][index].get(); + auto* bus = dynamic_cast(simulation->findByUserID("bus", busId)); + ASSERT_NE(bus, nullptr) << "bus " << busId; + auto* generator = bus->getGen(0); + ASSERT_NE(generator, nullptr) << "generator at bus " << busId; + auto* model = + dynamic_cast(generator->find("genmodel")); + ASSERT_NE(model, nullptr) << "GENROU at bus " << busId; + + model->dynInitializeA(0.0, 0); + griddyn::IOdata inputs(4, 0.0); + inputs[griddyn::VOLTAGE_IN_LOCATION] = reference["terminal_voltage"][index].get(); + inputs[griddyn::ANGLE_IN_LOCATION] = reference["terminal_angle"][index].get(); + griddyn::IOdata desiredOutput(2, 0.0); + desiredOutput[griddyn::POUT_LOCATION] = + reference["terminal_real_power"][index].get(); + desiredOutput[griddyn::QOUT_LOCATION] = + reference["terminal_reactive_power"][index].get(); + griddyn::IOdata fieldSet(4, 0.0); + model->dynInitializeB(inputs, desiredOutput, fieldSet); + + const auto& states = model->getStates(); + const auto& expectedStates = reference["genrou_state"][index]; + ASSERT_EQ(states.size(), expectedStates.size()) << "GENROU at bus " << busId; + for (std::size_t stateIndex = 0; stateIndex < states.size(); ++stateIndex) { + EXPECT_NEAR(states[stateIndex], expectedStates[stateIndex].get(), tolerance) + << "GENROU at bus " << busId << ", state " + << reference["grid_dyn_state_order"][stateIndex].get(); + } + EXPECT_NEAR(fieldSet[griddyn::genModelEftInLocation], + reference["field_voltage"][index].get(), + tolerance) + << "GENROU field voltage at bus " << busId; + EXPECT_NEAR(fieldSet[griddyn::genModelPmechInLocation], + reference["mechanical_power"][index].get(), + tolerance) + << "GENROU mechanical power at bus " << busId; + } +} diff --git a/test/componentTests/testGenModels.cpp b/test/componentTests/testGenModels.cpp index ee712296c..420cc7832 100644 --- a/test/componentTests/testGenModels.cpp +++ b/test/componentTests/testGenModels.cpp @@ -5,11 +5,14 @@ */ #include "../gtestHelper.h" +#include "core/CoreExceptions.h" #include "core/ObjectFactory.hpp" #include "gmlc/utilities/vectorOps.hpp" #include "griddyn/Generator.h" +#include "griddyn/genmodels/GenModelGENROU.h" #include #include +#include #include #include // test case for CoreObject object @@ -18,8 +21,176 @@ using namespace griddyn; +namespace { +void configureKundurGenrou(genmodels::GenModelGENROU& model, double s10, double s12) +{ + // Machine-base parameters for the first generator in the ANDES + // kundur/kundur_full.json reference case. + model.set("h", 6.5); + model.set("d", 0.0); + model.set("r", 0.0); + model.set("xl", 0.06); + model.set("xd", 1.8); + model.set("xq", 1.7); + model.set("xdp", 0.3); + model.set("xqp", 0.55); + model.set("xdpp", 0.25); + model.set("xqpp", 0.25); + model.set("tdop", 8.0); + model.set("tdopp", 0.03); + model.set("tqop", 0.4); + model.set("tqopp", 0.05); + model.set("s10", s10); + model.set("s12", s12); +} + +void checkGenrouInitialization(const std::vector& expectedState, + double expectedFieldVoltage, + double s10, + double s12) +{ + genmodels::GenModelGENROU model; + configureKundurGenrou(model, s10, s12); + model.dynInitializeA(0.0, 0); + + IOdata inputs(4, 0.0); + inputs[VOLTAGE_IN_LOCATION] = 1.0000000000182052; + inputs[ANGLE_IN_LOCATION] = 0.5702549171702084; + + // ANDES solves this case on the 100 MVA system base while the machine + // is rated 900 MVA. GridDyn generator models operate on the machine + // base, so the terminal P/Q references are divided by nine. + IOdata desiredOutput(2, 0.0); + desiredOutput[POUT_LOCATION] = 7.268029078708957 / 9.0; + desiredOutput[QOUT_LOCATION] = 1.0946333735232774 / 9.0; + IOdata fieldSet(4, 0.0); + model.dynInitializeB(inputs, desiredOutput, fieldSet); + + const auto& state = model.getStates(); + ASSERT_EQ(state.size(), expectedState.size()); + for (std::size_t index = 0; index < state.size(); ++index) { + EXPECT_NEAR(state[index], expectedState[index], 2e-8) << "state index " << index; + } + EXPECT_NEAR(fieldSet[genModelEftInLocation], expectedFieldVoltage, 2e-8); + EXPECT_NEAR(fieldSet[genModelPmechInLocation], 7.268029078708958 / 9.0, 2e-8); +} +} // namespace + class GenModelTests: public GridDynSimulationTestFixture, public ::testing::Test {}; +TEST_F(GenModelTests, GenrouFactoryRegistration) +{ + auto cof = CoreObjectFactory::instance(); + std::unique_ptr object(cof->createObject("genmodel", "genrou")); + ASSERT_NE(object, nullptr); + EXPECT_NE(dynamic_cast(object.get()), nullptr); +} + +TEST_F(GenModelTests, GenrouRejectsInvalidParameters) +{ + genmodels::GenModelGENROU model; + configureKundurGenrou(model, 0.0, 1.0); + model.set("xdp", 0.2); + model.set("xdpp", 0.25); + + EXPECT_THROW(model.dynInitializeA(0.0, 0), InvalidParameterValue); +} + +TEST_F(GenModelTests, GenrouAndesInitialization) +{ + // Captured from ANDES kundur_full initialization. ANDES Id, e1d, and e2q + // are sign-adjusted to GridDyn's established dq convention; currents are + // converted from the 100 MVA system base to the 900 MVA machine base. + checkGenrouInitialization({-6.181548626591364 / 9.0, + 3.9762954475880385 / 9.0, + 1.419948332280109, + 1.0, + -0.5080821960806938, + 0.8662650669828909, + -0.7245693926715981, + 0.7014237702737878}, + 1.8965231714147848, + 0.0, + 1.0); +} + +TEST_F(GenModelTests, GenrouAndesSaturatedInitialization) +{ + // Same ANDES operating point with S10=0.1 and S12=0.3. This exercises the + // ExcQuadSat-compatible initialization and saturation-dependent states. + checkGenrouInitialization({-5.998928215369296 / 9.0, + 4.246802229474443 / 9.0, + 1.375539180362926, + 1.0, + -0.46150083698625766, + 0.8928705752592525, + -0.6927156250354218, + 0.7328991561827378}, + 2.015401860073227, + 0.1, + 0.3); +} + +TEST_F(GenModelTests, GenrouAndesPerturbedDerivatives) +{ + std::string fileName = std::string(GENMODEL_TEST_DIRECTORY "test_model1.xml"); + gds = readSimXMLFile(fileName); + Generator* gen = gds->getGen(0); + ASSERT_NE(gen, nullptr); + auto* model = new genmodels::GenModelGENROU(); + configureKundurGenrou(*model, 0.1, 0.3); + gen->add(model); + ASSERT_EQ(gds->dynInitialize(), 0); + + // Values below are the GridDyn-sign-convention equivalent of a direct + // evaluation of the ANDES GENROU equations away from equilibrium. + std::vector state = gds->getState(cDaeSolverMode); + const auto& modelOffsets = model->getOffsets(cDaeSolverMode); + state[modelOffsets.algOffset] = -0.65; + state[modelOffsets.algOffset + 1] = 0.5; + state[modelOffsets.diffOffset] = 1.4; + state[modelOffsets.diffOffset + 1] = 1.002; + state[modelOffsets.diffOffset + 2] = -0.45; + state[modelOffsets.diffOffset + 3] = 0.9; + state[modelOffsets.diffOffset + 4] = -0.70; + state[modelOffsets.diffOffset + 5] = 0.75; + IOdata inputs{1.01, 0.57, 2.0, 0.82}; + std::vector derivative(state.size(), 0.0); + StateData stateData(0.0, state.data(), nullptr, 12345); + std::vector algebraicUpdate(state); + model->algebraicUpdate(inputs, stateData, algebraicUpdate.data(), cDaeSolverMode, 1.0); + model->derivative(inputs, stateData, derivative.data(), cDaeSolverMode); + + const std::vector expected{0.75398223686155097, + -0.00040493876689850193, + -0.11774384280401951, + 0.00034563024648560925, + 0.099999999999998423, + -0.19999999999999926}; + for (std::size_t index = 0; index < expected.size(); ++index) { + EXPECT_NEAR(derivative[modelOffsets.diffOffset + index], expected[index], 2e-12) + << "differential state index " << index; + } +} + +TEST_F(GenModelTests, GenrouSaturatedEquationChecks) +{ + std::string fileName = std::string(GENMODEL_TEST_DIRECTORY "test_model1.xml"); + gds = readSimXMLFile(fileName); + + Generator* gen = gds->getGen(0); + ASSERT_NE(gen, nullptr); + auto* model = new genmodels::GenModelGENROU(); + configureKundurGenrou(*model, 0.1, 0.3); + gen->add(model); + + ASSERT_EQ(gds->dynInitialize(), 0); + EXPECT_EQ(runResidualCheck(gds, cDaeSolverMode, false), 0); + EXPECT_EQ(runDerivativeCheck(gds, cDaeSolverMode, false), 0); + EXPECT_EQ(runAlgebraicCheck(gds, cDaeSolverMode, false), 0); + EXPECT_EQ(runJacobianCheck(gds, cDaeSolverMode, false), 0); +} + TEST_F(GenModelTests, ModelTest1) { std::string fileName = std::string(GENMODEL_TEST_DIRECTORY "test_model1.xml"); diff --git a/test/libraryTests/testSaturation.cpp b/test/libraryTests/testSaturation.cpp new file mode 100644 index 000000000..43634256d --- /dev/null +++ b/test/libraryTests/testSaturation.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2014-2026, Lawrence Livermore National Security + * See the top-level NOTICE for additional details. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "utilities/Saturation.h" +#include +#include +#include + +using utilities::Saturation; + +TEST(SaturationTests, CutoffScaledQuadraticReferencePoints) +{ + Saturation saturation(Saturation::SaturationType::CUTOFF_SCALED_QUADRATIC); + saturation.setParam(0.1, 0.3); + + const double ratio = std::sqrt(0.1 / (1.2 * 0.3)); + const double saturationStart = (1.0 - 1.2 * ratio) / (1.0 - ratio); + + EXPECT_DOUBLE_EQ(saturation.compute(0.5 * saturationStart), 0.0); + EXPECT_DOUBLE_EQ(saturation.deriv(0.5 * saturationStart), 0.0); + EXPECT_NEAR(saturation.compute(1.0), 0.1, 1e-14); + EXPECT_NEAR(saturation.compute(1.2), 0.3, 1e-14); + + const auto evaluation = saturation.evaluate(1.1); + const double step = 1e-6; + const double finiteDifference = + (saturation.compute(1.1 + step) - saturation.compute(1.1 - step)) / (2.0 * step); + EXPECT_NEAR(evaluation.value, saturation.compute(1.1), 1e-15); + EXPECT_NEAR(evaluation.derivative, finiteDifference, 1e-9); +} + +TEST(SaturationTests, DisabledCharacteristicIsFinite) +{ + Saturation saturation(Saturation::SaturationType::CUTOFF_SCALED_QUADRATIC); + + saturation.setParam(0.0, 1.0); + EXPECT_DOUBLE_EQ(saturation.compute(0.0), 0.0); + EXPECT_DOUBLE_EQ(saturation.compute(1.0), 0.0); + EXPECT_DOUBLE_EQ(saturation.deriv(1.0), 0.0); + EXPECT_TRUE(std::isfinite(saturation.compute(1.0))); + + saturation.setParam(0.1, 0.0); + EXPECT_DOUBLE_EQ(saturation.compute(1.2), 0.0); + EXPECT_DOUBLE_EQ(saturation.deriv(1.2), 0.0); +} + +TEST(SaturationTests, CopiesOwnTheirEvaluationFunctions) +{ + Saturation original(Saturation::SaturationType::CUTOFF_SCALED_QUADRATIC); + original.setParam(0.1, 0.3); + Saturation copied(original); + Saturation assigned; + assigned = original; + Saturation moved(std::move(copied)); + + original.setParam(0.2, 0.5); + EXPECT_NEAR(moved.compute(1.0), 0.1, 1e-14); + EXPECT_NEAR(assigned.compute(1.2), 0.3, 1e-14); + EXPECT_NE(moved.compute(1.0), original.compute(1.0)); +} + +TEST(SaturationTests, StringTypeAndInverse) +{ + Saturation saturation("cutoff_scaled_quadratic"); + saturation.setParam(0.1, 0.3); + + EXPECT_EQ(saturation.getType(), Saturation::SaturationType::CUTOFF_SCALED_QUADRATIC); + EXPECT_NEAR(saturation.inv(saturation.compute(1.1)), 1.1, 1e-12); +} diff --git a/test/test_files/andes_tests/andes_ieee14_genrou_reference.json b/test/test_files/andes_tests/andes_ieee14_genrou_reference.json new file mode 100644 index 000000000..5ac877c22 --- /dev/null +++ b/test/test_files/andes_tests/andes_ieee14_genrou_reference.json @@ -0,0 +1,28 @@ +{ + "description": "ANDES initialized IEEE 14-bus GENROU reference, translated to GridDyn's dq sign convention", + "source": { + "project": "ANDES", + "version": "v2.0.0", + "commit": "eda5163c9ee8d19945a1dd5d1771fec5da608c27", + "case": "andes/cases/ieee14/ieee14.raw + ieee14.dyr", + "raw_sha256": "539c35fd57f72079206d5b92b3eb5be2e0fbdbd6dc9037a2e0b99f9c6a774eb3", + "dyr_sha256": "1f97507a01df1c73c897092fb928a460d4dc5e5ba2263c2bac2cd63b8e97a888", + "procedure": "andes.load(raw, addfile=dyr); PFlow.run(); TDS.init()" + }, + "tolerance": 0.000001, + "generator_bus_ids": [1, 2, 3, 6, 8], + "terminal_voltage": [1.0300000000000118, 1.0300000000000578, 1.0100000000000189, 1.030000000000161, 1.0300000000000211], + "terminal_angle": [7.95343064549627e-14, -0.03078883541039719, -0.061734559533319605, -0.11262149580254596, -0.02687732768734367], + "terminal_real_power": [0.8142721424412257, 0.4000000000000001, 0.4000000000000001, 0.29999999999999993, 0.35], + "terminal_reactive_power": [-0.21617103400156973, 0.30436146758657606, 0.12597133273865904, 0.20986596444814581, 0.07396391539528427], + "grid_dyn_state_order": ["id", "iq", "delta", "omega", "e1d", "e1q", "e2q", "e2d"], + "genrou_state": [ + [-0.5985710190356957, 0.5574388995416362, 1.080456234134706, 1.0, -0.46268681407825607, 0.8441963253752449, -0.8250220987803196, 0.5748393668091818], + [-0.41634759402631394, 0.25453536015812445, 0.34083896184241125, 1.0, -0.1703982817326246, 1.2094981546919898, -0.33584626583540556, 1.0221417373801485], + [-0.29163508520999143, 0.2955544688548951, 0.41189326010620136, 1.0, -0.22423534938424006, 1.073799680522883, -0.4163457541399219, 0.9425638921783871], + [-0.28451173786375605, 0.21307776982683332, 0.20493644475406253, 1.0, -0.1511527458279928, 1.1492078431358204, -0.2896532962154345, 1.0211775610971303], + [-0.20814362481408286, 0.2780303091214823, 0.4074891972307295, 1.0, -0.21103666509782137, 1.0592370925985632, -0.39175636602678493, 0.9655724614322261] + ], + "field_voltage": [1.6175148024842534, 1.9709041251739947, 1.5853158212743705, 1.7042047062484473, 1.4713776946374997], + "mechanical_power": [0.8142721424412257, 0.4000000000000001, 0.4000000000000001, 0.29999999999999993, 0.35] +} diff --git a/test/test_files/andes_tests/ieee14.raw b/test/test_files/andes_tests/ieee14.raw new file mode 100644 index 000000000..fae1fdf69 --- /dev/null +++ b/test/test_files/andes_tests/ieee14.raw @@ -0,0 +1,93 @@ +0, 100.00, 32, 0, 1, 60.00 / PSS(R)E 32 RAW created by rawd32 SUN, MAY 17 2020 20:38 + IEEE 14 BUS TEST CASE + 03/06/14 CONTO 100.0 1962 W + 1,'BUS1 ', 69.0000,3, 1, 1, 1,1.03000, 0.0000 + 2,'BUS2 ', 69.0000,2, 1, 1, 1,1.01970, -1.6032 + 3,'BUS3 ', 69.0000,2, 1, 1, 1,1.00042, -3.4433 + 4,'BUS4 ', 69.0000,1, 1, 1, 1,0.99858, -4.2812 + 5,'BUS5 ', 69.0000,1, 1, 1, 1,1.00443, -3.6850 + 6,'BUS6 ', 138.0000,2, 2, 2, 2,0.99871, -6.3024 + 7,'BUS7 ', 138.0000,1, 2, 2, 2,1.00682, -4.8292 + 8,'BUS8 ', 69.0000,2, 2, 2, 2,1.01895, -1.3945 + 9,'BUS9 ', 138.0000,1, 2, 2, 2,1.00193, -7.3053 + 10,'BUS10 ', 138.0000,1, 2, 2, 2,0.99351, -7.4600 + 11,'BUS11 ', 138.0000,1, 2, 2, 2,0.99245, -7.0444 + 12,'BUS12 ', 138.0000,1, 2, 2, 2,0.98639, -7.3874 + 13,'BUS13 ', 138.0000,1, 2, 2, 2,0.98403, -7.6654 + 14,'BUS14 ', 138.0000,1, 2, 2, 2,0.99063, -9.5636 + 0 /End of Bus data, Begin Load data + 2,'1 ',1, 1, 1, 21.700, 12.700, 0.000, 0.000, 0.000, 0.000, 1,1 + 3,'1 ',1, 1, 1, 50.000, 25.000, 0.000, 0.000, 0.000, 0.000, 1,1 + 4,'1 ',1, 1, 1, 47.800, 10.000, 0.000, 0.000, 0.000, 0.000, 1,1 + 5,'1 ',1, 1, 1, 7.600, 1.600, 0.000, 0.000, 0.000, 0.000, 1,1 + 6,'1 ',1, 2, 2, 15.000, 7.500, 0.000, 0.000, 0.000, 0.000, 2,1 + 9,'1 ',1, 2, 2, 29.500, 16.600, 0.000, 0.000, 0.000, 0.000, 2,1 + 10,'1 ',1, 2, 2, 9.000, 5.800, 0.000, 0.000, 0.000, 0.000, 2,1 + 11,'1 ',1, 2, 2, 3.500, 1.800, 0.000, 0.000, 0.000, 0.000, 2,1 + 12,'1 ',1, 2, 2, 6.100, 1.600, 0.000, 0.000, 0.000, 0.000, 2,1 + 13,'1 ',1, 2, 2, 13.500, 5.800, 0.000, 0.000, 0.000, 0.000, 2,1 + 14,'1 ',1, 2, 2, 20.000, 7.000, 0.000, 0.000, 0.000, 0.000, 2,1 + 0 /End of Load data, Begin Fixed shunt data + 0 /End of Fixed shunt data, Begin Generator data + 1,'1 ', 81.442, 1.962, 100.000, -50.000,1.03000, 0, 100.000, 0.00000E+0, 2.30000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 200.000, 50.000, 1,1.0000 + 2,'1 ', 40.000, 15.000, 15.000, -40.000,1.03000, 0, 100.000, 0.00000E+0, 1.30000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 50.000, 10.000, 1,1.0000 + 3,'1 ', 40.000, 15.000, 15.000, -10.000,1.01000, 0, 100.000, 0.00000E+0, 1.30000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 50.000, 10.000, 1,1.0000 + 6,'1 ', 30.000, 10.000, 10.000, -6.000,1.03000, 0, 100.000, 0.00000E+0, 1.20000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 50.000, 10.000, 1,1.0000 + 8,'1 ', 35.000, 10.000, 10.000, -6.000,1.03000, 0, 100.000, 0.00000E+0, 1.20000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 50.000, 10.000, 1,1.0000 + 0 /End of Generator data, Begin Branch data + 1, 2,'1 ', 1.93800E-2, 5.91700E-2, 0.05280, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 1, 5,'1 ', 5.40300E-2, 2.23040E-1, 0.04920, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 2, 3,'1 ', 4.69900E-2, 1.97970E-1, 0.04380, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 2, 4,'1 ', 5.81100E-2, 1.76320E-1, 0.03400, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 2, 5,'1 ', 5.69500E-2, 1.73880E-1, 0.03460, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 3, 4,'1 ', 6.70100E-2, 1.71030E-1, 0.01280, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 4, 5,'1 ', 1.33500E-2, 4.21100E-2, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 6, 11,'1 ', 9.49800E-2, 1.98900E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 6, 12,'1 ', 1.22910E-1, 2.55810E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 6, 13,'1 ', 6.61500E-2, 1.30270E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 7, 9,'1 ',-0.00000E+0, 1.10010E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 9, 10,'1 ', 3.18100E-2, 8.45000E-2, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 9, 14,'1 ', 1.27110E-1, 2.70380E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 10, 11,'1 ', 8.20500E-2, 1.92070E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 12, 13,'1 ', 2.20920E-1, 1.99880E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 13, 14,'1 ', 1.70930E-1, 3.48020E-1, 0.00000, 100.00, 100.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.00, 1,1.0000 + 0 /End of Branch data, Begin Transformer data + 4, 7, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,2,' ',1, 1,1.0000 + 0.00000E+0, 2.09120E-1, 100.00 +0.99677, 0.000, 0.000, 20.00, 20.00, 0.00,-1, 0, 1.10000, 0.90000, 1.10000, 0.90000, 32, 0, 0.00000, 0.00000, 0.000 +1.00000, 0.000 + 4, 9, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,2,' ',1, 1,1.0000 + 0.00000E+0, 5.56180E-1, 100.00 +0.99677, 0.000, 0.000, 20.00, 20.00, 0.00,-1, 0, 1.10000, 0.90000, 1.10000, 0.90000, 32, 0, 0.00000, 0.00000, 0.000 +1.00000, 0.000 + 6, 5, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,1,' ',1, 1,1.0000 + 0.00000E+0, 2.52020E-1, 100.00 +0.99677, 0.000, 0.000, 50.00, 50.00, 0.00,-1, 0, 1.10000, 0.90000, 1.10000, 0.90000, 32, 0, 0.00000, 0.00000, 0.000 +1.00000, 0.000 + 8, 7, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,1,' ',1, 1,1.0000 + 0.00000E+0, 1.76150E-1, 100.00 +0.99677, 0.000, 0.000, 50.00, 50.00, 0.00,-1, 0, 1.10000, 0.90000, 1.10000, 0.90000, 32, 0, 0.00000, 0.00000, 0.000 +1.00000, 0.000 + 0 /End of Transformer data, Begin Area interchange data + 1, 2, 0.000, 999.990,'AREA1 ' + 2, 0, 0.000, 10.000,'AREA2 ' + 0 /End of Area interchange data, Begin Two-terminal dc line data + 0 /End of Two-terminal dc line data, Begin VSC dc line data + 0 /End of VSC dc line data, Begin Impedance correction table data + 0 /End of Impedance correction table data, Begin Multi-terminal dc line data + 0 /End of Multi-terminal dc line data, Begin Multi-section line data + 0 /End of Multi-section line data, Begin Zone data + 1,'ZONE1 ' + 2,'ZONE2 ' + 0 /End of Zone data, Begin Inter-area transfer data + 0 /End of Inter-area transfer data, Begin Owner data + 1,'OWNER1 ' + 2,'OWNER2 ' + 0 /End of Owner data, Begin FACTS device data + 0 /End of FACTS device data, Begin Switched shunt data + 9,1,0,1,1.02500,0.96000, 0, 100.0,' ', 19.00, 3, 5.00, 1, 4.00 + 14,1,0,1,1.02500,0.96000, 0, 100.0,' ', 15.00, 3, 5.00 + 0 /End of Switched shunt data, Begin GNE device data + 0 /End of GNE device data +Q + diff --git a/test/test_files/andes_tests/ieee14_genrou.dyr b/test/test_files/andes_tests/ieee14_genrou.dyr new file mode 100644 index 000000000..dd33c880f --- /dev/null +++ b/test/test_files/andes_tests/ieee14_genrou.dyr @@ -0,0 +1,15 @@ + 1 'GENROU' 1 6.5000 0.60000E-01 0.20000 0.50000E-01 + 4.0000 0.0000 1.8000 1.7500 0.60000 + 0.80000 0.23000 0.15000 0.90000E-01 0.38000 / + 2 'GENROU' 1 6.5000 0.60000E-01 0.20000 0.50000E-01 + 6.5000 0.0000 1.8000 1.7500 0.60000 + 0.80000 0.28000 0.15000 0.90000E-01 0.38000 / + 3 'GENROU' 1 6.5000 0.60000E-01 0.20000 0.50000E-01 + 5.0000 0.0000 1.8000 1.7500 0.60000 + 0.80000 0.34000 0.15000 0.90000E-01 0.38000 / + 6 'GENROU' 1 6.5000 0.60000E-01 0.20000 0.50000E-01 + 5.0000 0.0000 1.8000 1.7500 0.60000 + 0.80000 0.28000 0.15000 0.90000E-01 0.38000 / + 8 'GENROU' 1 6.5000 0.60000E-01 0.20000 0.50000E-01 + 5.0000 0.0000 1.8000 1.7500 0.60000 + 0.80000 0.34000 0.15000 0.90000E-01 0.38000 / From 5f721409348f6adaf5a5619fe926248b487a4e82 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:21:27 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/developer-guide/andes-compatibility.md | 182 +++++++++--------- src/griddyn/genmodels/GenModelGENROU.h | 4 +- test/CMakeLists.txt | 5 +- .../andes_ieee14_genrou_reference.json | 71 +++++-- test/test_files/andes_tests/ieee14.raw | 1 - 5 files changed, 153 insertions(+), 110 deletions(-) diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index b72a80277..addd5ced4 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -64,27 +64,27 @@ GridDyn's current DYR reader recognizes only `GENROU`, `ESDC1A`, `EXDC2`, `TGOV1`, and `SEXS`. Recognition alone is not compatibility: the existing adapters still have attachment, parameter, equation, and validation gaps. -| PSS/e DYR record(s) | ANDES destination | Existing GridDyn candidate | Current compatibility and required action | -| ------------------- | ----------------- | -------------------------- | ----------------------------------------- | -| `GENCLS` | `GENCLS` | `GenModelClassical` | **Partial.** Add a DYR adapter, verify machine/system-base conversion, initialization, and rotor trajectory. | -| `GENSAL` | `GENROU` | No verified exact equivalent | **Planned.** ANDES explicitly marks this as an approximation and fills GENROU q-axis values from d-axis data. Decide whether compatibility mode should reproduce that conversion before considering a distinct salient-pole model. | -| `GENROU` | `GENROU` | Registered `GenModelGENROU`; DYR uses the dedicated model | **Partial.** The adapter and dedicated model match all eight initialized machine states, field voltage, and mechanical power for five IEEE 14-bus machines, including quadratic saturation. Exact alphanumeric machine-ID lookup, arbitrary machine-base cases, native ANDES import, and disturbed-trajectory validation remain. | -| `IEEEVC` | `IEEEVC` | None | **No direct analogue.** Add a voltage-compensator input to the exciter path, including `rc`/`xc` terminal-current compensation. | -| `SEXS` | `SEXS` | `ExciterSEXS` | **Partial.** The adapter exists; verify lead-lag convention, limits, initialization, and trajectories. | -| `ESDC1A` | `ESDC1A` | `ExciterDC1A` / `ExciterIEEEtype1` | **Partial.** The current adapter ignores `TR`, switch behavior, and `E1`/`SE1`/`E2`/`SE2`; its fallback to type 1 when `TB` is zero must be compared with ANDES. | -| `EXDC2` | `EXDC2` | `ExciterDC2A` | **Partial.** The adapter exists but ignores `TR`, switch behavior, and saturation points. Confirm whether `DC2A` equations exactly represent ANDES `EXDC2`. | -| `IEEET1`, `IEEET3`, `ESDC2A` | Same named ANDES models | `ExciterIEEEtype1`, `ExciterIEEEtype2`, and `ExciterDC2A` are only candidates | **Planned.** Perform equation and limiter audits before selecting an analogue; similar names are insufficient. | -| `ESST1A`, `ESAC1A`, `AC8B`, `EXAC1`, `EXAC2`, `EXAC4`, `IEEEX1`, `EXST1`, `ESST3A`, `ESST4B` | Same named ANDES models | None exact | **No direct analogue.** Implement model-specific exciter blocks and DYR schemas, then add initialization and trajectory tests. | -| `ESAC6A`, `SCRX`, `EXPIC1` | `SEXS` | `ExciterSEXS` | **Planned compatibility approximations.** ANDES marks these conversions as TODO/approximate and currently discards most source parameters. Reproduce this only as an explicit compatibility mode and emit a diagnostic; do not describe it as exact model support. | -| `TGOV1` | `TGOV1` | `GovernorTgov1` | **Partial.** The adapter exists, but currently reads `T2`, `T3`, and `Dt` from the wrong token positions. Correct the schema and then audit limits, initialization, and trajectories. | -| `HYGOV` | `HYGOV` | `GovernorHydro` is a candidate | **Planned.** Compare equations, water-column dynamics, gate/rate limits, and parameter units before mapping it. | -| `IEESGO`, `IEEEG1` | Same named ANDES models | `GovernorReheat` and steam-governor classes are only candidates | **Planned.** No exact equivalence has been established; audit block diagrams before choosing reuse versus new models. | -| `GAST` | `GAST` | None exact | **No direct analogue.** Add a gas-turbine governor implementation and DYR adapter. | -| `GGOV1` | `TGOV1` | `GovernorTgov1` | **Planned compatibility approximation.** ANDES currently retains only `R` when converting this record. If reproduced, emit a diagnostic and keep exact GGOV1 support as a separate task. | -| `IEEEST`, `ST2CUT` | Same named ANDES models | Base `Stabilizer` only | **No direct analogue.** The base class is not a usable PSS implementation; add the two models and their generator/exciter signal connections. | -| `REGCA1`, `REECA1`, `REECB1`, `REPCA1` | Same named ANDES models | `GenModelInverter` is not equivalent | **No direct analogue.** Add the coordinated renewable generator, electrical-control, and plant-control chain rather than flattening these records into the generic inverter. | -| `WTDTA1`, `WTARA1`, `WTPTA1`, `WTTQA1` | Same named ANDES models | None | **No direct analogue.** Add drive-train, aerodynamic, pitch, and torque-control submodels with their shared interfaces. | -| `Toggle`, `Fault` | ANDES event models | GridDyn event/action and fault mechanisms | **Partial conceptually.** Define DYR record schemas and translate target resolution, timing, status changes, and fault clearing semantics; no adapter exists. | +| PSS/e DYR record(s) | ANDES destination | Existing GridDyn candidate | Current compatibility and required action | +| -------------------------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GENCLS` | `GENCLS` | `GenModelClassical` | **Partial.** Add a DYR adapter, verify machine/system-base conversion, initialization, and rotor trajectory. | +| `GENSAL` | `GENROU` | No verified exact equivalent | **Planned.** ANDES explicitly marks this as an approximation and fills GENROU q-axis values from d-axis data. Decide whether compatibility mode should reproduce that conversion before considering a distinct salient-pole model. | +| `GENROU` | `GENROU` | Registered `GenModelGENROU`; DYR uses the dedicated model | **Partial.** The adapter and dedicated model match all eight initialized machine states, field voltage, and mechanical power for five IEEE 14-bus machines, including quadratic saturation. Exact alphanumeric machine-ID lookup, arbitrary machine-base cases, native ANDES import, and disturbed-trajectory validation remain. | +| `IEEEVC` | `IEEEVC` | None | **No direct analogue.** Add a voltage-compensator input to the exciter path, including `rc`/`xc` terminal-current compensation. | +| `SEXS` | `SEXS` | `ExciterSEXS` | **Partial.** The adapter exists; verify lead-lag convention, limits, initialization, and trajectories. | +| `ESDC1A` | `ESDC1A` | `ExciterDC1A` / `ExciterIEEEtype1` | **Partial.** The current adapter ignores `TR`, switch behavior, and `E1`/`SE1`/`E2`/`SE2`; its fallback to type 1 when `TB` is zero must be compared with ANDES. | +| `EXDC2` | `EXDC2` | `ExciterDC2A` | **Partial.** The adapter exists but ignores `TR`, switch behavior, and saturation points. Confirm whether `DC2A` equations exactly represent ANDES `EXDC2`. | +| `IEEET1`, `IEEET3`, `ESDC2A` | Same named ANDES models | `ExciterIEEEtype1`, `ExciterIEEEtype2`, and `ExciterDC2A` are only candidates | **Planned.** Perform equation and limiter audits before selecting an analogue; similar names are insufficient. | +| `ESST1A`, `ESAC1A`, `AC8B`, `EXAC1`, `EXAC2`, `EXAC4`, `IEEEX1`, `EXST1`, `ESST3A`, `ESST4B` | Same named ANDES models | None exact | **No direct analogue.** Implement model-specific exciter blocks and DYR schemas, then add initialization and trajectory tests. | +| `ESAC6A`, `SCRX`, `EXPIC1` | `SEXS` | `ExciterSEXS` | **Planned compatibility approximations.** ANDES marks these conversions as TODO/approximate and currently discards most source parameters. Reproduce this only as an explicit compatibility mode and emit a diagnostic; do not describe it as exact model support. | +| `TGOV1` | `TGOV1` | `GovernorTgov1` | **Partial.** The adapter exists, but currently reads `T2`, `T3`, and `Dt` from the wrong token positions. Correct the schema and then audit limits, initialization, and trajectories. | +| `HYGOV` | `HYGOV` | `GovernorHydro` is a candidate | **Planned.** Compare equations, water-column dynamics, gate/rate limits, and parameter units before mapping it. | +| `IEESGO`, `IEEEG1` | Same named ANDES models | `GovernorReheat` and steam-governor classes are only candidates | **Planned.** No exact equivalence has been established; audit block diagrams before choosing reuse versus new models. | +| `GAST` | `GAST` | None exact | **No direct analogue.** Add a gas-turbine governor implementation and DYR adapter. | +| `GGOV1` | `TGOV1` | `GovernorTgov1` | **Planned compatibility approximation.** ANDES currently retains only `R` when converting this record. If reproduced, emit a diagnostic and keep exact GGOV1 support as a separate task. | +| `IEEEST`, `ST2CUT` | Same named ANDES models | Base `Stabilizer` only | **No direct analogue.** The base class is not a usable PSS implementation; add the two models and their generator/exciter signal connections. | +| `REGCA1`, `REECA1`, `REECB1`, `REPCA1` | Same named ANDES models | `GenModelInverter` is not equivalent | **No direct analogue.** Add the coordinated renewable generator, electrical-control, and plant-control chain rather than flattening these records into the generic inverter. | +| `WTDTA1`, `WTARA1`, `WTPTA1`, `WTTQA1` | Same named ANDES models | None | **No direct analogue.** Add drive-train, aerodynamic, pitch, and torque-control submodels with their shared interfaces. | +| `Toggle`, `Fault` | ANDES event models | GridDyn event/action and fault mechanisms | **Partial conceptually.** Define DYR record schemas and translate target resolution, timing, status changes, and fault clearing semantics; no adapter exists. | ### DYR reader infrastructure plan @@ -157,24 +157,24 @@ The target run is a converged power flow followed by dynamic initialization, an equilibrium interval, opening `Line_1` at 1.0 seconds, reclosing it at 1.1 seconds, and continuing through 2.0 seconds. The five controller chains are: -| Generator bus | Machine | Exciter | Governor | Stabilizer | -| ------------- | ------- | ------- | -------- | ---------- | -| 1 | `GENROU` | `ESST3A` | `TGOV1` | `ST2CUT` | -| 2 | `GENROU` | `EXST1` | `IEEEG1` | `ST2CUT` | -| 3 | `GENROU` | `ESST3A` | `IEEEG1` | `IEEEST` | -| 6 | `GENROU` | `ESST3A` | `TGOV1` | none | -| 8 | `GENROU` | `ESST3A` | `TGOV1` | none | +| Generator bus | Machine | Exciter | Governor | Stabilizer | +| ------------- | -------- | -------- | -------- | ---------- | +| 1 | `GENROU` | `ESST3A` | `TGOV1` | `ST2CUT` | +| 2 | `GENROU` | `EXST1` | `IEEEG1` | `ST2CUT` | +| 3 | `GENROU` | `ESST3A` | `IEEEG1` | `IEEEST` | +| 6 | `GENROU` | `ESST3A` | `TGOV1` | none | +| 8 | `GENROU` | `ESST3A` | `TGOV1` | none | ### PR status and dependency order -| PR | Deliverable | Status | Depends on | -| -- | ----------- | ------ | ---------- | -| 1 | Complete GENROU equations, saturation utility, DYR mapping, and five-machine initialization reference | Current changes; treat as complete after merge | none | -| 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | -| 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Planned | PR 2 | -| 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Planned | PR 3 | -| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | Planned | PR 4 | -| 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | +| PR | Deliverable | Status | Depends on | +| --- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------- | +| 1 | Complete GENROU equations, saturation utility, DYR mapping, and five-machine initialization reference | Current changes; treat as complete after merge | none | +| 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | +| 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Planned | PR 2 | +| 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Planned | PR 3 | +| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | Planned | PR 4 | +| 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | PRs must merge in this order. Each PR must pass without relying on production code from a later PR. Minimized fixtures and test-only probe models are @@ -483,18 +483,18 @@ cmd /v:on /c "set PATH=& set Path=& ""C:\Program Files\CMake\bin\cmake.exe"" --b ### Parameter contract -| PSS/e GENROU value | ANDES value | Intended GridDyn value / note | -| ------------------ | ----------- | ----------------------------- | -| `Td10`, `Td20` | `Td10`, `Td20` | `Tdop`, `Tdopp` | -| `Tq10`, `Tq20` | `Tq10`, `Tq20` | `Tqop`, `Tqopp` | -| `H` | `M = 2 H` | Store `H`; the perturbed derivative reference verifies equivalence to ANDES's `M` convention. | -| `D` | `D` | `D`; the perturbed derivative reference verifies the speed-deviation convention on the machine base. | -| `Xd`, `Xq` | `xd`, `xq` | `Xd`, `Xq` on the machine base. | -| `Xd1`, `Xq1` | `xd1`, `xq1` | `Xdp`, `Xqp` on the machine base. | -| `Xd2` | `xd2`, and `xq2 = xd2` | `Xdpp = Xqpp = Xd2`; this is the PSS/e GENROU single-subtransient-reactance convention. | -| `Xl` | `xl` | `Xl`; use it in the same gamma-factor formulation as ANDES, not as an extra reactance added twice. | -| `S10`, `S12` | `S10`, `S12` | Fit and apply the same quadratic saturation characteristic used by ANDES. | -| Not in DYR record | `ra`, `Sn`, `Vn`, static generator link | Obtain stator resistance, machine MVA base, rated voltage, and machine identity from the paired RAW generator. | +| PSS/e GENROU value | ANDES value | Intended GridDyn value / note | +| ------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `Td10`, `Td20` | `Td10`, `Td20` | `Tdop`, `Tdopp` | +| `Tq10`, `Tq20` | `Tq10`, `Tq20` | `Tqop`, `Tqopp` | +| `H` | `M = 2 H` | Store `H`; the perturbed derivative reference verifies equivalence to ANDES's `M` convention. | +| `D` | `D` | `D`; the perturbed derivative reference verifies the speed-deviation convention on the machine base. | +| `Xd`, `Xq` | `xd`, `xq` | `Xd`, `Xq` on the machine base. | +| `Xd1`, `Xq1` | `xd1`, `xq1` | `Xdp`, `Xqp` on the machine base. | +| `Xd2` | `xd2`, and `xq2 = xd2` | `Xdpp = Xqpp = Xd2`; this is the PSS/e GENROU single-subtransient-reactance convention. | +| `Xl` | `xl` | `Xl`; use it in the same gamma-factor formulation as ANDES, not as an extra reactance added twice. | +| `S10`, `S12` | `S10`, `S12` | Fit and apply the same quadratic saturation characteristic used by ANDES. | +| Not in DYR record | `ra`, `Sn`, `Vn`, static generator link | Obtain stator resistance, machine MVA base, rated voltage, and machine identity from the paired RAW generator. | ### GENROU implementation status @@ -522,24 +522,24 @@ records the lower-level GENROU work already completed or assigned to PR 2. ### Expected GridDyn implementation touchpoints -| Area | Expected files / change | -| ---- | ----------------------- | -| Canonical machine model | **Done:** `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` is complete at the equation/initialization level and `genrou` is registered in `src/griddyn/genmodels/GenModel.cpp`; generic model `6` remains unchanged. | -| DYR parsing and attachment | **Partial:** GENROU creates the dedicated model and applies parameters in the correct RAW/DYR order. Refactor `src/fileInput/gridDynReadDYR.cpp` into schema-backed adapters and add exact bus-plus-machine-ID lookup support. | -| Native ANDES import | Extend the ANDES JSON reader's dynamic-object dispatch to build the same registered machine/controller classes and control connections used by DYR adapters. | -| Model tests | **Mostly done:** focused equation, Jacobian, initialization, saturation, invalid-parameter, and factory tests are present; add a disturbed-trajectory reference and clone regression as the model is integrated. | -| Reader tests | **Partial:** `test/andesTests/testAndesDyrReader.cpp` is a dedicated RAW/DYR GENROU attachment and initialization test. Add parser edge cases and disturbed trajectories as support expands. | -| Numerical references | **Partial:** the IEEE 14-bus RAW input, minimized GENROU DYR input, and captured ANDES initialization reference are stored under `test/test_files/andes_tests/` and run without importing ANDES. Add time-series references next. | +| Area | Expected files / change | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Canonical machine model | **Done:** `src/griddyn/genmodels/GenModelGENROU.{h,cpp}` is complete at the equation/initialization level and `genrou` is registered in `src/griddyn/genmodels/GenModel.cpp`; generic model `6` remains unchanged. | +| DYR parsing and attachment | **Partial:** GENROU creates the dedicated model and applies parameters in the correct RAW/DYR order. Refactor `src/fileInput/gridDynReadDYR.cpp` into schema-backed adapters and add exact bus-plus-machine-ID lookup support. | +| Native ANDES import | Extend the ANDES JSON reader's dynamic-object dispatch to build the same registered machine/controller classes and control connections used by DYR adapters. | +| Model tests | **Mostly done:** focused equation, Jacobian, initialization, saturation, invalid-parameter, and factory tests are present; add a disturbed-trajectory reference and clone regression as the model is integrated. | +| Reader tests | **Partial:** `test/andesTests/testAndesDyrReader.cpp` is a dedicated RAW/DYR GENROU attachment and initialization test. Add parser edge cases and disturbed trajectories as support expands. | +| Numerical references | **Partial:** the IEEE 14-bus RAW input, minimized GENROU DYR input, and captured ANDES initialization reference are stored under `test/test_files/andes_tests/` and run without importing ANDES. Add time-series references next. | ### Planned GENROU reference cases -| Case | Purpose | Required comparisons | -| ---- | ------- | -------------------- | -| Single-machine/infinite-bus `GENROU` | Isolate machine equations and bases. | Initial `delta`, `omega`, d/q currents, transient and subtransient states, field voltage, mechanical/electrical power, terminal P/Q. | -| Same case with nonzero `S10`/`S12` | Exercise saturation independently. | Air-gap flux, saturation output, field-current term, and resulting trajectory. | -| `GENROU` + `TGOV1` | Add mechanical control after machine parity. | Speed, valve/governor states, mechanical power, and limiter activity after a small load step. | -| `GENROU` + `SEXS`, then `ESDC1A`/`EXDC2` | Add excitation controls one at a time. | Terminal voltage, field voltage, exciter states, reactive power, and limiter activity after a voltage-reference step. | -| Multi-machine RAW/DYR case | Verify identity, bases, and network coupling. | Correct model attachment plus selected machine angles, speeds, bus voltages, P/Q, and relative-angle trajectories through a cleared disturbance. | +| Case | Purpose | Required comparisons | +| ---------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Single-machine/infinite-bus `GENROU` | Isolate machine equations and bases. | Initial `delta`, `omega`, d/q currents, transient and subtransient states, field voltage, mechanical/electrical power, terminal P/Q. | +| Same case with nonzero `S10`/`S12` | Exercise saturation independently. | Air-gap flux, saturation output, field-current term, and resulting trajectory. | +| `GENROU` + `TGOV1` | Add mechanical control after machine parity. | Speed, valve/governor states, mechanical power, and limiter activity after a small load step. | +| `GENROU` + `SEXS`, then `ESDC1A`/`EXDC2` | Add excitation controls one at a time. | Terminal voltage, field voltage, exciter states, reactive power, and limiter activity after a voltage-reference step. | +| Multi-machine RAW/DYR case | Verify identity, bases, and network coupling. | Correct model attachment plus selected machine angles, speeds, bus voltages, P/Q, and relative-angle trajectories through a cleared disturbance. | ## Dynamic-model inventory @@ -549,27 +549,27 @@ can be split and checked off as soon as models in the same family diverge. A model is not compatible merely because a similarly named GridDyn model exists: it also needs native-input mapping, initialization, and a trajectory test. -| ANDES model(s) | GridDyn mapping / next action | Status | -| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------- | -| `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial | -| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial | -| `TGOV1` | `GovernorTgov1`; correct the DYR token mapping, then audit equations, limits, initialization, and trajectories. | Partial | -| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial | -| `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial | -| `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial | -| `ACE`, `ACEc`, `COI` | Map area-control and center-of-inertia services. | Untriaged | -| `PLBVFU1`, `IEEEVC` | No direct voltage-compensator/playback analogue; define exciter input and playback interfaces. | No direct analogue | -| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEEEG1`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Existing hydro/reheat/steam classes are candidates only; each needs an equation audit or a new model. | Planned | -| `SHAFT5` | Multi-mass shaft model; map states and mechanical interfaces. | Untriaged | -| `ESDC1A`, `ESDC2A`, `SEXS`, `IEEET1`, `IEEET3` | Existing DC/IEEE/SEXS exciters are candidates; complete model-specific audits and DYR/native mappings. | Partial | -| `IEEEX1`, `EXST1`, `ESST3A`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | -| `IEEEST`, `ST2CUT` | Base `Stabilizer` is not a functional equivalent; implement both models and signal connections. | No direct analogue | -| `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | -| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | -| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | -| `PVD1`, `ESD1`, `EV1`, `EV2`, `DGPRCT1`, `DGPRCTExt` | Distributed energy-resource and protection models. | Untriaged | -| `Fault`, `Alter`, `TimeSeries`, `Toggle` | Event/action semantics and time-series input mapping. | Untriaged | -| `Summary`, `Output` | Reporting configuration; define output-channel mapping after model compatibility. | Untriaged | +| ANDES model(s) | GridDyn mapping / next action | Status | +| --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial | +| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial | +| `TGOV1` | `GovernorTgov1`; correct the DYR token mapping, then audit equations, limits, initialization, and trajectories. | Partial | +| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial | +| `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial | +| `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial | +| `ACE`, `ACEc`, `COI` | Map area-control and center-of-inertia services. | Untriaged | +| `PLBVFU1`, `IEEEVC` | No direct voltage-compensator/playback analogue; define exciter input and playback interfaces. | No direct analogue | +| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEEEG1`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Existing hydro/reheat/steam classes are candidates only; each needs an equation audit or a new model. | Planned | +| `SHAFT5` | Multi-mass shaft model; map states and mechanical interfaces. | Untriaged | +| `ESDC1A`, `ESDC2A`, `SEXS`, `IEEET1`, `IEEET3` | Existing DC/IEEE/SEXS exciters are candidates; complete model-specific audits and DYR/native mappings. | Partial | +| `IEEEX1`, `EXST1`, `ESST3A`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | +| `IEEEST`, `ST2CUT` | Base `Stabilizer` is not a functional equivalent; implement both models and signal connections. | No direct analogue | +| `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | +| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | +| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | +| `PVD1`, `ESD1`, `EV1`, `EV2`, `DGPRCT1`, `DGPRCTExt` | Distributed energy-resource and protection models. | Untriaged | +| `Fault`, `Alter`, `TimeSeries`, `Toggle` | Event/action semantics and time-series input mapping. | Untriaged | +| `Summary`, `Output` | Reporting configuration; define output-channel mapping after model compatibility. | Untriaged | ## Numerical-regression policy @@ -602,11 +602,11 @@ wrong model attachment, or a different post-event equilibrium. ## Current reference cases -| Case | Coverage | -| -------------------------- | ----------------------------------------------------------------------- | -| `andes_kundur_vsc_pflow` | 10-bus AC network, DC resistor, PQ/VQ VSC controls, and AC/DC coupling. | -| `andes_two_bus_pflow` | Minimal AC Slack/PQ/Line power flow. | -| `andes_shunt_pflow` | Fixed conductance/susceptance shunt with non-system `Sn`/`Vn` bases. | -| `andes_jumper_pflow` | Active and inactive zero-impedance jumpers in a loaded AC network. | -| `andes_vsc_resistor_pflow` | Minimal AC/DC `VSCShunt` plus DC resistance power flow. | +| Case | Coverage | +| -------------------------- | ------------------------------------------------------------------------ | +| `andes_kundur_vsc_pflow` | 10-bus AC network, DC resistor, PQ/VQ VSC controls, and AC/DC coupling. | +| `andes_two_bus_pflow` | Minimal AC Slack/PQ/Line power flow. | +| `andes_shunt_pflow` | Fixed conductance/susceptance shunt with non-system `Sn`/`Vn` bases. | +| `andes_jumper_pflow` | Active and inactive zero-impedance jumpers in a loaded AC network. | +| `andes_vsc_resistor_pflow` | Minimal AC/DC `VSCShunt` plus DC resistance power flow. | | `andes_ieee14_genrou` | Five PSS/e DYR GENROU attachments and initialized machine states/inputs. | diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index ed8e02a5f..06beff122 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -137,10 +137,10 @@ namespace griddyn::genmodels { * * @par Equation sources * - ANDES + * href="https://github.com/CURRENT/andes/blob/eda5163c9ee8d19945a1dd5d1771fec5da608c27/andes/models/synchronous/genrou.py">ANDES * v2.0 GENROU equations and initialization * - ANDES + * href="https://github.com/CURRENT/andes/blob/eda5163c9ee8d19945a1dd5d1771fec5da608c27/andes/models/synchronous/genbase.py">ANDES * v2.0 synchronous-generator base equations * - OpenIPSL diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a6f868e67..797321c32 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -45,10 +45,7 @@ set(file_reader_gtest_sources libraryTests/testGridDynRunner.cpp ) -set(andes_compatibility_gtest_sources - gtestHelperFunctions.cpp - andesTests/testAndesDyrReader.cpp -) +set(andes_compatibility_gtest_sources gtestHelperFunctions.cpp andesTests/testAndesDyrReader.cpp) set(network_component_gtest_sources gtestHelperFunctions.cpp componentTests/testLoads.cpp componentTests/testdcLinks.cpp diff --git a/test/test_files/andes_tests/andes_ieee14_genrou_reference.json b/test/test_files/andes_tests/andes_ieee14_genrou_reference.json index 5ac877c22..72de96b06 100644 --- a/test/test_files/andes_tests/andes_ieee14_genrou_reference.json +++ b/test/test_files/andes_tests/andes_ieee14_genrou_reference.json @@ -11,18 +11,65 @@ }, "tolerance": 0.000001, "generator_bus_ids": [1, 2, 3, 6, 8], - "terminal_voltage": [1.0300000000000118, 1.0300000000000578, 1.0100000000000189, 1.030000000000161, 1.0300000000000211], - "terminal_angle": [7.95343064549627e-14, -0.03078883541039719, -0.061734559533319605, -0.11262149580254596, -0.02687732768734367], - "terminal_real_power": [0.8142721424412257, 0.4000000000000001, 0.4000000000000001, 0.29999999999999993, 0.35], - "terminal_reactive_power": [-0.21617103400156973, 0.30436146758657606, 0.12597133273865904, 0.20986596444814581, 0.07396391539528427], - "grid_dyn_state_order": ["id", "iq", "delta", "omega", "e1d", "e1q", "e2q", "e2d"], + "terminal_voltage": [ + 1.0300000000000118, 1.0300000000000578, 1.0100000000000189, + 1.030000000000161, 1.0300000000000211 + ], + "terminal_angle": [ + 7.95343064549627e-14, -0.03078883541039719, -0.061734559533319605, + -0.11262149580254596, -0.02687732768734367 + ], + "terminal_real_power": [ + 0.8142721424412257, 0.4000000000000001, 0.4000000000000001, + 0.29999999999999993, 0.35 + ], + "terminal_reactive_power": [ + -0.21617103400156973, 0.30436146758657606, 0.12597133273865904, + 0.20986596444814581, 0.07396391539528427 + ], + "grid_dyn_state_order": [ + "id", + "iq", + "delta", + "omega", + "e1d", + "e1q", + "e2q", + "e2d" + ], "genrou_state": [ - [-0.5985710190356957, 0.5574388995416362, 1.080456234134706, 1.0, -0.46268681407825607, 0.8441963253752449, -0.8250220987803196, 0.5748393668091818], - [-0.41634759402631394, 0.25453536015812445, 0.34083896184241125, 1.0, -0.1703982817326246, 1.2094981546919898, -0.33584626583540556, 1.0221417373801485], - [-0.29163508520999143, 0.2955544688548951, 0.41189326010620136, 1.0, -0.22423534938424006, 1.073799680522883, -0.4163457541399219, 0.9425638921783871], - [-0.28451173786375605, 0.21307776982683332, 0.20493644475406253, 1.0, -0.1511527458279928, 1.1492078431358204, -0.2896532962154345, 1.0211775610971303], - [-0.20814362481408286, 0.2780303091214823, 0.4074891972307295, 1.0, -0.21103666509782137, 1.0592370925985632, -0.39175636602678493, 0.9655724614322261] + [ + -0.5985710190356957, 0.5574388995416362, 1.080456234134706, 1.0, + -0.46268681407825607, 0.8441963253752449, -0.8250220987803196, + 0.5748393668091818 + ], + [ + -0.41634759402631394, 0.25453536015812445, 0.34083896184241125, 1.0, + -0.1703982817326246, 1.2094981546919898, -0.33584626583540556, + 1.0221417373801485 + ], + [ + -0.29163508520999143, 0.2955544688548951, 0.41189326010620136, 1.0, + -0.22423534938424006, 1.073799680522883, -0.4163457541399219, + 0.9425638921783871 + ], + [ + -0.28451173786375605, 0.21307776982683332, 0.20493644475406253, 1.0, + -0.1511527458279928, 1.1492078431358204, -0.2896532962154345, + 1.0211775610971303 + ], + [ + -0.20814362481408286, 0.2780303091214823, 0.4074891972307295, 1.0, + -0.21103666509782137, 1.0592370925985632, -0.39175636602678493, + 0.9655724614322261 + ] + ], + "field_voltage": [ + 1.6175148024842534, 1.9709041251739947, 1.5853158212743705, + 1.7042047062484473, 1.4713776946374997 ], - "field_voltage": [1.6175148024842534, 1.9709041251739947, 1.5853158212743705, 1.7042047062484473, 1.4713776946374997], - "mechanical_power": [0.8142721424412257, 0.4000000000000001, 0.4000000000000001, 0.29999999999999993, 0.35] + "mechanical_power": [ + 0.8142721424412257, 0.4000000000000001, 0.4000000000000001, + 0.29999999999999993, 0.35 + ] } diff --git a/test/test_files/andes_tests/ieee14.raw b/test/test_files/andes_tests/ieee14.raw index fae1fdf69..8f2850af2 100644 --- a/test/test_files/andes_tests/ieee14.raw +++ b/test/test_files/andes_tests/ieee14.raw @@ -90,4 +90,3 @@ 0 /End of Switched shunt data, Begin GNE device data 0 /End of GNE device data Q - From 88c545aea9bf846d0f078120e9f56009ea47c273 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 20 Aug 2026 18:50:04 -0700 Subject: [PATCH 03/12] clang-tidy fixes and test case resolution --- .github/actions/run-clang-tidy-pr.sh | 2 +- .github/workflows/compiler-tests.yml | 6 +- docs/developer-guide/andes-compatibility.md | 7 +- src/griddyn/genmodels/GenModel.cpp | 28 +- src/griddyn/genmodels/GenModel4.cpp | 1 - src/griddyn/genmodels/GenModelGENROU.cpp | 331 ++++++++++++-------- src/griddyn/genmodels/GenModelGENROU.h | 6 + src/utilities/Saturation.cpp | 63 ++-- src/utilities/Saturation.h | 25 +- test/componentTests/testGenModels.cpp | 26 ++ test/libraryTests/testSaturation.cpp | 2 +- 11 files changed, 310 insertions(+), 187 deletions(-) diff --git a/.github/actions/run-clang-tidy-pr.sh b/.github/actions/run-clang-tidy-pr.sh index 7b63e9950..b52df4b08 100755 --- a/.github/actions/run-clang-tidy-pr.sh +++ b/.github/actions/run-clang-tidy-pr.sh @@ -56,7 +56,7 @@ tidyerr=0 if ((filecount > 0 && filecount <= 25)); then echo "====Configure CMake====" mkdir build && cd build || exit - cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DGRIDDYN_BUILD_C_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON -DGRIDDYN_ENABLE_NETWORKING_LIBRARY=ON -DGRIDDYN_ENABLE_OPTIMIZATION_LIBRARY=ON -DGRIDDYN_ENABLE_TCP=ON -DGRIDDYN_ENABLE_DIME=ON -DGRIDDYN_ENABLE_ZMQ=ON .. + cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DGRIDDYN_BUILD_CXX_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON -DGRIDDYN_ENABLE_NETWORKING_LIBRARY=ON -DGRIDDYN_ENABLE_OPTIMIZATION_LIBRARY=ON -DGRIDDYN_ENABLE_TCP=ON -DGRIDDYN_ENABLE_DIME=ON -DGRIDDYN_ENABLE_ZMQ=ON .. TIDY_EXTRA_ARGS=() KLU_INCLUDE_DIR=$(grep '^KLU_INCLUDE_DIR:' CMakeCache.txt | cut -d= -f2- || true) append_include_args "$KLU_INCLUDE_DIR" diff --git a/.github/workflows/compiler-tests.yml b/.github/workflows/compiler-tests.yml index 5d23fe35d..aa5b65f48 100644 --- a/.github/workflows/compiler-tests.yml +++ b/.github/workflows/compiler-tests.yml @@ -27,16 +27,16 @@ jobs: include: - name: GCC 14 container: helics/buildenv:gcc14-builder - cmake_flags: -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_C_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON + cmake_flags: -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_CXX_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON - name: GCC 15 container: helics/buildenv:gcc15-builder cmake_flags: -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_ENABLE_OPTIMIZATION_LIBRARY=ON - name: Clang 20 container: helics/buildenv:clang20-builder - cmake_flags: -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_C_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON -DGRIDDYN_ENABLE_ZMQ=ON -DGRIDDYN_ENABLE_NETWORKING_LIBRARY=ON -DGRIDDYN_ENABLE_TCP=ON + cmake_flags: -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_CXX_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON -DGRIDDYN_ENABLE_ZMQ=ON -DGRIDDYN_ENABLE_NETWORKING_LIBRARY=ON -DGRIDDYN_ENABLE_TCP=ON - name: Clang 22 container: helics/buildenv:clang22-builder - cmake_flags: -DCMAKE_CXX_STANDARD=26 -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_C_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON + cmake_flags: -DCMAKE_CXX_STANDARD=26 -DGRIDDYN_BUILD_TESTS=ON -DGRIDDYN_BUILD_CXX_SHARED_LIBRARY=ON -DGRIDDYN_ENABLE_FMI_EXPORT=ON steps: - uses: actions/checkout@v6 diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index b72a80277..47121198a 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -468,8 +468,11 @@ cmd /v:on /c "set PATH=& set Path=& ""C:\Program Files\CMake\bin\cmake.exe"" --b `kundur/kundur_full.json` case. A perturbed-state test compares all six differential equations against direct ANDES-equation reference values. Residual, derivative, algebraic, and Jacobian finite-difference checks - exercise the saturated model, and invalid reactance ordering is rejected - during initialization. + exercise the saturated model. Invalid transient/subtransient ordering and + singular gamma-coefficient denominators are rejected during initialization. + As in ANDES, leakage reactance above subtransient reactance produces a + warning but remains runnable; this is required by the bundled IEEE 39-bus + DYR case. - The DYR reader creates the registered `GenModelGENROU` and applies the DYR machine parameters after attachment so RAW stator data is retained without allowing the RAW source reactance to overwrite `Xd`. A dedicated diff --git a/src/griddyn/genmodels/GenModel.cpp b/src/griddyn/genmodels/GenModel.cpp index a6308dd31..612905c9e 100644 --- a/src/griddyn/genmodels/GenModel.cpp +++ b/src/griddyn/genmodels/GenModel.cpp @@ -37,38 +37,38 @@ namespace { } } // namespace -static TypeFactory genModelFactory("genmodel", - std::to_array({"trivial"})); +static TypeFactory gGenModelFactory("genmodel", + std::to_array({"trivial"})); static ChildTypeFactory - inverterGenModelFactory("genmodel", std::to_array({"inverter"})); -static ChildTypeFactory classicalGenModelFactory( + gInverterGenModelFactory("genmodel", std::to_array({"inverter"})); +static ChildTypeFactory gClassicalGenModelFactory( "genmodel", std::to_array( {"basic", "2", "second", "secondorder", "classic", "classical", "II"})); static ChildTypeFactory - thirdOrderGenModelFactory("genmodel", - std::to_array({"3", "third", "thirdorder", "III"})); -static ChildTypeFactory fourthOrderGenModelFactory( + gThirdOrderGenModelFactory("genmodel", + std::to_array({"3", "third", "thirdorder", "III"})); +static ChildTypeFactory gFourthOrderGenModelFactory( "genmodel", std::to_array({"4", "fourth", "fourthorder", "IV", "grdc"}), "4"); -static ChildTypeFactory fifthOrderGenModelFactory( +static ChildTypeFactory gFifthOrderGenModelFactory( "genmodel", std::to_array({"5", "fifth", "fifthorder", "5.1", "Vtype1", "V"})); static ChildTypeFactory - fifthOrderGenModelType2Factory( + gFifthOrderGenModelType2Factory( "genmodel", std::to_array({"5.2", "fifthtype2", "fifthordertype2", "Vtype2"})); static ChildTypeFactory - sixthOrderGenModelFactory("genmodel", - std::to_array({"6", "six", "sixthorder", "VI"})); + gSixthOrderGenModelFactory("genmodel", + std::to_array({"6", "six", "sixthorder", "VI"})); static ChildTypeFactory - sixthOrderGenModelType2Factory( + gSixthOrderGenModelType2Factory( "genmodel", std::to_array({"6.2", "sixtype2", "sixthordertype2", "VItype2", "VI.2"})); static ChildTypeFactory - genrouModelFactory("genmodel", std::to_array({"genrou"})); -static ChildTypeFactory eighthOrderGenModelFactory( + gGenrouModelFactory("genmodel", std::to_array({"genrou"})); +static ChildTypeFactory gEighthOrderGenModelFactory( "genmodel", std::to_array({"8", "eight", "eighthorder", "VIII"})); diff --git a/src/griddyn/genmodels/GenModel4.cpp b/src/griddyn/genmodels/GenModel4.cpp index e1676a71c..f4e18e7d0 100644 --- a/src/griddyn/genmodels/GenModel4.cpp +++ b/src/griddyn/genmodels/GenModel4.cpp @@ -24,7 +24,6 @@ CoreObject* GenModel4::clone(CoreObject* obj) const gd->Tqop = Tqop; gd->S10 = S10; gd->S12 = S12; - gd->sat = sat; return gd; } diff --git a/src/griddyn/genmodels/GenModelGENROU.cpp b/src/griddyn/genmodels/GenModelGENROU.cpp index ece32cf38..5a624508c 100644 --- a/src/griddyn/genmodels/GenModelGENROU.cpp +++ b/src/griddyn/genmodels/GenModelGENROU.cpp @@ -17,6 +17,10 @@ #include namespace griddyn::genmodels { +// The implementation mirrors the published equations, whose conventional +// grouping is clearer than the extra precedence-only parentheses requested by +// this clang-tidy check. +// NOLINTBEGIN(readability-math-missing-parentheses) namespace { /** Coefficients used to eliminate ANDES's auxiliary flux algebraic states. * @@ -25,28 +29,32 @@ namespace { * without changing the machine equations documented on GenModelGENROU. */ struct GenrouCoefficients { - double gd1; - double gq1; - double gd2; - double gq2; - double gqd; + double mGd1; + double mGq1; + double mGd2; + double mGq2; + double mGqd; }; - GenrouCoefficients computeCoefficients(double xd, - double xq, - double xdp, - double xqp, - double xdpp, - double xqpp, - double xl) + GenrouCoefficients computeCoefficients(double directAxisReactance, + double quadratureAxisReactance, + double directAxisTransientReactance, + double quadratureAxisTransientReactance, + double directAxisSubtransientReactance, + double quadratureAxisSubtransientReactance, + double leakageReactance) { - const double xdDifference = xdp - xl; - const double xqDifference = xqp - xl; - return {(xdpp - xl) / xdDifference, - (xqpp - xl) / xqDifference, - (xdp - xdpp) / (xdDifference * xdDifference), - (xqp - xqpp) / (xqDifference * xqDifference), - (xq - xl) / (xd - xl)}; + const double xdDifference = directAxisTransientReactance - leakageReactance; + const double xqDifference = quadratureAxisTransientReactance - leakageReactance; + return {.mGd1 = (directAxisSubtransientReactance - leakageReactance) / xdDifference, + .mGq1 = (quadratureAxisSubtransientReactance - leakageReactance) / xqDifference, + .mGd2 = (directAxisTransientReactance - directAxisSubtransientReactance) / + (xdDifference * xdDifference), + .mGq2 = (quadratureAxisTransientReactance - + quadratureAxisSubtransientReactance) / + (xqDifference * xqDifference), + .mGqd = (quadratureAxisReactance - leakageReactance) / + (directAxisReactance - leakageReactance)}; } } // namespace @@ -65,20 +73,35 @@ GenModelGENROU::GenModelGENROU(const std::string& objName): GenModel5(objName) CoreObject* GenModelGENROU::clone(CoreObject* obj) const { - auto* gd = cloneBase(this, obj); - if (gd == nullptr) { + auto* genrouClone = cloneBase(this, obj); + if (genrouClone == nullptr) { return obj; } - return gd; + genrouClone->sat = sat; + return genrouClone; } void GenModelGENROU::dynObjectInitializeA(CoreTime /*time0*/, std::uint32_t /*flags*/) { + constexpr double minimumReactanceDifference = 1e-9; if ((H <= 0.0) || (Tdop <= 0.0) || (Tdopp <= 0.0) || (Tqop <= 0.0) || (Tqopp <= 0.0) || - (Xd <= Xdp) || (Xdp < Xdpp) || (Xdpp < Xl) || (Xq <= Xqp) || (Xqp < Xqpp) || (Xqpp < Xl)) { + (Xd <= Xdp) || (Xdp < Xdpp) || (Xq <= Xqp) || (Xqp < Xqpp) || + (std::abs(Xdp - Xl) <= minimumReactanceDifference) || + (std::abs(Xqp - Xl) <= minimumReactanceDifference) || + (std::abs(Xd - Xl) <= minimumReactanceDifference)) { throw InvalidParameterValue("GENROU reactances, inertia, or time constants"); } + // ANDES reports xl > xd2 as an initialization warning but continues to + // solve the model. Preserve that behavior for compatible PSS/E data; the + // gamma coefficients remain well-defined unless a denominator above is + // singular. + if ((Xl > Xdpp) || (Xl > Xqpp)) { + logging::warning(this, + "GENROU leakage reactance exceeds a subtransient reactance; " + "continuing with the supplied parameters"); + } + offsets.local().local.diffSize = 6; offsets.local().local.algSize = 2; offsets.local().local.jacSize = 50; @@ -108,7 +131,7 @@ void GenModelGENROU::dynObjectInitializeB(const IOdata& inputs, const double fluxCurrentAngle = std::arg(subtransientFlux) - std::arg(terminalCurrent); const double angleNumerator = currentMagnitude * (Xqpp - Xq) * std::cos(fluxCurrentAngle); const double angleDenominator = currentMagnitude * (Xqpp - Xq) * std::sin(fluxCurrentAngle) - - fluxMagnitude * (1.0 + saturation * coefficients.gqd); + fluxMagnitude * (1.0 + saturation * coefficients.mGqd); const double rotorAngle = std::atan(angleNumerator / angleDenominator) + std::arg(subtransientFlux); @@ -116,66 +139,79 @@ void GenModelGENROU::dynObjectInitializeB(const IOdata& inputs, const std::complex fluxDq = subtransientFlux * dqRotation; const std::complex currentDq = std::conj(terminalCurrent * dqRotation); const double andesId = std::imag(currentDq); - const double iq = std::real(currentDq); + const double quadratureCurrent = std::real(currentDq); const double idCurrent = -andesId; const double psi2d = std::real(fluxDq); const double psi2q = std::imag(fluxDq); // negative of the ANDES q-axis flux convention - double* gm = m_state.data(); - gm[0] = idCurrent; - gm[1] = iq; - gm[2] = rotorAngle; - gm[3] = 1.0; + double* state = m_state.data(); + state[0] = idCurrent; + state[1] = quadratureCurrent; + state[2] = rotorAngle; + state[3] = 1.0; Vd = -voltageMagnitude * std::sin(rotorAngle - voltageAngle); Vq = voltageMagnitude * std::cos(rotorAngle - voltageAngle); const double fieldVoltage = (1.0 + saturation) * psi2d + (Xd - Xdpp) * andesId; - gm[4] = -(Xq - Xqp) * iq - saturation * coefficients.gqd * psi2q; - gm[5] = (Xd - Xdp) * idCurrent - saturation * psi2d + fieldVoltage; - gm[6] = -(Xq - Xl) * iq - saturation * coefficients.gqd * psi2q; - gm[7] = (Xd - Xl) * idCurrent - saturation * psi2d + fieldVoltage; - - const double mechanicalPower = (Vd + Rs * idCurrent) * idCurrent + (Vq + Rs * iq) * iq; + state[4] = + -(Xq - Xqp) * quadratureCurrent - saturation * coefficients.mGqd * psi2q; + state[5] = (Xd - Xdp) * idCurrent - saturation * psi2d + fieldVoltage; + state[6] = -(Xq - Xl) * quadratureCurrent - saturation * coefficients.mGqd * psi2q; + state[7] = (Xd - Xl) * idCurrent - saturation * psi2d + fieldVoltage; + + const double mechanicalPower = (Vd + Rs * idCurrent) * idCurrent + + (Vq + Rs * quadratureCurrent) * quadratureCurrent; fieldSet[genModelEftInLocation] = fieldVoltage; fieldSet[genModelPmechInLocation] = mechanicalPower; } void GenModelGENROU::algebraicUpdate(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double update[], const SolverMode& sMode, double /*alpha*/) { - auto Loc = offsets.getLocations(sD, update, sMode, this); - updateLocalCache(inputs, sD, sMode); + auto locations = offsets.getLocations(stateData, update, sMode, this); + updateLocalCache(inputs, stateData, sMode); const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); const double psi2q = - coefficients.gq1 * Loc.diffStateLoc[2] + (1.0 - coefficients.gq1) * Loc.diffStateLoc[4]; + coefficients.mGq1 * locations.diffStateLoc[2] + + (1.0 - coefficients.mGq1) * locations.diffStateLoc[4]; const double psi2d = - coefficients.gd1 * Loc.diffStateLoc[3] + (1.0 - coefficients.gd1) * Loc.diffStateLoc[5]; + coefficients.mGd1 * locations.diffStateLoc[3] + + (1.0 - coefficients.mGd1) * locations.diffStateLoc[5]; gmlc::utilities::solve2x2( - Rs, Xqpp, -Xdpp, Rs, psi2q - Vd, psi2d - Vq, Loc.destLoc[0], Loc.destLoc[1]); - m_output = -(Loc.destLoc[1] * Vq + Loc.destLoc[0] * Vd); + Rs, + Xqpp, + -Xdpp, + Rs, + psi2q - Vd, + psi2d - Vq, + locations.destLoc[0], + locations.destLoc[1]); + m_output = -(locations.destLoc[1] * Vq + locations.destLoc[0] * Vd); } void GenModelGENROU::derivative(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double deriv[], const SolverMode& sMode) { if (isAlgebraicOnly(sMode)) { return; } - auto Loc = offsets.getLocations(sD, deriv, sMode, this); - const double* alg = Loc.algStateLoc; - const double* state = Loc.diffStateLoc; - double* dstate = Loc.destDiffLoc; + auto locations = offsets.getLocations(stateData, deriv, sMode, this); + const double* alg = locations.algStateLoc; + const double* state = locations.diffStateLoc; + double* dstate = locations.destDiffLoc; const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); - const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; - const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + const double psi2q = + coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = + coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; const double fluxMagnitude = std::hypot(psi2d, psi2q); const double saturation = sat.compute(fluxMagnitude); @@ -187,12 +223,14 @@ void GenModelGENROU::derivative(const IOdata& inputs, dstate[1] = 0.5 * (mechanicalPower - electricalTorque - D * (state[1] - 1.0)) / H; dstate[2] = (-state[2] - - (Xq - Xqp) * (coefficients.gq2 * (state[2] - state[4]) + coefficients.gq1 * alg[1]) - - saturation * coefficients.gqd * psi2q) / + (Xq - Xqp) * + (coefficients.mGq2 * (state[2] - state[4]) + coefficients.mGq1 * alg[1]) - + saturation * coefficients.mGqd * psi2q) / Tqop; dstate[3] = (fieldVoltage - state[3] + - (Xd - Xdp) * (coefficients.gd1 * alg[0] + coefficients.gd2 * (state[5] - state[3])) - + (Xd - Xdp) * + (coefficients.mGd1 * alg[0] + coefficients.mGd2 * (state[5] - state[3])) - saturation * psi2d) / Tdop; dstate[4] = (-state[4] + state[2] - (Xqp - Xl) * alg[1]) / Tqopp; @@ -200,27 +238,29 @@ void GenModelGENROU::derivative(const IOdata& inputs, } void GenModelGENROU::residual(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double resid[], const SolverMode& sMode) { - auto Loc = offsets.getLocations(sD, resid, sMode, this); - const double* alg = Loc.algStateLoc; - const double* state = Loc.diffStateLoc; - const double* dstate = Loc.dstateLoc; - double* algResidual = Loc.destLoc; - double* stateResidual = Loc.destDiffLoc; - updateLocalCache(inputs, sD, sMode); + auto locations = offsets.getLocations(stateData, resid, sMode, this); + const double* alg = locations.algStateLoc; + const double* state = locations.diffStateLoc; + const double* dstate = locations.dstateLoc; + double* algResidual = locations.destLoc; + double* stateResidual = locations.destDiffLoc; + updateLocalCache(inputs, stateData, sMode); const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); if (hasAlgebraic(sMode)) { - const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; - const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + const double psi2q = + coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = + coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; algResidual[0] = Vd + Rs * alg[0] + Xqpp * alg[1] - psi2q; algResidual[1] = Vq + Rs * alg[1] - Xdpp * alg[0] - psi2d; } if (hasDifferential(sMode)) { - derivative(inputs, sD, resid, sMode); + derivative(inputs, stateData, resid, sMode); for (index_t index = 0; index < 6; ++index) { stateResidual[index] -= dstate[index]; } @@ -228,72 +268,81 @@ void GenModelGENROU::residual(const IOdata& inputs, } void GenModelGENROU::jacobianElements(const IOdata& inputs, - const StateData& sD, - MatrixData& md, + const StateData& stateData, + MatrixData& matrixData, const IOlocs& inputLocs, const SolverMode& sMode) { - auto Loc = offsets.getLocations(sD, sMode, this); - const double* alg = Loc.algStateLoc; - const double* state = Loc.diffStateLoc; - const auto refAlg = Loc.algOffset; - const auto refDiff = Loc.diffOffset; + auto locations = offsets.getLocations(stateData, sMode, this); + const double* alg = locations.algStateLoc; + const double* state = locations.diffStateLoc; + const auto refAlg = locations.algOffset; + const auto refDiff = locations.diffOffset; const auto voltageLoc = inputLocs[VOLTAGE_IN_LOCATION]; const auto angleLoc = inputLocs[ANGLE_IN_LOCATION]; - updateLocalCache(inputs, sD, sMode); + updateLocalCache(inputs, stateData, sMode); const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); if (hasAlgebraic(sMode)) { if (angleLoc != kNullLocation) { - md.assign(refAlg, angleLoc, Vq); - md.assign(refAlg + 1, angleLoc, -Vd); + matrixData.assign(refAlg, angleLoc, Vq); + matrixData.assign(refAlg + 1, angleLoc, -Vd); } if (voltageLoc != kNullLocation) { - md.assign(refAlg, voltageLoc, Vd / inputs[VOLTAGE_IN_LOCATION]); - md.assign(refAlg + 1, voltageLoc, Vq / inputs[VOLTAGE_IN_LOCATION]); + matrixData.assign(refAlg, voltageLoc, Vd / inputs[VOLTAGE_IN_LOCATION]); + matrixData.assign(refAlg + 1, voltageLoc, Vq / inputs[VOLTAGE_IN_LOCATION]); } - md.assign(refAlg, refAlg, Rs); - md.assign(refAlg, refAlg + 1, Xqpp); - md.assign(refAlg + 1, refAlg, -Xdpp); - md.assign(refAlg + 1, refAlg + 1, Rs); + matrixData.assign(refAlg, refAlg, Rs); + matrixData.assign(refAlg, refAlg + 1, Xqpp); + matrixData.assign(refAlg + 1, refAlg, -Xdpp); + matrixData.assign(refAlg + 1, refAlg + 1, Rs); if (isAlgebraicOnly(sMode)) { return; } - md.assign(refAlg, refDiff, -Vq); - md.assign(refAlg, refDiff + 2, -coefficients.gq1); - md.assign(refAlg, refDiff + 4, -(1.0 - coefficients.gq1)); - md.assign(refAlg + 1, refDiff, Vd); - md.assign(refAlg + 1, refDiff + 3, -coefficients.gd1); - md.assign(refAlg + 1, refDiff + 5, -(1.0 - coefficients.gd1)); + matrixData.assign(refAlg, refDiff, -Vq); + matrixData.assign(refAlg, refDiff + 2, -coefficients.mGq1); + matrixData.assign(refAlg, refDiff + 4, -(1.0 - coefficients.mGq1)); + matrixData.assign(refAlg + 1, refDiff, Vd); + matrixData.assign(refAlg + 1, refDiff + 3, -coefficients.mGd1); + matrixData.assign(refAlg + 1, refDiff + 5, -(1.0 - coefficients.mGd1)); } if (!hasDifferential(sMode)) { return; } - md.assign(refDiff, refDiff, -sD.cj); - md.assign(refDiff, refDiff + 1, systemBaseFrequency); + matrixData.assign(refDiff, refDiff, -stateData.cj); + matrixData.assign(refDiff, refDiff + 1, systemBaseFrequency); const double inverseTwoH = 0.5 / H; if (hasAlgebraic(sMode)) { - md.assign(refDiff + 1, refAlg, -inverseTwoH * (Vd + 2.0 * Rs * alg[0])); - md.assign(refDiff + 1, refAlg + 1, -inverseTwoH * (Vq + 2.0 * Rs * alg[1])); + matrixData.assign(refDiff + 1, refAlg, -inverseTwoH * (Vd + 2.0 * Rs * alg[0])); + matrixData.assign(refDiff + 1, + refAlg + 1, + -inverseTwoH * (Vq + 2.0 * Rs * alg[1])); } - md.assign(refDiff + 1, refDiff, inverseTwoH * (Vq * alg[0] - Vd * alg[1])); - md.assign(refDiff + 1, refDiff + 1, -inverseTwoH * D - sD.cj); - md.assignCheckCol(refDiff + 1, inputLocs[genModelPmechInLocation], inverseTwoH); + matrixData.assign(refDiff + 1, refDiff, inverseTwoH * (Vq * alg[0] - Vd * alg[1])); + matrixData.assign(refDiff + 1, refDiff + 1, -inverseTwoH * D - stateData.cj); + matrixData.assignCheckCol(refDiff + 1, + inputLocs[genModelPmechInLocation], + inverseTwoH); if (angleLoc != kNullLocation) { - md.assign(refDiff + 1, angleLoc, -inverseTwoH * (Vq * alg[0] - Vd * alg[1])); + matrixData.assign(refDiff + 1, + angleLoc, + -inverseTwoH * (Vq * alg[0] - Vd * alg[1])); } if (voltageLoc != kNullLocation) { - md.assign(refDiff + 1, - voltageLoc, - -inverseTwoH * (Vd * alg[0] + Vq * alg[1]) / inputs[VOLTAGE_IN_LOCATION]); + matrixData.assign(refDiff + 1, + voltageLoc, + -inverseTwoH * (Vd * alg[0] + Vq * alg[1]) / + inputs[VOLTAGE_IN_LOCATION]); } - const double psi2q = coefficients.gq1 * state[2] + (1.0 - coefficients.gq1) * state[4]; - const double psi2d = coefficients.gd1 * state[3] + (1.0 - coefficients.gd1) * state[5]; + const double psi2q = + coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = + coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; const double fluxMagnitude = std::hypot(psi2d, psi2q); const auto saturation = sat.evaluate(fluxMagnitude); double dSaturationD = 0.0; @@ -302,46 +351,70 @@ void GenModelGENROU::jacobianElements(const IOdata& inputs, dSaturationD = saturation.derivative * psi2d / fluxMagnitude; dSaturationQ = saturation.derivative * psi2q / fluxMagnitude; } - const double dQSatD = coefficients.gqd * psi2q * dSaturationD; - const double dQSatQ = coefficients.gqd * (saturation.value + psi2q * dSaturationQ); + const double dQSatD = coefficients.mGqd * psi2q * dSaturationD; + const double dQSatQ = coefficients.mGqd * (saturation.value + psi2q * dSaturationQ); const double dDSatD = saturation.value + psi2d * dSaturationD; const double dDSatQ = psi2d * dSaturationQ; if (hasAlgebraic(sMode)) { - md.assign(refDiff + 2, refAlg + 1, -(Xq - Xqp) * coefficients.gq1 / Tqop); + matrixData.assign(refDiff + 2, + refAlg + 1, + -(Xq - Xqp) * coefficients.mGq1 / Tqop); } - md.assign(refDiff + 2, - refDiff + 2, - (-1.0 - (Xq - Xqp) * coefficients.gq2 - dQSatQ * coefficients.gq1) / Tqop - sD.cj); - md.assign(refDiff + 2, - refDiff + 4, - ((Xq - Xqp) * coefficients.gq2 - dQSatQ * (1.0 - coefficients.gq1)) / Tqop); - md.assign(refDiff + 2, refDiff + 3, -dQSatD * coefficients.gd1 / Tqop); - md.assign(refDiff + 2, refDiff + 5, -dQSatD * (1.0 - coefficients.gd1) / Tqop); + matrixData.assign(refDiff + 2, + refDiff + 2, + (-1.0 - (Xq - Xqp) * coefficients.mGq2 - + dQSatQ * coefficients.mGq1) / + Tqop - + stateData.cj); + matrixData.assign(refDiff + 2, + refDiff + 4, + ((Xq - Xqp) * coefficients.mGq2 - + dQSatQ * (1.0 - coefficients.mGq1)) / + Tqop); + matrixData.assign(refDiff + 2, + refDiff + 3, + -dQSatD * coefficients.mGd1 / Tqop); + matrixData.assign(refDiff + 2, + refDiff + 5, + -dQSatD * (1.0 - coefficients.mGd1) / Tqop); if (hasAlgebraic(sMode)) { - md.assign(refDiff + 3, refAlg, (Xd - Xdp) * coefficients.gd1 / Tdop); + matrixData.assign(refDiff + 3, + refAlg, + (Xd - Xdp) * coefficients.mGd1 / Tdop); } - md.assign(refDiff + 3, - refDiff + 3, - (-1.0 - (Xd - Xdp) * coefficients.gd2 - dDSatD * coefficients.gd1) / Tdop - sD.cj); - md.assign(refDiff + 3, - refDiff + 5, - ((Xd - Xdp) * coefficients.gd2 - dDSatD * (1.0 - coefficients.gd1)) / Tdop); - md.assign(refDiff + 3, refDiff + 2, -dDSatQ * coefficients.gq1 / Tdop); - md.assign(refDiff + 3, refDiff + 4, -dDSatQ * (1.0 - coefficients.gq1) / Tdop); - md.assignCheckCol(refDiff + 3, inputLocs[genModelEftInLocation], 1.0 / Tdop); + matrixData.assign(refDiff + 3, + refDiff + 3, + (-1.0 - (Xd - Xdp) * coefficients.mGd2 - + dDSatD * coefficients.mGd1) / + Tdop - + stateData.cj); + matrixData.assign(refDiff + 3, + refDiff + 5, + ((Xd - Xdp) * coefficients.mGd2 - + dDSatD * (1.0 - coefficients.mGd1)) / + Tdop); + matrixData.assign(refDiff + 3, + refDiff + 2, + -dDSatQ * coefficients.mGq1 / Tdop); + matrixData.assign(refDiff + 3, + refDiff + 4, + -dDSatQ * (1.0 - coefficients.mGq1) / Tdop); + matrixData.assignCheckCol(refDiff + 3, + inputLocs[genModelEftInLocation], + 1.0 / Tdop); if (hasAlgebraic(sMode)) { - md.assign(refDiff + 4, refAlg + 1, -(Xqp - Xl) / Tqopp); + matrixData.assign(refDiff + 4, refAlg + 1, -(Xqp - Xl) / Tqopp); } - md.assign(refDiff + 4, refDiff + 2, 1.0 / Tqopp); - md.assign(refDiff + 4, refDiff + 4, -1.0 / Tqopp - sD.cj); + matrixData.assign(refDiff + 4, refDiff + 2, 1.0 / Tqopp); + matrixData.assign(refDiff + 4, refDiff + 4, -1.0 / Tqopp - stateData.cj); if (hasAlgebraic(sMode)) { - md.assign(refDiff + 5, refAlg, (Xdp - Xl) / Tdopp); + matrixData.assign(refDiff + 5, refAlg, (Xdp - Xl) / Tdopp); } - md.assign(refDiff + 5, refDiff + 3, 1.0 / Tdopp); - md.assign(refDiff + 5, refDiff + 5, -1.0 / Tdopp - sD.cj); + matrixData.assign(refDiff + 5, refDiff + 3, 1.0 / Tdopp); + matrixData.assign(refDiff + 5, refDiff + 5, -1.0 / Tdopp - stateData.cj); } void GenModelGENROU::set(std::string_view param, std::string_view val) @@ -365,10 +438,12 @@ void GenModelGENROU::set(std::string_view param, double val, units::unit unitTyp } } -static const stringVec genModelGenrouNames{"id", "iq", "delta", "freq", "e1d", "e1q", "e2q", "e2d"}; +static const stringVec GEN_MODEL_GENROU_NAMES{ + "id", "iq", "delta", "freq", "e1d", "e1q", "e2q", "e2d"}; stringVec GenModelGENROU::localStateNames() const { - return genModelGenrouNames; + return GEN_MODEL_GENROU_NAMES; } +// NOLINTEND(readability-math-missing-parentheses) } // namespace griddyn::genmodels diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index ed8e02a5f..d2f1187bf 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -156,6 +156,12 @@ namespace griddyn::genmodels { * @note PSS/E requires the GENROU d- and q-axis subtransient reactances to be * equal. The class retains separate \c Xdpp and \c Xqpp storage to fit the * GridDyn architecture; the \c xpp setter assigns both values. + * + * @note ANDES warns when leakage reactance \f$x_l\f$ exceeds subtransient + * reactance \f$x''_d\f$ but continues initialization. GridDyn follows that + * compatibility behavior because the equations remain defined. Parameter + * sets that make \f$x'_d-x_l\f$, \f$x'_q-x_l\f$, or \f$x_d-x_l\f$ singular are + * rejected. */ class GenModelGENROU: public GenModel5 { protected: diff --git a/src/utilities/Saturation.cpp b/src/utilities/Saturation.cpp index 3689f399c..4c6517384 100644 --- a/src/utilities/Saturation.cpp +++ b/src/utilities/Saturation.cpp @@ -9,7 +9,10 @@ #include namespace utilities { -Saturation::Saturation(SaturationType sT): type(sT) +// These functions implement standard fitted curves. Conventional arithmetic +// grouping is kept to make the equations recognizable. +// NOLINTBEGIN(readability-math-missing-parentheses) +Saturation::Saturation(SaturationType saturationType): type(saturationType) { loadFunctions(); computeParam(); @@ -77,46 +80,53 @@ void Saturation::setType(const std::string& stype) computeParam(); } -void Saturation::setParam(double S1, double S2) +void Saturation::setParam(double saturationAtOne, double saturationAtOnePointTwo) { - s10 = S1; - s12 = S2; + s10 = saturationAtOne; + s12 = saturationAtOnePointTwo; computeParam(); } -void Saturation::setParam(double V1, double S1, double V2, double S2) +void Saturation::setParam(double firstInput, + double firstSaturation, + double secondInput, + double secondSaturation) { switch (type) { case SaturationType::QUADRATIC: { - double ssv = sqrt(S1 / S2); - A = -(V2 * ssv - V1) / (V1 - ssv); - B = S1 / ((V1 - A) * (V1 - A)); + const double ssv = sqrt(firstSaturation / secondSaturation); + A = -(secondInput * ssv - firstInput) / (firstInput - ssv); + B = firstSaturation / ((firstInput - A) * (firstInput - A)); } break; case SaturationType::SCALED_QUADRATIC: case SaturationType::CUTOFF_SCALED_QUADRATIC: { - if ((V1 <= 0.0) || (V2 <= 0.0) || (S1 <= 0.0) || (S2 <= 0.0)) { + if ((firstInput <= 0.0) || (secondInput <= 0.0) || + (firstSaturation <= 0.0) || (secondSaturation <= 0.0)) { A = 0.0; B = 0.0; break; } - double ssv = sqrt((S1 * V1) / (S2 * V2)); - const double fitDenominator = V1 - ssv; + const double ssv = + sqrt((firstSaturation * firstInput) / (secondSaturation * secondInput)); + const double fitDenominator = firstInput - ssv; if (std::abs(fitDenominator) < 1e-12) { A = 0.0; B = 0.0; break; } - A = -(V2 * ssv - V1) / fitDenominator; - const double distance = V1 - A; - B = (std::abs(distance) < 1e-12) ? 0.0 : S1 / (distance * distance); + A = -(secondInput * ssv - firstInput) / fitDenominator; + const double distance = firstInput - A; + B = (std::abs(distance) < 1e-12) ? + 0.0 : + firstSaturation / (distance * distance); } break; case SaturationType::EXPONENTIAL: - A = log(S1 / S2) / log(V1 / V2); - B = S1 / (pow(V1, A)); + A = log(firstSaturation / secondSaturation) / log(firstInput / secondInput); + B = firstSaturation / (pow(firstInput, A)); break; case SaturationType::LINEAR: - B = (S2 - S1) / (V2 - V1); - A = V1 - S1 / B; + B = (secondSaturation - firstSaturation) / (secondInput - firstInput); + A = firstInput - firstSaturation / B; break; case SaturationType::NONE: default: @@ -128,9 +138,9 @@ void Saturation::setParam(double V1, double S1, double V2, double S2) s12 = compute(1.2); } -void Saturation::setType(SaturationType sT) +void Saturation::setType(SaturationType saturationType) { - type = sT; + type = saturationType; loadFunctions(); computeParam(); } @@ -153,7 +163,7 @@ double Saturation::deriv(double val) const } Saturation::Evaluation Saturation::evaluate(double val) const { - return {satFunc(val), derivFunc(val)}; + return {.value = satFunc(val), .derivative = derivFunc(val)}; } double Saturation::inv(double val) const { @@ -167,7 +177,7 @@ double Saturation::inv(double val) const break; case SaturationType::SCALED_QUADRATIC: case SaturationType::CUTOFF_SCALED_QUADRATIC: { - double temp = (2 * A + val / B); + const double temp = (2 * A + val / B); ret = (temp + sqrt(temp * temp - 4 * A * A)) / 2; break; } @@ -193,7 +203,7 @@ void Saturation::computeParam() B = 0.0; break; } - double ssv = sqrt(s10 / s12); + const double ssv = sqrt(s10 / s12); const double fitDenominator = 1.0 - ssv; if (std::abs(fitDenominator) < 1e-12) { A = 0.0; @@ -212,7 +222,7 @@ void Saturation::computeParam() B = 0.0; break; } - double ssv = sqrt((s10 * 1.0) / (s12 * 1.2)); + const double ssv = sqrt((s10 * 1.0) / (s12 * 1.2)); const double fitDenominator = 1.0 - ssv; if (std::abs(fitDenominator) < 1e-12) { A = 0.0; @@ -260,8 +270,8 @@ void Saturation::loadFunctions() if ((B == 0.0) || (val == 0.0)) { return 0.0; } - double v1 = (val - A); - return (B * (2 * val * v1 - v1 * v1) / (val * val)); + const double distance = (val - A); + return (B * (2 * val * distance - distance * distance) / (val * val)); }; break; case SaturationType::CUTOFF_SCALED_QUADRATIC: @@ -296,4 +306,5 @@ void Saturation::loadFunctions() } } +// NOLINTEND(readability-math-missing-parentheses) } // namespace utilities diff --git a/src/utilities/Saturation.h b/src/utilities/Saturation.h index 7abf2990e..6a501ba48 100644 --- a/src/utilities/Saturation.h +++ b/src/utilities/Saturation.h @@ -54,9 +54,9 @@ class Saturation { public: /** construction saturation from saturation type * @details constructor is converting type - * @param[in] sT saturation Type + * @param[in] saturationType saturation type */ - explicit Saturation(SaturationType sT = SaturationType::SCALED_QUADRATIC); + explicit Saturation(SaturationType saturationType = SaturationType::SCALED_QUADRATIC); /** construct from string naming saturation type *@param[in] satType a string containing the type of the saturation*/ explicit Saturation(const std::string& satType); @@ -68,21 +68,24 @@ class Saturation { /** set the S10 and S12 parameter *@details sets the parameters of the saturation function previously specified at the point 1.0 *and 1.2 The values input should correspond to the reduction in values so 0.0 for no saturation - *@param[in] S1 the value reduction at 1.0 - *@param[in] S2 the value reduction at 1.2 + *@param[in] saturationAtOne the value reduction at 1.0 + *@param[in] saturationAtOnePointTwo the value reduction at 1.2 */ - void setParam(double S1, double S2); + void setParam(double saturationAtOne, double saturationAtOnePointTwo); /** @brief define the saturation function by specifying the reduction at two points *@details sets the parameters of the saturation function previously specified at the points V1 *and V2 The values input should correspond to the reduction in values so 0.0 for no saturation - *@param[in] V1 the point along the saturation curve that S1 is given - *@param[in] S1 the value reduction at V1 - *@param[in] V2 the point along the saturation curve from which S2 is given - *@param[in] S2 the value reduction at V2 + *@param[in] firstInput the point along the saturation curve for the first value + *@param[in] firstSaturation the value reduction at firstInput + *@param[in] secondInput the point along the saturation curve for the second value + *@param[in] secondSaturation the value reduction at secondInput */ - void setParam(double V1, double S1, double V2, double S2); + void setParam(double firstInput, + double firstSaturation, + double secondInput, + double secondSaturation); /** update the saturation type function by enumeration*/ - void setType(SaturationType sT); + void setType(SaturationType saturationType); /** update the saturation function by a string*/ void setType(const std::string& stype); /** get the saturation function type by enumeration*/ diff --git a/test/componentTests/testGenModels.cpp b/test/componentTests/testGenModels.cpp index 420cc7832..d88dbbb3d 100644 --- a/test/componentTests/testGenModels.cpp +++ b/test/componentTests/testGenModels.cpp @@ -96,6 +96,30 @@ TEST_F(GenModelTests, GenrouRejectsInvalidParameters) EXPECT_THROW(model.dynInitializeA(0.0, 0), InvalidParameterValue); } +TEST_F(GenModelTests, GenrouAcceptsAndesLeakageReactanceWarningRange) +{ + genmodels::GenModelGENROU model; + configureKundurGenrou(model, 0.0, 1.0); + + // The IEEE 39-bus DYR case uses xl > xpp. ANDES warns about this input + // relationship but initializes the machine because its equations remain + // well-defined. + model.set("xl", 0.35); + model.set("xdpp", 0.2); + model.set("xqpp", 0.2); + + EXPECT_NO_THROW(model.dynInitializeA(0.0, 0)); +} + +TEST_F(GenModelTests, GenrouRejectsSingularReactanceCoefficient) +{ + genmodels::GenModelGENROU model; + configureKundurGenrou(model, 0.0, 1.0); + model.set("xl", 0.3); + + EXPECT_THROW(model.dynInitializeA(0.0, 0), InvalidParameterValue); +} + TEST_F(GenModelTests, GenrouAndesInitialization) { // Captured from ANDES kundur_full initialization. ANDES Id, e1d, and e2q @@ -124,6 +148,8 @@ TEST_F(GenModelTests, GenrouAndesSaturatedInitialization) 1.0, -0.46150083698625766, 0.8928705752592525, + // Captured ANDES state, not an approximation of ln(2). + // NOLINTNEXTLINE(modernize-use-std-numbers) -0.6927156250354218, 0.7328991561827378}, 2.015401860073227, diff --git a/test/libraryTests/testSaturation.cpp b/test/libraryTests/testSaturation.cpp index 43634256d..87fb53732 100644 --- a/test/libraryTests/testSaturation.cpp +++ b/test/libraryTests/testSaturation.cpp @@ -17,7 +17,7 @@ TEST(SaturationTests, CutoffScaledQuadraticReferencePoints) saturation.setParam(0.1, 0.3); const double ratio = std::sqrt(0.1 / (1.2 * 0.3)); - const double saturationStart = (1.0 - 1.2 * ratio) / (1.0 - ratio); + const double saturationStart = (1.0 - (1.2 * ratio)) / (1.0 - ratio); EXPECT_DOUBLE_EQ(saturation.compute(0.5 * saturationStart), 0.0); EXPECT_DOUBLE_EQ(saturation.deriv(0.5 * saturationStart), 0.0); From 97cac757081fc0f85e6bf4ec77d31a730e1cfb21 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:51:58 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/griddyn/genmodels/GenModel.cpp | 6 +- src/griddyn/genmodels/GenModelGENROU.cpp | 112 +++++++---------------- src/utilities/Saturation.cpp | 8 +- 3 files changed, 38 insertions(+), 88 deletions(-) diff --git a/src/griddyn/genmodels/GenModel.cpp b/src/griddyn/genmodels/GenModel.cpp index 612905c9e..6657b38b9 100644 --- a/src/griddyn/genmodels/GenModel.cpp +++ b/src/griddyn/genmodels/GenModel.cpp @@ -45,9 +45,9 @@ static ChildTypeFactory gClassi "genmodel", std::to_array( {"basic", "2", "second", "secondorder", "classic", "classical", "II"})); -static ChildTypeFactory - gThirdOrderGenModelFactory("genmodel", - std::to_array({"3", "third", "thirdorder", "III"})); +static ChildTypeFactory gThirdOrderGenModelFactory( + "genmodel", + std::to_array({"3", "third", "thirdorder", "III"})); static ChildTypeFactory gFourthOrderGenModelFactory( "genmodel", std::to_array({"4", "fourth", "fourthorder", "IV", "grdc"}), diff --git a/src/griddyn/genmodels/GenModelGENROU.cpp b/src/griddyn/genmodels/GenModelGENROU.cpp index 5a624508c..10bfda9c0 100644 --- a/src/griddyn/genmodels/GenModelGENROU.cpp +++ b/src/griddyn/genmodels/GenModelGENROU.cpp @@ -50,8 +50,7 @@ namespace { .mGq1 = (quadratureAxisSubtransientReactance - leakageReactance) / xqDifference, .mGd2 = (directAxisTransientReactance - directAxisSubtransientReactance) / (xdDifference * xdDifference), - .mGq2 = (quadratureAxisTransientReactance - - quadratureAxisSubtransientReactance) / + .mGq2 = (quadratureAxisTransientReactance - quadratureAxisSubtransientReactance) / (xqDifference * xqDifference), .mGqd = (quadratureAxisReactance - leakageReactance) / (directAxisReactance - leakageReactance)}; @@ -154,14 +153,13 @@ void GenModelGENROU::dynObjectInitializeB(const IOdata& inputs, Vq = voltageMagnitude * std::cos(rotorAngle - voltageAngle); const double fieldVoltage = (1.0 + saturation) * psi2d + (Xd - Xdpp) * andesId; - state[4] = - -(Xq - Xqp) * quadratureCurrent - saturation * coefficients.mGqd * psi2q; + state[4] = -(Xq - Xqp) * quadratureCurrent - saturation * coefficients.mGqd * psi2q; state[5] = (Xd - Xdp) * idCurrent - saturation * psi2d + fieldVoltage; state[6] = -(Xq - Xl) * quadratureCurrent - saturation * coefficients.mGqd * psi2q; state[7] = (Xd - Xl) * idCurrent - saturation * psi2d + fieldVoltage; - const double mechanicalPower = (Vd + Rs * idCurrent) * idCurrent + - (Vq + Rs * quadratureCurrent) * quadratureCurrent; + const double mechanicalPower = + (Vd + Rs * idCurrent) * idCurrent + (Vq + Rs * quadratureCurrent) * quadratureCurrent; fieldSet[genModelEftInLocation] = fieldVoltage; fieldSet[genModelPmechInLocation] = mechanicalPower; } @@ -175,22 +173,13 @@ void GenModelGENROU::algebraicUpdate(const IOdata& inputs, auto locations = offsets.getLocations(stateData, update, sMode, this); updateLocalCache(inputs, stateData, sMode); const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); - const double psi2q = - coefficients.mGq1 * locations.diffStateLoc[2] + + const double psi2q = coefficients.mGq1 * locations.diffStateLoc[2] + (1.0 - coefficients.mGq1) * locations.diffStateLoc[4]; - const double psi2d = - coefficients.mGd1 * locations.diffStateLoc[3] + + const double psi2d = coefficients.mGd1 * locations.diffStateLoc[3] + (1.0 - coefficients.mGd1) * locations.diffStateLoc[5]; gmlc::utilities::solve2x2( - Rs, - Xqpp, - -Xdpp, - Rs, - psi2q - Vd, - psi2d - Vq, - locations.destLoc[0], - locations.destLoc[1]); + Rs, Xqpp, -Xdpp, Rs, psi2q - Vd, psi2d - Vq, locations.destLoc[0], locations.destLoc[1]); m_output = -(locations.destLoc[1] * Vq + locations.destLoc[0] * Vd); } @@ -208,10 +197,8 @@ void GenModelGENROU::derivative(const IOdata& inputs, double* dstate = locations.destDiffLoc; const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); - const double psi2q = - coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; - const double psi2d = - coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; + const double psi2q = coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; const double fluxMagnitude = std::hypot(psi2d, psi2q); const double saturation = sat.compute(fluxMagnitude); @@ -223,14 +210,12 @@ void GenModelGENROU::derivative(const IOdata& inputs, dstate[1] = 0.5 * (mechanicalPower - electricalTorque - D * (state[1] - 1.0)) / H; dstate[2] = (-state[2] - - (Xq - Xqp) * - (coefficients.mGq2 * (state[2] - state[4]) + coefficients.mGq1 * alg[1]) - + (Xq - Xqp) * (coefficients.mGq2 * (state[2] - state[4]) + coefficients.mGq1 * alg[1]) - saturation * coefficients.mGqd * psi2q) / Tqop; dstate[3] = (fieldVoltage - state[3] + - (Xd - Xdp) * - (coefficients.mGd1 * alg[0] + coefficients.mGd2 * (state[5] - state[3])) - + (Xd - Xdp) * (coefficients.mGd1 * alg[0] + coefficients.mGd2 * (state[5] - state[3])) - saturation * psi2d) / Tdop; dstate[4] = (-state[4] + state[2] - (Xqp - Xl) * alg[1]) / Tqopp; @@ -252,10 +237,8 @@ void GenModelGENROU::residual(const IOdata& inputs, const auto coefficients = computeCoefficients(Xd, Xq, Xdp, Xqp, Xdpp, Xqpp, Xl); if (hasAlgebraic(sMode)) { - const double psi2q = - coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; - const double psi2d = - coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; + const double psi2q = coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; algResidual[0] = Vd + Rs * alg[0] + Xqpp * alg[1] - psi2q; algResidual[1] = Vq + Rs * alg[1] - Xdpp * alg[0] - psi2d; } @@ -318,31 +301,22 @@ void GenModelGENROU::jacobianElements(const IOdata& inputs, const double inverseTwoH = 0.5 / H; if (hasAlgebraic(sMode)) { matrixData.assign(refDiff + 1, refAlg, -inverseTwoH * (Vd + 2.0 * Rs * alg[0])); - matrixData.assign(refDiff + 1, - refAlg + 1, - -inverseTwoH * (Vq + 2.0 * Rs * alg[1])); + matrixData.assign(refDiff + 1, refAlg + 1, -inverseTwoH * (Vq + 2.0 * Rs * alg[1])); } matrixData.assign(refDiff + 1, refDiff, inverseTwoH * (Vq * alg[0] - Vd * alg[1])); matrixData.assign(refDiff + 1, refDiff + 1, -inverseTwoH * D - stateData.cj); - matrixData.assignCheckCol(refDiff + 1, - inputLocs[genModelPmechInLocation], - inverseTwoH); + matrixData.assignCheckCol(refDiff + 1, inputLocs[genModelPmechInLocation], inverseTwoH); if (angleLoc != kNullLocation) { - matrixData.assign(refDiff + 1, - angleLoc, - -inverseTwoH * (Vq * alg[0] - Vd * alg[1])); + matrixData.assign(refDiff + 1, angleLoc, -inverseTwoH * (Vq * alg[0] - Vd * alg[1])); } if (voltageLoc != kNullLocation) { matrixData.assign(refDiff + 1, voltageLoc, - -inverseTwoH * (Vd * alg[0] + Vq * alg[1]) / - inputs[VOLTAGE_IN_LOCATION]); + -inverseTwoH * (Vd * alg[0] + Vq * alg[1]) / inputs[VOLTAGE_IN_LOCATION]); } - const double psi2q = - coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; - const double psi2d = - coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; + const double psi2q = coefficients.mGq1 * state[2] + (1.0 - coefficients.mGq1) * state[4]; + const double psi2d = coefficients.mGd1 * state[3] + (1.0 - coefficients.mGd1) * state[5]; const double fluxMagnitude = std::hypot(psi2d, psi2q); const auto saturation = sat.evaluate(fluxMagnitude); double dSaturationD = 0.0; @@ -357,53 +331,31 @@ void GenModelGENROU::jacobianElements(const IOdata& inputs, const double dDSatQ = psi2d * dSaturationQ; if (hasAlgebraic(sMode)) { - matrixData.assign(refDiff + 2, - refAlg + 1, - -(Xq - Xqp) * coefficients.mGq1 / Tqop); + matrixData.assign(refDiff + 2, refAlg + 1, -(Xq - Xqp) * coefficients.mGq1 / Tqop); } matrixData.assign(refDiff + 2, refDiff + 2, - (-1.0 - (Xq - Xqp) * coefficients.mGq2 - - dQSatQ * coefficients.mGq1) / - Tqop - + (-1.0 - (Xq - Xqp) * coefficients.mGq2 - dQSatQ * coefficients.mGq1) / Tqop - stateData.cj); matrixData.assign(refDiff + 2, refDiff + 4, - ((Xq - Xqp) * coefficients.mGq2 - - dQSatQ * (1.0 - coefficients.mGq1)) / - Tqop); - matrixData.assign(refDiff + 2, - refDiff + 3, - -dQSatD * coefficients.mGd1 / Tqop); - matrixData.assign(refDiff + 2, - refDiff + 5, - -dQSatD * (1.0 - coefficients.mGd1) / Tqop); + ((Xq - Xqp) * coefficients.mGq2 - dQSatQ * (1.0 - coefficients.mGq1)) / Tqop); + matrixData.assign(refDiff + 2, refDiff + 3, -dQSatD * coefficients.mGd1 / Tqop); + matrixData.assign(refDiff + 2, refDiff + 5, -dQSatD * (1.0 - coefficients.mGd1) / Tqop); if (hasAlgebraic(sMode)) { - matrixData.assign(refDiff + 3, - refAlg, - (Xd - Xdp) * coefficients.mGd1 / Tdop); + matrixData.assign(refDiff + 3, refAlg, (Xd - Xdp) * coefficients.mGd1 / Tdop); } matrixData.assign(refDiff + 3, refDiff + 3, - (-1.0 - (Xd - Xdp) * coefficients.mGd2 - - dDSatD * coefficients.mGd1) / - Tdop - + (-1.0 - (Xd - Xdp) * coefficients.mGd2 - dDSatD * coefficients.mGd1) / Tdop - stateData.cj); matrixData.assign(refDiff + 3, refDiff + 5, - ((Xd - Xdp) * coefficients.mGd2 - - dDSatD * (1.0 - coefficients.mGd1)) / - Tdop); - matrixData.assign(refDiff + 3, - refDiff + 2, - -dDSatQ * coefficients.mGq1 / Tdop); - matrixData.assign(refDiff + 3, - refDiff + 4, - -dDSatQ * (1.0 - coefficients.mGq1) / Tdop); - matrixData.assignCheckCol(refDiff + 3, - inputLocs[genModelEftInLocation], - 1.0 / Tdop); + ((Xd - Xdp) * coefficients.mGd2 - dDSatD * (1.0 - coefficients.mGd1)) / Tdop); + matrixData.assign(refDiff + 3, refDiff + 2, -dDSatQ * coefficients.mGq1 / Tdop); + matrixData.assign(refDiff + 3, refDiff + 4, -dDSatQ * (1.0 - coefficients.mGq1) / Tdop); + matrixData.assignCheckCol(refDiff + 3, inputLocs[genModelEftInLocation], 1.0 / Tdop); if (hasAlgebraic(sMode)) { matrixData.assign(refDiff + 4, refAlg + 1, -(Xqp - Xl) / Tqopp); @@ -438,8 +390,8 @@ void GenModelGENROU::set(std::string_view param, double val, units::unit unitTyp } } -static const stringVec GEN_MODEL_GENROU_NAMES{ - "id", "iq", "delta", "freq", "e1d", "e1q", "e2q", "e2d"}; +static const stringVec + GEN_MODEL_GENROU_NAMES{"id", "iq", "delta", "freq", "e1d", "e1q", "e2q", "e2d"}; stringVec GenModelGENROU::localStateNames() const { diff --git a/src/utilities/Saturation.cpp b/src/utilities/Saturation.cpp index 4c6517384..7606a640b 100644 --- a/src/utilities/Saturation.cpp +++ b/src/utilities/Saturation.cpp @@ -100,8 +100,8 @@ void Saturation::setParam(double firstInput, } break; case SaturationType::SCALED_QUADRATIC: case SaturationType::CUTOFF_SCALED_QUADRATIC: { - if ((firstInput <= 0.0) || (secondInput <= 0.0) || - (firstSaturation <= 0.0) || (secondSaturation <= 0.0)) { + if ((firstInput <= 0.0) || (secondInput <= 0.0) || (firstSaturation <= 0.0) || + (secondSaturation <= 0.0)) { A = 0.0; B = 0.0; break; @@ -116,9 +116,7 @@ void Saturation::setParam(double firstInput, } A = -(secondInput * ssv - firstInput) / fitDenominator; const double distance = firstInput - A; - B = (std::abs(distance) < 1e-12) ? - 0.0 : - firstSaturation / (distance * distance); + B = (std::abs(distance) < 1e-12) ? 0.0 : firstSaturation / (distance * distance); } break; case SaturationType::EXPONENTIAL: A = log(firstSaturation / secondSaturation) / log(firstInput / secondInput); From 64fc03d7c8b00ff221f3f45b471e7c42e728f968 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 20 Aug 2026 19:18:14 -0700 Subject: [PATCH 05/12] more clang-tidy and doc fixes --- src/griddyn/genmodels/GenModelGENROU.h | 30 +++++++++++--------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index 3303b97c9..24d391278 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -136,18 +136,14 @@ namespace griddyn::genmodels { * power, torque, or rotor motion. * * @par Equation sources - * - ANDES - * v2.0 GENROU equations and initialization - * - ANDES - * v2.0 synchronous-generator base equations - * - OpenIPSL - * PSS/E GENROU initialization reference - * - ANDES - * GENROU verification against PSS/E, OpenIPSL, and TSAT + * - + * ANDES v2.0 GENROU equations and initialization + * - + * ANDES v2.0 synchronous-generator base equations + * - + * OpenIPSL PSS/E GENROU initialization reference + * - + * ANDES GENROU verification against PSS/E, OpenIPSL, and TSAT * - H. Cui, F. Li, and K. Tomsovic, "Hybrid Symbolic-Numeric Framework for * Power System Modeling and Analysis," IEEE Transactions on Power Systems, * 36(2), 1373-1384, 2021, @@ -180,20 +176,20 @@ class GenModelGENROU: public GenModel5 { virtual stringVec localStateNames() const override; // dynamics virtual void residual(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double resid[], const SolverMode& sMode) override; virtual void derivative(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double deriv[], const SolverMode& sMode) override; virtual void jacobianElements(const IOdata& inputs, - const StateData& sD, - MatrixData& md, + const StateData& stateData, + MatrixData& matrixData, const IOlocs& inputLocs, const SolverMode& sMode) override; virtual void algebraicUpdate(const IOdata& inputs, - const StateData& sD, + const StateData& stateData, double update[], const SolverMode& sMode, double alpha) override; From 64d79076ec1e7fe824b4b9bba1e18cb141e1a6c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:19:48 +0000 Subject: [PATCH 06/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/griddyn/genmodels/GenModelGENROU.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index 24d391278..147881c05 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -136,13 +136,17 @@ namespace griddyn::genmodels { * power, torque, or rotor motion. * * @par Equation sources - * - + * - * ANDES v2.0 GENROU equations and initialization - * - + * - * ANDES v2.0 synchronous-generator base equations - * - + * - * OpenIPSL PSS/E GENROU initialization reference - * - + * - * ANDES GENROU verification against PSS/E, OpenIPSL, and TSAT * - H. Cui, F. Li, and K. Tomsovic, "Hybrid Symbolic-Numeric Framework for * Power System Modeling and Analysis," IEEE Transactions on Power Systems, From 9f028337dc83b4affd04691833e05a5ef5fc8639 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 21 Aug 2026 06:14:25 -0700 Subject: [PATCH 07/12] more tests and fixes --- src/fileInput/readSimulationElement.cpp | 104 +++++++++--------- src/griddyn/genmodels/GenModelGENROU.h | 18 +-- src/griddyn/links/VSCShunt.h | 2 + test/CMakeLists.txt | 1 + test/libraryTests/testSolverReader.cpp | 31 ++++++ .../xml_tests/test_named_default_solver.xml | 6 + 6 files changed, 102 insertions(+), 60 deletions(-) create mode 100644 test/libraryTests/testSolverReader.cpp create mode 100644 test/test_files/xml_tests/test_named_default_solver.xml diff --git a/src/fileInput/readSimulationElement.cpp b/src/fileInput/readSimulationElement.cpp index 48d721cf5..5223b682d 100644 --- a/src/fileInput/readSimulationElement.cpp +++ b/src/fileInput/readSimulationElement.cpp @@ -15,7 +15,7 @@ namespace griddyn { using readerConfig::defMatchType; namespace { - void loadDefaultObjectTranslations(ReaderInfo& ReaderInformation); + void loadDefaultObjectTranslations(ReaderInfo& readerInformation); const IgnoreListType& simIgnoreFields() { @@ -31,24 +31,24 @@ static constexpr char libstring[] = "library"; // CoreObject * readSimXMLFile(const std::string &fileName, CoreObject *gco, const std::string // prefix, ReaderInfo *ri) const GridSimulation* readSimulationElement(std::shared_ptr& element, - ReaderInfo& ReaderInformation, + ReaderInfo& readerInformation, CoreObject* searchObject, GridSimulation* simulationObject) { // pointers const bool isMaster = isMasterObject(searchObject, simulationObject); - auto riScope = ReaderInformation.newScope(); + auto riScope = readerInformation.newScope(); - loadDefines(element, ReaderInformation); - loadDirectories(element, ReaderInformation); + loadDefines(element, readerInformation); + loadDirectories(element, readerInformation); if (isMaster) { - loadDefaultObjectTranslations(ReaderInformation); + loadDefaultObjectTranslations(readerInformation); } - loadTranslations(element, ReaderInformation); - loadCustomSections(element, ReaderInformation); + loadTranslations(element, readerInformation); + loadCustomSections(element, readerInformation); GridSimulation* simulation = elementReaderSetup( - element, simulationObject, "simulation", ReaderInformation, searchObject); + element, simulationObject, "simulation", readerInformation, searchObject); // load the simulation name and id const std::string simulationName = getElementField(element, "name", defMatchType); @@ -60,16 +60,16 @@ GridSimulation* readSimulationElement(std::shared_ptr& element, if (!versionString.empty()) { simulation->set("version", versionString); } - setIndex(element, simulation, ReaderInformation); + setIndex(element, simulation, readerInformation); // load any other attributes objSetAttributes( - simulation, element, simulation->getName(), ReaderInformation, simIgnoreFields()); + simulation, element, simulation->getName(), readerInformation, simIgnoreFields()); if (isMaster) { const std::string basePowerText = getElementField(element, "basepower", defMatchType); if (!basePowerText.empty()) { - const double basePowerValue = interpretString(basePowerText, ReaderInformation); - ReaderInformation.base = basePowerValue; + const double basePowerValue = interpretString(basePowerText, readerInformation); + readerInformation.base = basePowerValue; simulation->set("basepower", basePowerValue); } } @@ -77,30 +77,32 @@ GridSimulation* readSimulationElement(std::shared_ptr& element, if (element->hasElement(libstring)) { element->moveToFirstChild(libstring); while (element->isValid()) { - readLibraryElement(element, ReaderInformation); + readLibraryElement(element, readerInformation); element->moveToNextSibling(libstring); } element->moveToParent(); } - readImports(element, ReaderInformation, simulation, false); + readImports(element, readerInformation, simulation, false); // load all other objects besides bus and area - loadSubObjects(element, ReaderInformation, simulation); - - paramLoopElement( - simulation, element, simulation->getName(), ReaderInformation, simIgnoreFields()); - - // read imports marked final - readImports(element, ReaderInformation, simulation, true); + loadSubObjects(element, readerInformation, simulation); + // Solvers must be registered before simulation parameters are applied because + // defdae, defpowerflow, defdynalg, and defdyndiff can refer to them by name. element->moveToFirstChild("solver"); while (element->isValid()) { - loadSolverElement(element, ReaderInformation, dynamic_cast(simulation)); + loadSolverElement(element, readerInformation, dynamic_cast(simulation)); element->moveToNextSibling("solver"); } element->moveToParent(); + paramLoopElement( + simulation, element, simulation->getName(), readerInformation, simIgnoreFields()); + + // read imports marked final + readImports(element, readerInformation, simulation, true); + if (isMaster) { const int busCount = simulation->getInt("totalbuscount"); const int linkCount = simulation->getInt("totallinkcount"); @@ -108,15 +110,15 @@ GridSimulation* readSimulationElement(std::shared_ptr& element, LEVELPRINT(READER_NORMAL_PRINT, "loaded Power simulation " << simulation->getName()); LEVELPRINT(READER_SUMMARY_PRINT, "Summary: " << busCount << " buses Loaded "); LEVELPRINT(READER_SUMMARY_PRINT, "Summary: " << linkCount << " links Loaded "); - if (!ReaderInformation.collectors.empty()) { + if (!readerInformation.collectors.empty()) { LEVELPRINT(READER_SUMMARY_PRINT, - "Summary: " << ReaderInformation.collectors.size() << " collectors Loaded "); + "Summary: " << readerInformation.collectors.size() << " collectors Loaded "); } - if (!ReaderInformation.events.empty()) { + if (!readerInformation.events.empty()) { LEVELPRINT(READER_SUMMARY_PRINT, - "Summary: " << ReaderInformation.events.size() << " events Loaded "); + "Summary: " << readerInformation.events.size() << " events Loaded "); } - for (auto& col : ReaderInformation.collectors) { + for (auto& col : readerInformation.collectors) { auto* owner = col->getOwner(); if (owner != nullptr) { try { @@ -133,37 +135,37 @@ GridSimulation* readSimulationElement(std::shared_ptr& element, } } // add the events - simulation->add(ReaderInformation.events); + simulation->add(readerInformation.events); } - ReaderInformation.closeScope(riScope); + readerInformation.closeScope(riScope); return simulation; } namespace { - void loadDefaultObjectTranslations(ReaderInfo& ReaderInformation) + void loadDefaultObjectTranslations(ReaderInfo& readerInformation) { - ReaderInformation.addTranslate("fuse", "relay"); - ReaderInformation.addTranslate("breaker", "relay"); - ReaderInformation.addTranslate("sensor", "relay"); - ReaderInformation.addTranslate("control", "relay"); - ReaderInformation.addTranslate("pmu", "relay"); - ReaderInformation.addTranslate("controlblock", "block"); - ReaderInformation.addTranslate("model", "genmodel"); - ReaderInformation.addTranslate("gen", "generator"); - ReaderInformation.addTranslate("transformer", "link"); - ReaderInformation.addTranslate("line", "link"); - ReaderInformation.addTranslate("tie", "link"); - ReaderInformation.addTranslate("subsystem", "link"); - ReaderInformation.addTranslate("busmodify", "bus"); - ReaderInformation.addTranslate("areamodify", "area"); - ReaderInformation.addTranslate("linkmodify", "link"); - ReaderInformation.addTranslate("gov", "governor"); - ReaderInformation.addTranslate("recorder", "collector"); - ReaderInformation.addTranslate("player", "event"); - ReaderInformation.addTranslate("scenario", "event"); - ReaderInformation.addTranslate("loop", "array"); + readerInformation.addTranslate("fuse", "relay"); + readerInformation.addTranslate("breaker", "relay"); + readerInformation.addTranslate("sensor", "relay"); + readerInformation.addTranslate("control", "relay"); + readerInformation.addTranslate("pmu", "relay"); + readerInformation.addTranslate("controlblock", "block"); + readerInformation.addTranslate("model", "genmodel"); + readerInformation.addTranslate("gen", "generator"); + readerInformation.addTranslate("transformer", "link"); + readerInformation.addTranslate("line", "link"); + readerInformation.addTranslate("tie", "link"); + readerInformation.addTranslate("subsystem", "link"); + readerInformation.addTranslate("busmodify", "bus"); + readerInformation.addTranslate("areamodify", "area"); + readerInformation.addTranslate("linkmodify", "link"); + readerInformation.addTranslate("gov", "governor"); + readerInformation.addTranslate("recorder", "collector"); + readerInformation.addTranslate("player", "event"); + readerInformation.addTranslate("scenario", "event"); + readerInformation.addTranslate("loop", "array"); } bool isMasterObject(const CoreObject* searchObject, const GridSimulation* simulationObject) diff --git a/src/griddyn/genmodels/GenModelGENROU.h b/src/griddyn/genmodels/GenModelGENROU.h index 24d391278..d4dad8415 100644 --- a/src/griddyn/genmodels/GenModelGENROU.h +++ b/src/griddyn/genmodels/GenModelGENROU.h @@ -136,18 +136,18 @@ namespace griddyn::genmodels { * power, torque, or rotor motion. * * @par Equation sources - * - - * ANDES v2.0 GENROU equations and initialization - * - - * ANDES v2.0 synchronous-generator base equations - * - - * OpenIPSL PSS/E GENROU initialization reference - * - - * ANDES GENROU verification against PSS/E, OpenIPSL, and TSAT + * - ANDES v2.0 GENROU equations and initialization: + * https://github.com/CURRENT/andes/blob/eda5163c9ee8d19945a1dd5d1771fec5da608c27/andes/models/synchronous/genrou.py + * - ANDES v2.0 synchronous-generator base equations: + * https://github.com/CURRENT/andes/blob/eda5163c9ee8d19945a1dd5d1771fec5da608c27/andes/models/synchronous/genbase.py + * - OpenIPSL PSS/E GENROU initialization reference: + * https://github.com/OpenIPSL/OpenIPSL/blob/master/OpenIPSL/Electrical/Machines/PSSE/GENROU.mo + * - ANDES GENROU verification against PSS/E, OpenIPSL, and TSAT: + * https://docs.andes.app/en/v1.8.10/_examples/verification/andes-ieee14-verification.html * - H. Cui, F. Li, and K. Tomsovic, "Hybrid Symbolic-Numeric Framework for * Power System Modeling and Analysis," IEEE Transactions on Power Systems, * 36(2), 1373-1384, 2021, - * doi:10.1109/TPWRS.2020.3017019. + * https://doi.org/10.1109/TPWRS.2020.3017019. * * @note PSS/E requires the GENROU d- and q-axis subtransient reactances to be * equal. The class retains separate \c Xdpp and \c Xqpp storage to fit the diff --git a/src/griddyn/links/VSCShunt.h b/src/griddyn/links/VSCShunt.h index 4f2660a80..47663cf54 100644 --- a/src/griddyn/links/VSCShunt.h +++ b/src/griddyn/links/VSCShunt.h @@ -77,11 +77,13 @@ namespace links { void updateLocalCache(const IOdata& inputs, const StateData& stateDataValue, const SolverMode& sMode) override; + using AcDcConverter::ioPartialDerivatives; void ioPartialDerivatives(id_type_t busId, const StateData& stateDataValue, MatrixData& matrixDataValue, const IOlocs& inputLocs, const SolverMode& sMode) override; + using AcDcConverter::outputPartialDerivatives; void outputPartialDerivatives(id_type_t busId, const StateData& stateDataValue, MatrixData& matrixDataValue, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 797321c32..28487510b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -39,6 +39,7 @@ set(file_reader_gtest_sources gtestHelperFunctions.cpp libraryTests/testReaderInfo.cpp libraryTests/testExampleReaders.cpp + libraryTests/testSolverReader.cpp libraryTests/testXML.cpp libraryTests/testJsonReader.cpp libraryTests/testElementReaders.cpp diff --git a/test/libraryTests/testSolverReader.cpp b/test/libraryTests/testSolverReader.cpp new file mode 100644 index 000000000..06b95fa91 --- /dev/null +++ b/test/libraryTests/testSolverReader.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014-2026, Lawrence Livermore National Security + * See the top-level NOTICE for additional details. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "../gtestHelper.h" +#include +#include +#include + +namespace griddyn { +namespace { + constexpr std::string_view xmlTestDirectory{GRIDDYN_TEST_DIRECTORY "/xml_tests/"}; + + class SolverReaderTests: public GridDynSimulationTestFixture, public ::testing::Test {}; + + TEST_F(SolverReaderTests, NamedSolverCanBeSelectedAsDefault) + { + const std::string fileName = + std::string{xmlTestDirectory} + "test_named_default_solver.xml"; + gds = readSimXMLFile(fileName); + + ASSERT_NE(gds, nullptr); + const auto namedSolver = gds->getSolverInterface("basic"); + ASSERT_NE(namedSolver, nullptr); + EXPECT_EQ(gds->getSolverInterface("powerflow"), namedSolver); + EXPECT_EQ(readerConfig::warnCount, 0); + } +} // namespace +} // namespace griddyn diff --git a/test/test_files/xml_tests/test_named_default_solver.xml b/test/test_files/xml_tests/test_named_default_solver.xml new file mode 100644 index 000000000..6da582097 --- /dev/null +++ b/test/test_files/xml_tests/test_named_default_solver.xml @@ -0,0 +1,6 @@ + + + + + basic + From 736e2bee463b398fe4846a84a3fac0a88e4af78d Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 21 Aug 2026 12:02:26 -0700 Subject: [PATCH 08/12] braid fixes --- .github/workflows/braid-build.yml | 25 +++++++++++++++++++------ config/cmake/FindLAPACKE.cmake | 2 +- src/extraSolvers/CMakeLists.txt | 9 ++++++--- src/gridDynMain/gridDynMain.cpp | 4 +++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/workflows/braid-build.yml b/.github/workflows/braid-build.yml index 7ac386477..2216d019a 100644 --- a/.github/workflows/braid-build.yml +++ b/.github/workflows/braid-build.yml @@ -27,18 +27,31 @@ jobs: with: submodules: recursive + - name: Install Braid dependencies + shell: bash + run: | + apt-get update + apt-get install -y --no-install-recommends liblapacke-dev + - name: Build XBraID shell: bash - run: ./scripts/xbraid_build.sh + run: | + ./scripts/xbraid_build.sh + test -f "${GITHUB_WORKSPACE}/../xbraid/braid/braid.h" + test -f "${GITHUB_WORKSPACE}/../xbraid/braid/libbraid.a" - name: Configure shell: bash run: | - mkdir -p build - cd build - eval cmake .. ${CMAKE_FLAGS} - env: - CMAKE_FLAGS: -DGRIDDYN_ENABLE_MPI=ON -DGRIDDYN_ENABLE_OPENMP=OFF -DGRIDDYN_ENABLE_EXTRA_SOLVERS=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_BRAID_SOLVER=ON -DBRAID_INSTALL_DIR=${{ github.workspace }}/../xbraid/braid + cmake -S . -B build \ + -DGRIDDYN_ENABLE_MPI=ON \ + -DGRIDDYN_ENABLE_OPENMP=OFF \ + -DGRIDDYN_ENABLE_EXTRA_SOLVERS=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_BRAID_SOLVER=ON \ + "-DBRAID_INSTALL_DIR=${GITHUB_WORKSPACE}/../xbraid/braid" + grep -Fq "loadExtraSolvers();" build/src/gridDynLoader/libraryLoader.cpp - name: Build shell: bash diff --git a/config/cmake/FindLAPACKE.cmake b/config/cmake/FindLAPACKE.cmake index a7dca90dc..588e480ec 100644 --- a/config/cmake/FindLAPACKE.cmake +++ b/config/cmake/FindLAPACKE.cmake @@ -145,7 +145,7 @@ find_package_handle_standard_args( ) if(LAPACKE_FOUND) - set(LAPACKE_INCLUDE_DIRS ${LAPACKE_CBLAS_INCLUDE_DIR} ${LAPACKE_CBLAS_INCLUDE_DIR}) + set(LAPACKE_INCLUDE_DIRS ${LAPACKE_CBLAS_INCLUDE_DIR} ${LAPACKE_LAPACKE_INCLUDE_DIR}) list(REMOVE_DUPLICATES LAPACKE_INCLUDE_DIRS) if("${CMAKE_C_COMPILER_ID}" MATCHES ".*Clang.*" OR "${CMAKE_C_COMPILER_ID}" MATCHES ".*GNU.*" OR "${CMAKE_C_COMPILER_ID}" MATCHES ".*Intel.*" diff --git a/src/extraSolvers/CMakeLists.txt b/src/extraSolvers/CMakeLists.txt index 0fb6b6214..ecd3ab967 100644 --- a/src/extraSolvers/CMakeLists.txt +++ b/src/extraSolvers/CMakeLists.txt @@ -152,11 +152,14 @@ endif() if(BUILD_BRAID_SOLVER) find_package(Braid QUIET) if(NOT GRIDDYN_ENABLE_MPI) - message(WARNING "Braid requires MPI support; skipping Braid solver") + message(FATAL_ERROR "Braid was requested but GRIDDYN_ENABLE_MPI is OFF") elseif(NOT BRAID_FOUND) - message(WARNING "Braid solver requested but Braid was not found; skipping Braid solver") + message( + FATAL_ERROR + "Braid was requested but was not found; set BRAID_INSTALL_DIR to the directory containing braid.h and libbraid" + ) elseif(NOT LAPACKE_FOUND) - message(WARNING "Braid solver requested but LAPACKE was not found; skipping Braid solver") + message(FATAL_ERROR "Braid was requested but LAPACKE was not found") else() if(NOT GRIDDYN_ENABLE_PARADAE_SOLVERS) list(APPEND extra_solver_sources ${paradae_sources}) diff --git a/src/gridDynMain/gridDynMain.cpp b/src/gridDynMain/gridDynMain.cpp index 85acd07fd..11cb8623e 100644 --- a/src/gridDynMain/gridDynMain.cpp +++ b/src/gridDynMain/gridDynMain.cpp @@ -12,6 +12,7 @@ #include "runner/gridDynRunner.h" #include #include +#include #include #include #ifdef ENABLE_HELICS_EXECUTABLE @@ -203,7 +204,8 @@ int main(int argc, char* argv[]) return 0; } - catch (const std::exception&) { + catch (const std::exception& error) { + std::cerr << "GridDyn execution failed: " << error.what() << '\n'; return -5; } } From e36a39cdedc27cd2537085c1635f132a8a69fdc2 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 21 Aug 2026 13:41:39 -0700 Subject: [PATCH 09/12] update lapacke find --- config/cmake/FindLAPACKE.cmake | 43 +++++++-------------------------- src/extraSolvers/CMakeLists.txt | 6 ----- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/config/cmake/FindLAPACKE.cmake b/config/cmake/FindLAPACKE.cmake index 588e480ec..a2fb793d5 100644 --- a/config/cmake/FindLAPACKE.cmake +++ b/config/cmake/FindLAPACKE.cmake @@ -65,13 +65,8 @@ if(LAPACKE_FOUND) endif() # ################################################################################################## -# First search for headers -find_path( - LAPACKE_CBLAS_INCLUDE_DIR - NAMES cblas.h - PATHS ${LAPACKE_SEARCH_PATHS} - PATH_SUFFIXES include include/lapack -) +# First search for headers. GridDyn uses the LAPACKE API directly and does not require a +# separately installed CBLAS header or library. find_path( LAPACKE_LAPACKE_INCLUDE_DIR NAMES lapacke.h @@ -88,24 +83,8 @@ find_library( PATHS ${LAPACKE_SEARCH_PATHS} PATH_SUFFIXES ${PATH_SUFFIXES_LIST} ) -find_library( - CBLAS_LIB - NAMES cblas - PATHS ${LAPACKE_SEARCH_PATHS} - PATH_SUFFIXES ${PATH_SUFFIXES_LIST} -) -find_library( - LAPACK_LIB - NAMES lapack - PATHS ${LAPACKE_SEARCH_PATHS} - PATH_SUFFIXES ${PATH_SUFFIXES_LIST} -) -find_library( - BLAS_LIB - NAMES blas - PATHS ${LAPACKE_SEARCH_PATHS} - PATH_SUFFIXES ${PATH_SUFFIXES_LIST} -) +find_package(LAPACK QUIET) +find_package(BLAS QUIET) # TODO: Get version components # ------------------------------------------------------------------------ @@ -135,18 +114,15 @@ find_package_handle_standard_args( LAPACKE FOUND_VAR LAPACKE_FOUND REQUIRED_VARS - LAPACKE_CBLAS_INCLUDE_DIR LAPACKE_LAPACKE_INCLUDE_DIR LAPACKE_LIB - LAPACK_LIB - CBLAS_LIB - BLAS_LIB + LAPACK_FOUND + BLAS_FOUND VERSION_VAR LAPACKE_VERSION_STRING ) if(LAPACKE_FOUND) - set(LAPACKE_INCLUDE_DIRS ${LAPACKE_CBLAS_INCLUDE_DIR} ${LAPACKE_LAPACKE_INCLUDE_DIR}) - list(REMOVE_DUPLICATES LAPACKE_INCLUDE_DIRS) + set(LAPACKE_INCLUDE_DIRS ${LAPACKE_LAPACKE_INCLUDE_DIR}) if("${CMAKE_C_COMPILER_ID}" MATCHES ".*Clang.*" OR "${CMAKE_C_COMPILER_ID}" MATCHES ".*GNU.*" OR "${CMAKE_C_COMPILER_ID}" MATCHES ".*Intel.*" ) # NOT MSVC @@ -156,9 +132,8 @@ if(LAPACKE_FOUND) APPEND LAPACKE_LIBRARIES ${LAPACKE_LIB} - ${LAPACK_LIB} - ${BLAS_LIB} - ${CBLAS_LIB} + ${LAPACK_LIBRARIES} + ${BLAS_LIBRARIES} ) # Check for a common combination, and find required gfortran support libraries diff --git a/src/extraSolvers/CMakeLists.txt b/src/extraSolvers/CMakeLists.txt index ecd3ab967..0e61e4bcf 100644 --- a/src/extraSolvers/CMakeLists.txt +++ b/src/extraSolvers/CMakeLists.txt @@ -133,12 +133,6 @@ set(GRIDDYN_ENABLE_BRAID_SOLVER OFF) if(BUILD_PARADAE_SOLVERS OR BUILD_BRAID_SOLVER) find_package(LAPACKE QUIET) - if(NOT LAPACKE_FOUND) - message( - WARNING - "PARADAE and Braid solvers require LAPACKE; extra solver support will be limited" - ) - endif() endif() if(BUILD_PARADAE_SOLVERS AND LAPACKE_FOUND) From 2565fde3325445e9edbb9629d3a3d8bdc3189d1a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:42:41 +0000 Subject: [PATCH 10/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/cmake/FindLAPACKE.cmake | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/config/cmake/FindLAPACKE.cmake b/config/cmake/FindLAPACKE.cmake index a2fb793d5..ef7117b5a 100644 --- a/config/cmake/FindLAPACKE.cmake +++ b/config/cmake/FindLAPACKE.cmake @@ -65,8 +65,8 @@ if(LAPACKE_FOUND) endif() # ################################################################################################## -# First search for headers. GridDyn uses the LAPACKE API directly and does not require a -# separately installed CBLAS header or library. +# First search for headers. GridDyn uses the LAPACKE API directly and does not require a separately +# installed CBLAS header or library. find_path( LAPACKE_LAPACKE_INCLUDE_DIR NAMES lapacke.h @@ -113,11 +113,7 @@ include(FindPackageHandleStandardArgs) find_package_handle_standard_args( LAPACKE FOUND_VAR LAPACKE_FOUND - REQUIRED_VARS - LAPACKE_LAPACKE_INCLUDE_DIR - LAPACKE_LIB - LAPACK_FOUND - BLAS_FOUND + REQUIRED_VARS LAPACKE_LAPACKE_INCLUDE_DIR LAPACKE_LIB LAPACK_FOUND BLAS_FOUND VERSION_VAR LAPACKE_VERSION_STRING ) @@ -128,13 +124,7 @@ if(LAPACKE_FOUND) ) # NOT MSVC set(MATH_LIB m) endif() - list( - APPEND - LAPACKE_LIBRARIES - ${LAPACKE_LIB} - ${LAPACK_LIBRARIES} - ${BLAS_LIBRARIES} - ) + list(APPEND LAPACKE_LIBRARIES ${LAPACKE_LIB} ${LAPACK_LIBRARIES} ${BLAS_LIBRARIES}) # Check for a common combination, and find required gfortran support libraries if(1) From 0bb5a37adbf850897f6ef6293533091f51f11a54 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 21 Aug 2026 15:19:39 -0700 Subject: [PATCH 11/12] more braid fixes, but turn off the ci job for now --- .github/workflows/braid-build.yml | 6 ------ src/extraSolvers/braid/braidSolver.cpp | 1 - src/extraSolvers/braid/braid_driver.cxx | 8 ++++++++ src/extraSolvers/paradae/common/Timer.cxx | 1 + 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/braid-build.yml b/.github/workflows/braid-build.yml index 2216d019a..63fd9959b 100644 --- a/.github/workflows/braid-build.yml +++ b/.github/workflows/braid-build.yml @@ -1,12 +1,6 @@ name: Braid Build on: - push: - branches: - - main - pull_request: - branches: - - main workflow_dispatch: permissions: diff --git a/src/extraSolvers/braid/braidSolver.cpp b/src/extraSolvers/braid/braidSolver.cpp index c4cc45d1f..9ab97dd16 100644 --- a/src/extraSolvers/braid/braidSolver.cpp +++ b/src/extraSolvers/braid/braidSolver.cpp @@ -30,7 +30,6 @@ #include namespace griddyn::braid { -using griddyn::paradae::BackwardDiff; using griddyn::paradae::BDF; using griddyn::paradae::Equation; using griddyn::paradae::EquationGridDyn; diff --git a/src/extraSolvers/braid/braid_driver.cxx b/src/extraSolvers/braid/braid_driver.cxx index 0778340a3..e0b859ebe 100644 --- a/src/extraSolvers/braid/braid_driver.cxx +++ b/src/extraSolvers/braid/braid_driver.cxx @@ -21,13 +21,21 @@ #endif using griddyn::paradae::BackwardDiff; +using griddyn::paradae::BDF; using griddyn::paradae::DATA_Struct; using griddyn::paradae::ODE; +using griddyn::paradae::ODEProblem; using griddyn::paradae::PVector; using griddyn::paradae::RCODE; using griddyn::paradae::Real; using griddyn::paradae::SMultiVector; using griddyn::paradae::SVector; +using griddyn::paradae::WARN_ROOT; +using griddyn::paradae::iVan_2; +using griddyn::paradae::iVan_3; +using griddyn::paradae::iVan_4; +using griddyn::paradae::iVan_5; +using griddyn::paradae::iVan_6; using std::abort; using std::abs; using std::ceil; diff --git a/src/extraSolvers/paradae/common/Timer.cxx b/src/extraSolvers/paradae/common/Timer.cxx index da0eaf108..ebc8684f7 100644 --- a/src/extraSolvers/paradae/common/Timer.cxx +++ b/src/extraSolvers/paradae/common/Timer.cxx @@ -17,6 +17,7 @@ using std::cout; using std::endl; using std::ostringstream; +using std::pair; using std::printf; using std::string; From fe6dde860eb22b80b4ee78cad9a2c2435f0b4576 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:21:02 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/extraSolvers/braid/braid_driver.cxx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/extraSolvers/braid/braid_driver.cxx b/src/extraSolvers/braid/braid_driver.cxx index e0b859ebe..99a2be2b9 100644 --- a/src/extraSolvers/braid/braid_driver.cxx +++ b/src/extraSolvers/braid/braid_driver.cxx @@ -23,6 +23,11 @@ using griddyn::paradae::BackwardDiff; using griddyn::paradae::BDF; using griddyn::paradae::DATA_Struct; +using griddyn::paradae::iVan_2; +using griddyn::paradae::iVan_3; +using griddyn::paradae::iVan_4; +using griddyn::paradae::iVan_5; +using griddyn::paradae::iVan_6; using griddyn::paradae::ODE; using griddyn::paradae::ODEProblem; using griddyn::paradae::PVector; @@ -31,11 +36,6 @@ using griddyn::paradae::Real; using griddyn::paradae::SMultiVector; using griddyn::paradae::SVector; using griddyn::paradae::WARN_ROOT; -using griddyn::paradae::iVan_2; -using griddyn::paradae::iVan_3; -using griddyn::paradae::iVan_4; -using griddyn::paradae::iVan_5; -using griddyn::paradae::iVan_6; using std::abort; using std::abs; using std::ceil;