From 744ace7693296fe6c56b1334e7db8ea9f7740fe7 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 21 Oct 2026 04:37:13 -0700 Subject: [PATCH 1/6] add more capability to the aux reader --- src/fileInput/fileInput.h | 41 ++++++++++ src/fileInput/gridDynReadAUX.cpp | 109 +++++++++++++++++++++++++ test/libraryTests/testSolverReader.cpp | 51 ++++++++++++ 3 files changed, 201 insertions(+) diff --git a/src/fileInput/fileInput.h b/src/fileInput/fileInput.h index a3d0f0ee7..58583fdcb 100644 --- a/src/fileInput/fileInput.h +++ b/src/fileInput/fileInput.h @@ -12,7 +12,10 @@ define some functions and operations for configuring file reader operations and #include "../griddyn/gridDynDefinitions.hpp" #include "ReaderInfo.h" #include +#include #include +#include +#include namespace griddyn { class Event; @@ -114,6 +117,44 @@ void loadAux(CoreObject* parentObject, const std::string& fileName, const BasicReaderInfo& readerOptions = defInfo); +/** Named PowerWorld object whose numeric ID can be referenced by other AUX rows. */ +struct AuxNamedRecord { + int mNumber = 0; + std::string mName; +}; + +struct AuxSubstationRecord { + int mNumber = 0; + std::string mName; + std::optional mLatitude; + std::optional mLongitude; +}; + +/** Geographic and organizational references from an AUX Bus row. */ +struct AuxBusMetadata { + int mBusNumber = 0; + int mAreaNumber = 0; + int mZoneNumber = 0; + int mOwnerNumber = 0; + int mSubstationNumber = 0; + int mBalancingAuthorityNumber = 0; + std::optional mLatitude; + std::optional mLongitude; +}; + +/** Metadata that does not have a direct GridDyn simulation object. */ +struct AuxCaseMetadata { + std::unordered_map mOwners; + std::unordered_map mZones; + std::unordered_map mBalancingAuthorities; + std::unordered_map mSubstations; + std::unordered_map mBuses; + std::vector mSuperAreas; +}; + +/** Read PowerWorld case hierarchy and bus assignments without changing a simulation. */ +[[nodiscard]] AuxCaseMetadata readAuxCaseMetadata(const std::string& fileName); + void loadDyr(CoreObject* parentObject, const std::string& fileName, const BasicReaderInfo& readerOptions = defInfo); diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index ec94c3061..b18800bb3 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -142,6 +142,13 @@ namespace { } }; + [[nodiscard]] std::optional auxOptionalNumber( + const AuxRow& row, std::initializer_list names) + { + const auto value = row.number(names, std::numeric_limits::quiet_NaN()); + return std::isfinite(value) ? std::optional{value} : std::nullopt; + } + // AUX has both DATA (Object, [columns], ...) and concise Object (columns) headers. AuxSection parseAuxHeader(std::string_view header) { @@ -1091,4 +1098,106 @@ void loadAux(CoreObject* parentObject, finishAuxGeneratorControls(network); } +AuxCaseMetadata readAuxCaseMetadata(const std::string& fileName) +{ + AuxCaseMetadata metadata; + scanAuxFile( + fileName, + [](std::string_view type) { + return type == "owner" || type == "zone" || type == "substation" || + type == "superarea" || type == "balancingauthority" || type == "bus"; + }, + [&](const AuxRow& row) { + const auto& type = row.mSection.mType; + if (type == "owner") { + const int number = row.integer({"ownernum"}); + if (number <= 0) { + return; + } + auto& owner = metadata.mOwners[number]; + owner.mNumber = number; + if (auto name = trimAux(row.value({"ownername"})); !name.empty()) { + owner.mName = std::move(name); + } + } else if (type == "zone") { + const int number = row.integer({"zonenum"}); + if (number <= 0) { + return; + } + auto& zone = metadata.mZones[number]; + zone.mNumber = number; + if (auto name = trimAux(row.value({"zonename"})); !name.empty()) { + zone.mName = std::move(name); + } + } else if (type == "balancingauthority") { + const int number = row.integer({"banumber"}); + if (number <= 0) { + return; + } + auto& authority = metadata.mBalancingAuthorities[number]; + authority.mNumber = number; + if (auto name = trimAux(row.value({"baname"})); !name.empty()) { + authority.mName = std::move(name); + } + } else if (type == "substation") { + const int number = row.integer({"subnum"}); + if (number <= 0) { + return; + } + auto& substation = metadata.mSubstations[number]; + substation.mNumber = number; + if (auto name = trimAux(row.value({"subname"})); !name.empty()) { + substation.mName = std::move(name); + } + if (auto latitude = auxOptionalNumber(row, {"latitude"}); latitude.has_value()) { + substation.mLatitude = latitude; + } + if (auto longitude = auxOptionalNumber(row, {"longitude"}); + longitude.has_value()) { + substation.mLongitude = longitude; + } + } else if (type == "superarea") { + auto name = trimAux(row.value({"saname"})); + if (!name.empty() && std::find(metadata.mSuperAreas.begin(), + metadata.mSuperAreas.end(), name) == + metadata.mSuperAreas.end()) { + metadata.mSuperAreas.push_back(std::move(name)); + } + } else if (type == "bus") { + const int number = row.integer({"busnum"}); + if (number <= 0) { + return; + } + auto& bus = metadata.mBuses[number]; + bus.mBusNumber = number; + // A later Bus section may supply only a few fields. Retain + // assignments obtained from earlier sections of the same file. + if (row.has({"areanum"})) { + bus.mAreaNumber = row.integer({"areanum"}); + } + if (row.has({"zonenum"})) { + bus.mZoneNumber = row.integer({"zonenum"}); + } + if (row.has({"ownernum"})) { + bus.mOwnerNumber = row.integer({"ownernum"}); + } + if (row.has({"subnum"})) { + bus.mSubstationNumber = row.integer({"subnum"}); + } + if (row.has({"banumber"})) { + bus.mBalancingAuthorityNumber = row.integer({"banumber"}); + } + if (auto latitude = auxOptionalNumber(row, {"latitude:1", "latitude"}); + latitude.has_value()) { + bus.mLatitude = latitude; + } + if (auto longitude = auxOptionalNumber(row, {"longitude:1", "longitude"}); + longitude.has_value()) { + bus.mLongitude = longitude; + } + } + }); + return metadata; +} + } // namespace griddyn diff --git a/test/libraryTests/testSolverReader.cpp b/test/libraryTests/testSolverReader.cpp index 52a8d9904..27ed86bd6 100644 --- a/test/libraryTests/testSolverReader.cpp +++ b/test/libraryTests/testSolverReader.cpp @@ -5,6 +5,7 @@ */ #include "../gtestHelper.h" +#include "fileInput/fileInput.h" #include "fileInput/readerHelper.h" #include "griddyn/Generator.h" #include "griddyn/GridArea.h" @@ -228,6 +229,39 @@ namespace { EXPECT_EQ(gds->getInt("totallinkcount"), 2); } + TEST_F(SolverReaderTests, AuxCaseMetadataKeepsNamedReferencesAndBusAssignments) + { + const auto metadata = readAuxCaseMetadata( + std::string{inputTestDirectory} + "aux_case_metadata.aux"); + ASSERT_EQ(metadata.mOwners.size(), 1); + EXPECT_EQ(metadata.mOwners.at(7).mName, "First Owner"); + ASSERT_EQ(metadata.mZones.size(), 2); + EXPECT_EQ(metadata.mZones.at(4).mName, "Inland"); + ASSERT_EQ(metadata.mSubstations.size(), 1); + const auto& substation = metadata.mSubstations.at(20); + EXPECT_EQ(substation.mName, "North Yard"); + ASSERT_TRUE(substation.mLatitude.has_value()); + ASSERT_TRUE(substation.mLongitude.has_value()); + EXPECT_DOUBLE_EQ(*substation.mLatitude, 35.1); + EXPECT_DOUBLE_EQ(*substation.mLongitude, -118.1); + ASSERT_EQ(metadata.mSuperAreas.size(), 1); + EXPECT_EQ(metadata.mSuperAreas.front(), "Western System"); + ASSERT_EQ(metadata.mBalancingAuthorities.size(), 1); + EXPECT_EQ(metadata.mBalancingAuthorities.at(9).mName, "Test BA"); + ASSERT_EQ(metadata.mBuses.size(), 2); + const auto& firstBus = metadata.mBuses.at(101); + EXPECT_EQ(firstBus.mAreaNumber, 5); + EXPECT_EQ(firstBus.mZoneNumber, 4); + EXPECT_EQ(firstBus.mOwnerNumber, 7); + EXPECT_EQ(firstBus.mSubstationNumber, 20); + EXPECT_EQ(firstBus.mBalancingAuthorityNumber, 9); + ASSERT_TRUE(firstBus.mLatitude.has_value()); + ASSERT_TRUE(firstBus.mLongitude.has_value()); + EXPECT_DOUBLE_EQ(*firstBus.mLatitude, 35.2); + EXPECT_DOUBLE_EQ(*firstBus.mLongitude, -118.2); + EXPECT_FALSE(metadata.mBuses.at(102).mLatitude.has_value()); + } + TEST_F(SolverReaderTests, AuxRemoteGeneratorRegulation) { gds = std::make_unique(); @@ -319,5 +353,22 @@ namespace { ASSERT_EQ(gds->powerflow(), 0); EXPECT_EQ(gds->currentProcessState(), GridDynSimulation::GridState::POWERFLOW_COMPLETE); } + + TEST_F(SolverReaderTests, AuxActivsg10kMetadataWhenCaseIsAvailable) + { + const char* path = std::getenv("GRIDDYN_ACTIVSG10K_AUX"); + if (path == nullptr || *path == '\0') { + GTEST_SKIP() << "Set GRIDDYN_ACTIVSG10K_AUX to run the full-case metadata regression"; + } + const auto metadata = readAuxCaseMetadata(path); + EXPECT_EQ(metadata.mBuses.size(), 10000); + EXPECT_FALSE(metadata.mOwners.empty()); + EXPECT_FALSE(metadata.mZones.empty()); + EXPECT_FALSE(metadata.mSubstations.empty()); + const auto& firstBus = metadata.mBuses.at(10001); + EXPECT_EQ(firstBus.mSubstationNumber, 10000); + ASSERT_TRUE(firstBus.mLatitude.has_value()); + ASSERT_TRUE(firstBus.mLongitude.has_value()); + } } // namespace } // namespace griddyn From 24d13a041feee249e70db92d5fbc834d9485a651 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 21 Oct 2026 06:04:48 -0700 Subject: [PATCH 2/6] add aux readers for opf costs --- docs/capabilities.md | 5 +- docs/python-interface.md | 6 +- src/fileInput/gridDynReadAUX.cpp | 224 +++++++++++++++++- test/optimizationTests/testDcFormulation.cpp | 94 ++++++++ .../input_tests/aux_case_metadata.aux | 35 +++ test/test_files/input_tests/aux_opf_costs.aux | 38 +++ 6 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 test/test_files/input_tests/aux_case_metadata.aux create mode 100644 test/test_files/input_tests/aux_opf_costs.aux diff --git a/docs/capabilities.md b/docs/capabilities.md index eeddf84a6..e82c0566e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -11,10 +11,11 @@ features that are selected at build time. | Dynamic simulation | Initialize and advance time-domain simulation; inspect recorder samples in memory or write them to files. | | Network edits | Read and set supported model parameters, including numeric values with unit conversion. | | File output | Write PYPOWER/MATPOWER static cases and CSV/XML power-flow result summaries through `Simulation.write_file()`. | -| Optimal power flow | Run continuous DC OPF for supported polynomial costs through quadratic order when built with the optimization library. | +| Optimal power flow | Run DC OPF with convex quadratic and piecewise-linear generator costs when built with the optimization library. | Current Python API boundaries include no AC OPF, integer unit commitment, -piecewise-linear cost solution, or general network-construction API. The +or general network-construction API. Nonconvex piecewise costs need the HiGHS +backend; cubic polynomial costs remain unsupported. The static case exporters omit dynamic models, controls, relays, and events. Unsupported export data may be reported in the warnings returned by `Simulation.write_file()`. diff --git a/docs/python-interface.md b/docs/python-interface.md index 441afd0a8..239eab103 100644 --- a/docs/python-interface.md +++ b/docs/python-interface.md @@ -234,8 +234,10 @@ The result dictionary includes `success`, `status`, `message`, `solver`, `objective_value`, iteration count, feasibility/stationarity diagnostics, and whether the dispatch was applied. This entry point handles DC OPF with polynomial costs through quadratic order. AC OPF and integer commitment are -outside its current scope. Piecewise-linear curves are retained and exported; -the current OPF solvers return an unsupported status for them. +outside its current scope. Convex piecewise-linear generator costs are also +supported by the native DC OPF; nonconvex piecewise curves require HiGHS because +their formulation contains binary segment selectors. A nonzero cubic cost term +remains unsupported by this DC OPF. ## Capability notes diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index b18800bb3..6e4959b96 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -17,6 +17,9 @@ #include "griddyn/loads/Svd.h" #include "griddyn/loads/ZipLoad.h" #include "griddyn/primary/AcBus.h" +#ifdef GRIDDYN_ENABLE_OPTIMIZATION_LIBRARY +# include "optimization/gridDynOpt.h" +#endif #include #include #include @@ -27,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -250,10 +254,12 @@ namespace { using AuxRowHandler = std::function; using AuxSectionFilter = std::function; + using AuxSubdataHandler = std::function; void scanAuxFile(const std::string& fileName, const AuxSectionFilter& wantsSection, - const AuxRowHandler& handleRow) + const AuxRowHandler& handleRow, + const AuxSubdataHandler& handleSubdata = {}) { std::ifstream input(fileName); if (!input) { @@ -265,6 +271,7 @@ namespace { bool inHeader = false; bool inBody = false; bool inSubdata = false; + std::string subdataName; bool active = false; size_t lineNumber = 0; while (std::getline(input, line)) { @@ -289,14 +296,33 @@ namespace { header += trimmed; } else { if (inSubdata) { - // Embedded SUBDATA belongs to its parent PowerWorld object, - // not to the tabular network row that precedes it. if (lowerAux(trimmed).starts_with("'); + if (end == std::string::npos) { + throw std::runtime_error(fileName + ':' + std::to_string(lineNumber) + + ": unterminated AUX SUBDATA tag"); + } + subdataName = lowerAux(trimAux(std::string_view(trimmed).substr( + std::string_view("getName() + "_Gen_" + identifier; auto* generator = dynamic_cast(bus->find(name)); if (generator == nullptr) { - if (!row.has({"genmwsetpoint", "mwsetpoint", "genmwmax", "mwmax"})) { + if (!row.has({"genmwsetpoint", "genmw", "mwsetpoint", "genmwmax", "mwmax"})) { return; // An AUX supplement may only amend an existing generator. } generator = network.mOptions.checkFlag(ASSUME_POWERFLOW_ONLY) ? @@ -536,11 +562,11 @@ namespace { if (row.has({"genstatus", "status"}) && !auxClosed(row.value({"genstatus", "status"}))) { generator->disable(); } - if (row.has({"genmwsetpoint", "mwsetpoint"})) { - generator->set("p", row.number({"genmwsetpoint", "mwsetpoint"}), units::MW); + if (row.has({"genmwsetpoint", "genmw", "mwsetpoint"})) { + generator->set("p", row.number({"genmwsetpoint", "genmw", "mwsetpoint"}), units::MW); } - if (row.has({"genmvrsetpoint", "mvarsetpoint"})) { - generator->set("q", row.number({"genmvrsetpoint", "mvarsetpoint"}), units::MVAR); + if (row.has({"genmvrsetpoint", "genmvr", "mvarsetpoint"})) { + generator->set("q", row.number({"genmvrsetpoint", "genmvr", "mvarsetpoint"}), units::MVAR); } if (row.has({"genmwmax", "mwmax"})) { generator->set("pmax", row.number({"genmwmax", "mwmax"}), units::MW); @@ -1040,6 +1066,171 @@ namespace { } } +#ifdef GRIDDYN_ENABLE_OPTIMIZATION_LIBRARY + struct AuxGeneratorCost { + int mBusNumber = 0; + std::string mGeneratorId; + std::string mModel; + double mFixed = 0.0; + double mFuel = 0.0; + double mVariableOm = 0.0; + double mIoA = 0.0; + double mIoB = 0.0; + double mIoC = 0.0; + double mIoD = 0.0; + double mMultiplier = 1.0; + bool mHasUnsupportedExtras = false; + std::vector> mBids; + }; + + void loadAuxOpfCosts(AuxNetwork& network, + const std::string& fileName, + GridDynOptimization& optimization) + { + std::optional pending; + size_t unsupportedCubic = 0; + size_t unsupportedExtras = 0; + size_t missingBids = 0; + size_t unmatchedGenerators = 0; + size_t invalidBids = 0; + const auto finish = [&]() { + if (!pending.has_value() || (pending->mModel != "cubic" && + pending->mModel != "piecewise linear")) { + return; + } + if (pending->mHasUnsupportedExtras) { + ++unsupportedExtras; + return; + } + auto* bus = findAuxBus(network, pending->mBusNumber); + if (bus == nullptr) { + ++unmatchedGenerators; + return; + } + auto* generator = dynamic_cast( + bus->find(bus->getName() + "_Gen_" + pending->mGeneratorId)); + if (generator == nullptr) { + ++unmatchedGenerators; + return; + } + MatPowerCostCurve curve; + const auto multiplier = pending->mMultiplier; + if (pending->mModel == "cubic") { + // GridDyn's DC OPF evaluates costs through quadratic order. + // Never silently discard a nonzero cubic term. + if (pending->mIoD != 0.0) { + ++unsupportedCubic; + return; + } + curve.model = 2; + curve.coefficients = { + multiplier * pending->mFuel * pending->mIoC, + multiplier * (pending->mFuel * pending->mIoB + pending->mVariableOm), + multiplier * (pending->mFixed + pending->mFuel * pending->mIoA)}; + } else if (pending->mModel == "piecewise linear" && !pending->mBids.empty()) { + // PowerWorld stores the marginal price from each MW break to + // the next. MATPOWER stores total cost at each break instead. + curve.model = 1; + double previousMw = 0.0; + double cost = pending->mFixed * multiplier; + double price = pending->mBids.front().second * multiplier; + curve.coefficients = {0.0, cost}; + for (const auto& [mw, nextPrice] : pending->mBids) { + if (mw < previousMw) { + ++invalidBids; + return; + } + // Repeated MW breaks have no width. Keep the last price + // as the slope of the segment to their right. + if (mw > previousMw) { + cost += (mw - previousMw) * price; + curve.coefficients.insert(curve.coefficients.end(), {mw, cost}); + } + previousMw = mw; + price = nextPrice * multiplier; + } + const double maximumMw = generator->get("pmax", units::MW); + if (maximumMw > previousMw) { + cost += (maximumMw - previousMw) * price; + curve.coefficients.insert(curve.coefficients.end(), {maximumMw, cost}); + } + } else { + ++missingBids; + return; + } + if (curve.valid()) { + optimization.setGeneratorCostCurve(generator, curve); + } else { + ++invalidBids; + } + }; + scanAuxFile( + fileName, + [](std::string_view type) { return type == "gen"; }, + [&](const AuxRow& row) { + finish(); + pending.reset(); + if (!row.has({"gencostmodel"})) { + return; + } + AuxGeneratorCost cost; + cost.mBusNumber = row.integer({"busnum"}); + cost.mGeneratorId = trimAux(row.value({"genid"})); + if (cost.mGeneratorId.empty()) { + cost.mGeneratorId = "1"; + } + cost.mModel = lowerAux(trimAux(row.value({"gencostmodel"}))); + cost.mFixed = row.number({"genfixedcost"}); + cost.mFuel = row.number({"genfuelcost"}); + cost.mVariableOm = row.number({"genvariableom"}); + cost.mIoA = row.number({"genioa"}); + cost.mIoB = row.number({"geniob"}); + cost.mIoC = row.number({"genioc"}); + cost.mIoD = row.number({"geniod"}); + cost.mMultiplier = row.number({"gencostmultiplier"}, 1.0); + // These PowerWorld adjustments need a separate, verified + // mapping; omitting them would change the OPF objective. + cost.mHasUnsupportedExtras = + row.number({"genextrafuelcost"}) != 0.0 || + row.number({"genextraomcost"}) != 0.0 || + row.number({"gencostshift"}) != 0.0; + pending = std::move(cost); + }, + [&](std::string_view name, std::string_view line, size_t /*lineNumber*/) { + if (!pending.has_value() || name != "bidcurve") { + return; + } + const auto values = tokenizeAuxRow(line); + if (values.size() != 2U) { + throw std::runtime_error("AUX generator BidCurve requires MW and marginal price"); + } + std::size_t used = 0; + const double mw = std::stod(values[0], &used); + if (used != values[0].size() || !std::isfinite(mw) || mw < 0.0) { + throw std::runtime_error("invalid AUX generator BidCurve MW break"); + } + const double price = std::stod(values[1], &used); + if (used != values[1].size() || !std::isfinite(price)) { + throw std::runtime_error("invalid AUX generator BidCurve marginal price"); + } + pending->mBids.emplace_back(mw, price); + }); + finish(); + if (unsupportedCubic + unsupportedExtras + missingBids + unmatchedGenerators + + invalidBids != 0U) { + optimization.log( + &optimization, + PrintLevel::WARNING, + "AUX OPF costs skipped: " + std::to_string(unsupportedCubic) + + " nonzero cubic terms, " + std::to_string(unsupportedExtras) + + " unmodeled cost adjustments, " + std::to_string(missingBids) + + " missing BidCurve data, " + std::to_string(unmatchedGenerators) + + " unmatched generators, " + std::to_string(invalidBids) + + " invalid curves"); + } + } +#endif + } // namespace void loadAux(CoreObject* parentObject, @@ -1062,8 +1253,9 @@ void loadAux(CoreObject* parentObject, if (!network.mExistingNetwork) { GridSimulation::resetObjectCounters(); } - // Stream the file three times rather than buffering it. Areas and base - // power must precede buses, which must precede equipment references. + // Stream the network in three passes rather than buffering it. Areas and + // base power precede buses and equipment; optimization costs need a fourth + // pass after their generator objects exist. scanAuxFile( fileName, [](std::string_view type) { @@ -1082,7 +1274,9 @@ void loadAux(CoreObject* parentObject, }, [&](const AuxRow& row) { if (row.mSection.mType == "gen") { - readAuxGenerator(network, row); + if (row.has({"genmwsetpoint", "genmw", "genmwmax", "genmwmin", "genstatus"})) { + readAuxGenerator(network, row); + } } else if (row.mSection.mType == "load") { readAuxLoad(network, row); } else if (row.mSection.mType == "shunt") { @@ -1096,6 +1290,12 @@ void loadAux(CoreObject* parentObject, } }); finishAuxGeneratorControls(network); +#ifdef GRIDDYN_ENABLE_OPTIMIZATION_LIBRARY + auto* optimization = dynamic_cast(parentObject->getRoot()); + if (optimization != nullptr) { + loadAuxOpfCosts(network, fileName, *optimization); + } +#endif } AuxCaseMetadata readAuxCaseMetadata(const std::string& fileName) diff --git a/test/optimizationTests/testDcFormulation.cpp b/test/optimizationTests/testDcFormulation.cpp index 3e2949b5d..35272677b 100644 --- a/test/optimizationTests/testDcFormulation.cpp +++ b/test/optimizationTests/testDcFormulation.cpp @@ -938,6 +938,100 @@ TEST(OptimizationDcFormulationTests, NativeOptimizerMaterializesConvexPiecewiseL static_cast(root->constraintSize(mode))); } +TEST(OptimizationDcFormulationTests, AuxImportsCubicAndMarginalBidCosts) +{ + auto gds = std::make_unique(); + const auto filePath = std::filesystem::path{GRIDDYN_TEST_DIRECTORY} / "input_tests" / + "aux_opf_costs.aux"; + griddyn::loadFile(gds.get(), filePath.string()); + + auto* bus1 = dynamic_cast(gds->findByUserID("bus", 1)); + auto* bus2 = dynamic_cast(gds->findByUserID("bus", 2)); + ASSERT_NE(bus1, nullptr); + ASSERT_NE(bus2, nullptr); + auto* generator1 = bus1->getGen(0); + auto* generator2 = bus2->getGen(0); + ASSERT_NE(generator1, nullptr); + ASSERT_NE(generator2, nullptr); + + const auto* cubic = gds->matPowerCostCurve(generator1); + ASSERT_NE(cubic, nullptr); + EXPECT_EQ(cubic->model, 2); + EXPECT_EQ(cubic->coefficients, (std::vector{0.2, 10.0, 7.0})); + + const auto* bid = gds->matPowerCostCurve(generator2); + ASSERT_NE(bid, nullptr); + EXPECT_EQ(bid->model, 1); + EXPECT_EQ(bid->coefficients, + (std::vector{0.0, 5.0, 20.0, 205.0, 40.0, 445.0, 60.0, 725.0})); + + const auto mode = makeDcMode(griddyn::LinearityMode::QUADRATIC); + gds->initializeOptimizationModel(mode); + auto* root = gds->getOptimizationObject(); + ASSERT_NE(root, nullptr); + griddyn::NativeOptimizer optimizer(gds.get(), mode); + ASSERT_EQ(optimizer.allocate(root->objSize(mode), root->constraintSize(mode)), + FUNCTION_EXECUTION_SUCCESS); + optimizer.initialize(0.0); + ASSERT_EQ(optimizer.prepareProblemData(0.0), FUNCTION_EXECUTION_SUCCESS) + << optimizer.getLastErrorString(); +#ifdef GRIDDYN_ENABLE_HIGHS + griddyn::HighsOptimizer highs(gds.get(), mode); + ASSERT_EQ(highs.allocate(root->objSize(mode), root->constraintSize(mode)), + FUNCTION_EXECUTION_SUCCESS); + highs.initialize(0.0); + double returnTime = -1.0; + ASSERT_EQ(highs.solve(0.0, returnTime), FUNCTION_EXECUTION_SUCCESS) + << highs.lastSolveResult().message; + EXPECT_EQ(highs.lastSolveResult().status, griddyn::NativeSolveStatus::OPTIMAL); +#endif +} + +TEST(OptimizationDcFormulationTests, AuxActivsg2000CostWhenCaseIsAvailable) +{ + const char* caseFile = std::getenv("GRIDDYN_ACTIVSG2000_AUX"); + if (caseFile == nullptr || *caseFile == '\0') { + GTEST_SKIP() << "Set GRIDDYN_ACTIVSG2000_AUX to run the full-case cost check"; + } + auto gds = std::make_unique(); + griddyn::loadFile(gds.get(), caseFile); + auto* bus = dynamic_cast(gds->findByUserID("bus", 2057)); + ASSERT_NE(bus, nullptr); + auto* generator = bus->getGen(0); + ASSERT_NE(generator, nullptr); + const auto* curve = gds->matPowerCostCurve(generator); + ASSERT_NE(curve, nullptr); + EXPECT_EQ(curve->model, 2); + ASSERT_EQ(curve->coefficients.size(), 3U); + EXPECT_GT(curve->coefficients[0], 0.0); + EXPECT_GT(curve->coefficients[1], 0.0); +} + +TEST(OptimizationDcFormulationTests, AuxTexas7kBidWhenCaseIsAvailable) +{ + const char* caseFile = std::getenv("GRIDDYN_TEXAS7K_AUX"); + if (caseFile == nullptr || *caseFile == '\0') { + GTEST_SKIP() << "Set GRIDDYN_TEXAS7K_AUX to run the full-case bid check"; + } + auto gds = std::make_unique(); + griddyn::loadFile(gds.get(), caseFile); + auto* bus = dynamic_cast(gds->findByUserID("bus", 111180)); + ASSERT_NE(bus, nullptr); + auto* generator = bus->getGen(0); + ASSERT_NE(generator, nullptr); + const auto* curve = gds->matPowerCostCurve(generator); + ASSERT_NE(curve, nullptr); + EXPECT_EQ(curve->model, 1); + EXPECT_GE(curve->coefficients.size(), 4U); + if (curve->coefficients.size() >= 4U) { + EXPECT_NEAR(curve->coefficients[2], 94.0, 1e-8); + EXPECT_NEAR((curve->coefficients[3] - curve->coefficients[1]) / + (curve->coefficients[2] - curve->coefficients[0]), + 45.83, + 1e-8); + } +} + #ifdef GRIDDYN_ENABLE_HIGHS TEST(OptimizationDcFormulationTests, HighsOptimizerSolvesNonconvexPiecewiseLinearCost) { diff --git a/test/test_files/input_tests/aux_case_metadata.aux b/test/test_files/input_tests/aux_case_metadata.aux new file mode 100644 index 000000000..f6f78ecd0 --- /dev/null +++ b/test/test_files/input_tests/aux_case_metadata.aux @@ -0,0 +1,35 @@ +// Case hierarchy can be read independently of network creation. +DATA (Owner, [OwnerName,OwnerNum]) +{ + "First Owner" 7 +} +DATA (Owner, [OwnerNum,DataMaintainerAssign]) +{ + 7 "" +} +DATA (Zone, [ZoneNum,ZoneName]) +{ + 3 "Coastal" + 4 "Inland" +} +DATA (Substation, [Latitude,SubName,SubNum,Longitude]) +{ + 35.1 "North Yard" 20 -118.1 +} +DATA (SuperArea, [SAName]) +{ + "Western System" +} +DATA (BalancingAuthority, [BAName,BANumber]) +{ + "Test BA" 9 +} +DATA (Bus, [BusNum,SubNum,ZoneNum,OwnerNum,AreaNum,BANumber,Latitude:1,Longitude:1]) +{ + 101 20 3 7 5 9 35.2 -118.2 +} +DATA (Bus, [BusNum,ZoneNum]) +{ + 101 4 + 102 3 +} diff --git a/test/test_files/input_tests/aux_opf_costs.aux b/test/test_files/input_tests/aux_opf_costs.aux new file mode 100644 index 000000000..d8168b68a --- /dev/null +++ b/test/test_files/input_tests/aux_opf_costs.aux @@ -0,0 +1,38 @@ +// Cubic I/O data and a separate PowerWorld marginal BidCurve section. +DATA (Sim_Solution_Options_Value, [VariableName, ValueField]) +{ + "MvaBase" 100 +} +DATA (Bus, [BusNum, BusName, BusNomVolt, BusPUVolt, BusSlack]) +{ + 1 "ONE" 230 1 YES + 2 "TWO" 230 1 NO +} +DATA (Gen, [GenID, BusNum, GenStatus, GenMWSetPoint, GenMWMin, GenMWMax, + GenCostModel, GenFuelCost, GenIOA, GenIOB, GenIOC, GenIOD, + GenFixedCost, GenVariableOM]) +{ + "A" 1 Closed 20 0 80 "Cubic" 2 0 3 0.1 0 7 4 + "B" 2 Closed 30 0 60 "None" 0 0 0 0 0 0 0 +} +DATA (Gen, [BusNum, GenID, GenCostModel, GenFixedCost, GenCostMultiplier]) +{ + 2 "B" "Piecewise Linear" 5 1 + + // MW Price[$/MWhr], used as the slope beginning at this MW + 0 10 + 20 12 + // PowerWorld exports can repeat an MW break with no intervening segment. + 20 12 + 40 14 + +} +DATA (Load, [BusNum, LoadID, LoadStatus, LoadSMW, LoadSMVR]) +{ + 2 "1" Closed 50 0 +} +DATA (Branch, [BusNum, BusNum:1, LineCircuit, BranchDeviceType, + LineStatus, LineR, LineX, LineAMVA]) +{ + 1 2 "1" Line Closed 0 0.1 100 +} From 2b70cb003d75f2d3841b429d155a5d294ab635aa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:06:22 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/capabilities.md | 14 ++-- src/fileInput/gridDynReadAUX.cpp | 71 ++++++++++---------- test/libraryTests/testSolverReader.cpp | 4 +- test/optimizationTests/testDcFormulation.cpp | 4 +- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/capabilities.md b/docs/capabilities.md index e82c0566e..c058acc83 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -4,13 +4,13 @@ This page summarizes the Python package surface documented for the current release. GridDyn's C++ application and optional libraries expose additional features that are selected at build time. -| Workflow | Current Python support | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------- | -| Load a network | Load extension-selected input formats. See the [reader list](python-interface.md#load-and-run-a-network). | -| Power flow | Run the steady-state power-flow solver and inspect bus, generator, and other object collections. | -| Dynamic simulation | Initialize and advance time-domain simulation; inspect recorder samples in memory or write them to files. | -| Network edits | Read and set supported model parameters, including numeric values with unit conversion. | -| File output | Write PYPOWER/MATPOWER static cases and CSV/XML power-flow result summaries through `Simulation.write_file()`. | +| Workflow | Current Python support | +| ------------------ | --------------------------------------------------------------------------------------------------------------- | +| Load a network | Load extension-selected input formats. See the [reader list](python-interface.md#load-and-run-a-network). | +| Power flow | Run the steady-state power-flow solver and inspect bus, generator, and other object collections. | +| Dynamic simulation | Initialize and advance time-domain simulation; inspect recorder samples in memory or write them to files. | +| Network edits | Read and set supported model parameters, including numeric values with unit conversion. | +| File output | Write PYPOWER/MATPOWER static cases and CSV/XML power-flow result summaries through `Simulation.write_file()`. | | Optimal power flow | Run DC OPF with convex quadratic and piecewise-linear generator costs when built with the optimization library. | Current Python API boundaries include no AC OPF, integer unit commitment, diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index 6e4959b96..65fb17a61 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -146,8 +146,8 @@ namespace { } }; - [[nodiscard]] std::optional auxOptionalNumber( - const AuxRow& row, std::initializer_list names) + [[nodiscard]] std::optional + auxOptionalNumber(const AuxRow& row, std::initializer_list names) { const auto value = row.number(names, std::numeric_limits::quiet_NaN()); return std::isfinite(value) ? std::optional{value} : std::nullopt; @@ -304,10 +304,9 @@ namespace { handleSubdata(subdataName, trimmed, lineNumber); } catch (const std::exception& error) { - throw std::runtime_error(fileName + ':' + - std::to_string(lineNumber) + " (" + - section.mType + " SUBDATA " + subdataName + - "): " + error.what()); + throw std::runtime_error(fileName + ':' + std::to_string(lineNumber) + + " (" + section.mType + " SUBDATA " + + subdataName + "): " + error.what()); } } continue; @@ -320,9 +319,10 @@ namespace { throw std::runtime_error(fileName + ':' + std::to_string(lineNumber) + ": unterminated AUX SUBDATA tag"); } - subdataName = lowerAux(trimAux(std::string_view(trimmed).substr( - std::string_view("set("p", row.number({"genmwsetpoint", "genmw", "mwsetpoint"}), units::MW); } if (row.has({"genmvrsetpoint", "genmvr", "mvarsetpoint"})) { - generator->set("q", row.number({"genmvrsetpoint", "genmvr", "mvarsetpoint"}), units::MVAR); + generator->set("q", + row.number({"genmvrsetpoint", "genmvr", "mvarsetpoint"}), + units::MVAR); } if (row.has({"genmwmax", "mwmax"})) { generator->set("pmax", row.number({"genmwmax", "mwmax"}), units::MW); @@ -1094,8 +1096,8 @@ namespace { size_t unmatchedGenerators = 0; size_t invalidBids = 0; const auto finish = [&]() { - if (!pending.has_value() || (pending->mModel != "cubic" && - pending->mModel != "piecewise linear")) { + if (!pending.has_value() || + (pending->mModel != "cubic" && pending->mModel != "piecewise linear")) { return; } if (pending->mHasUnsupportedExtras) { @@ -1123,10 +1125,11 @@ namespace { return; } curve.model = 2; - curve.coefficients = { - multiplier * pending->mFuel * pending->mIoC, - multiplier * (pending->mFuel * pending->mIoB + pending->mVariableOm), - multiplier * (pending->mFixed + pending->mFuel * pending->mIoA)}; + curve.coefficients = {multiplier * pending->mFuel * pending->mIoC, + multiplier * + (pending->mFuel * pending->mIoB + pending->mVariableOm), + multiplier * + (pending->mFixed + pending->mFuel * pending->mIoA)}; } else if (pending->mModel == "piecewise linear" && !pending->mBids.empty()) { // PowerWorld stores the marginal price from each MW break to // the next. MATPOWER stores total cost at each break instead. @@ -1190,10 +1193,8 @@ namespace { cost.mMultiplier = row.number({"gencostmultiplier"}, 1.0); // These PowerWorld adjustments need a separate, verified // mapping; omitting them would change the OPF objective. - cost.mHasUnsupportedExtras = - row.number({"genextrafuelcost"}) != 0.0 || - row.number({"genextraomcost"}) != 0.0 || - row.number({"gencostshift"}) != 0.0; + cost.mHasUnsupportedExtras = row.number({"genextrafuelcost"}) != 0.0 || + row.number({"genextraomcost"}) != 0.0 || row.number({"gencostshift"}) != 0.0; pending = std::move(cost); }, [&](std::string_view name, std::string_view line, size_t /*lineNumber*/) { @@ -1202,7 +1203,8 @@ namespace { } const auto values = tokenizeAuxRow(line); if (values.size() != 2U) { - throw std::runtime_error("AUX generator BidCurve requires MW and marginal price"); + throw std::runtime_error( + "AUX generator BidCurve requires MW and marginal price"); } std::size_t used = 0; const double mw = std::stod(values[0], &used); @@ -1217,16 +1219,16 @@ namespace { }); finish(); if (unsupportedCubic + unsupportedExtras + missingBids + unmatchedGenerators + - invalidBids != 0U) { - optimization.log( - &optimization, - PrintLevel::WARNING, - "AUX OPF costs skipped: " + std::to_string(unsupportedCubic) + - " nonzero cubic terms, " + std::to_string(unsupportedExtras) + - " unmodeled cost adjustments, " + std::to_string(missingBids) + - " missing BidCurve data, " + std::to_string(unmatchedGenerators) + - " unmatched generators, " + std::to_string(invalidBids) + - " invalid curves"); + invalidBids != + 0U) { + optimization.log(&optimization, + PrintLevel::WARNING, + "AUX OPF costs skipped: " + std::to_string(unsupportedCubic) + + " nonzero cubic terms, " + std::to_string(unsupportedExtras) + + " unmodeled cost adjustments, " + std::to_string(missingBids) + + " missing BidCurve data, " + std::to_string(unmatchedGenerators) + + " unmatched generators, " + std::to_string(invalidBids) + + " invalid curves"); } } #endif @@ -1352,14 +1354,13 @@ AuxCaseMetadata readAuxCaseMetadata(const std::string& fileName) if (auto latitude = auxOptionalNumber(row, {"latitude"}); latitude.has_value()) { substation.mLatitude = latitude; } - if (auto longitude = auxOptionalNumber(row, {"longitude"}); - longitude.has_value()) { + if (auto longitude = auxOptionalNumber(row, {"longitude"}); longitude.has_value()) { substation.mLongitude = longitude; } } else if (type == "superarea") { auto name = trimAux(row.value({"saname"})); - if (!name.empty() && std::find(metadata.mSuperAreas.begin(), - metadata.mSuperAreas.end(), name) == + if (!name.empty() && + std::find(metadata.mSuperAreas.begin(), metadata.mSuperAreas.end(), name) == metadata.mSuperAreas.end()) { metadata.mSuperAreas.push_back(std::move(name)); } diff --git a/test/libraryTests/testSolverReader.cpp b/test/libraryTests/testSolverReader.cpp index 27ed86bd6..bb05e0bb7 100644 --- a/test/libraryTests/testSolverReader.cpp +++ b/test/libraryTests/testSolverReader.cpp @@ -231,8 +231,8 @@ namespace { TEST_F(SolverReaderTests, AuxCaseMetadataKeepsNamedReferencesAndBusAssignments) { - const auto metadata = readAuxCaseMetadata( - std::string{inputTestDirectory} + "aux_case_metadata.aux"); + const auto metadata = + readAuxCaseMetadata(std::string{inputTestDirectory} + "aux_case_metadata.aux"); ASSERT_EQ(metadata.mOwners.size(), 1); EXPECT_EQ(metadata.mOwners.at(7).mName, "First Owner"); ASSERT_EQ(metadata.mZones.size(), 2); diff --git a/test/optimizationTests/testDcFormulation.cpp b/test/optimizationTests/testDcFormulation.cpp index 35272677b..c193cf810 100644 --- a/test/optimizationTests/testDcFormulation.cpp +++ b/test/optimizationTests/testDcFormulation.cpp @@ -941,8 +941,8 @@ TEST(OptimizationDcFormulationTests, NativeOptimizerMaterializesConvexPiecewiseL TEST(OptimizationDcFormulationTests, AuxImportsCubicAndMarginalBidCosts) { auto gds = std::make_unique(); - const auto filePath = std::filesystem::path{GRIDDYN_TEST_DIRECTORY} / "input_tests" / - "aux_opf_costs.aux"; + const auto filePath = + std::filesystem::path{GRIDDYN_TEST_DIRECTORY} / "input_tests" / "aux_opf_costs.aux"; griddyn::loadFile(gds.get(), filePath.string()); auto* bus1 = dynamic_cast(gds->findByUserID("bus", 1)); From 5bdc3e7ea680eb7964e447071d02cc01bf8ec3df Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 21 Oct 2026 06:29:04 -0700 Subject: [PATCH 4/6] clang-tidy updates --- src/fileInput/gridDynReadAUX.cpp | 37 ++++++++++++++++---------- test/libraryTests/testSolverReader.cpp | 12 +++------ 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index 65fb17a61..2d3c15a83 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -304,9 +304,16 @@ namespace { handleSubdata(subdataName, trimmed, lineNumber); } catch (const std::exception& error) { - throw std::runtime_error(fileName + ':' + std::to_string(lineNumber) + - " (" + section.mType + " SUBDATA " + - subdataName + "): " + error.what()); + std::string message = fileName; + message.push_back(':'); + message.append(std::to_string(lineNumber)); + message.append(" ("); + message.append(section.mType); + message.append(" SUBDATA "); + message.append(subdataName); + message.append("): "); + message.append(error.what()); + throw std::runtime_error(std::move(message)); } } continue; @@ -1127,9 +1134,10 @@ namespace { curve.model = 2; curve.coefficients = {multiplier * pending->mFuel * pending->mIoC, multiplier * - (pending->mFuel * pending->mIoB + pending->mVariableOm), + ((pending->mFuel * pending->mIoB) + + pending->mVariableOm), multiplier * - (pending->mFixed + pending->mFuel * pending->mIoA)}; + (pending->mFixed + (pending->mFuel * pending->mIoA))}; } else if (pending->mModel == "piecewise linear" && !pending->mBids.empty()) { // PowerWorld stores the marginal price from each MW break to // the next. MATPOWER stores total cost at each break instead. @@ -1138,18 +1146,18 @@ namespace { double cost = pending->mFixed * multiplier; double price = pending->mBids.front().second * multiplier; curve.coefficients = {0.0, cost}; - for (const auto& [mw, nextPrice] : pending->mBids) { - if (mw < previousMw) { + for (const auto& [breakpointMw, nextPrice] : pending->mBids) { + if (breakpointMw < previousMw) { ++invalidBids; return; } // Repeated MW breaks have no width. Keep the last price // as the slope of the segment to their right. - if (mw > previousMw) { - cost += (mw - previousMw) * price; - curve.coefficients.insert(curve.coefficients.end(), {mw, cost}); + if (breakpointMw > previousMw) { + cost += (breakpointMw - previousMw) * price; + curve.coefficients.insert(curve.coefficients.end(), {breakpointMw, cost}); } - previousMw = mw; + previousMw = breakpointMw; price = nextPrice * multiplier; } const double maximumMw = generator->get("pmax", units::MW); @@ -1207,15 +1215,16 @@ namespace { "AUX generator BidCurve requires MW and marginal price"); } std::size_t used = 0; - const double mw = std::stod(values[0], &used); - if (used != values[0].size() || !std::isfinite(mw) || mw < 0.0) { + const double breakpointMw = std::stod(values[0], &used); + if (used != values[0].size() || !std::isfinite(breakpointMw) || + breakpointMw < 0.0) { throw std::runtime_error("invalid AUX generator BidCurve MW break"); } const double price = std::stod(values[1], &used); if (used != values[1].size() || !std::isfinite(price)) { throw std::runtime_error("invalid AUX generator BidCurve marginal price"); } - pending->mBids.emplace_back(mw, price); + pending->mBids.emplace_back(breakpointMw, price); }); finish(); if (unsupportedCubic + unsupportedExtras + missingBids + unmatchedGenerators + diff --git a/test/libraryTests/testSolverReader.cpp b/test/libraryTests/testSolverReader.cpp index bb05e0bb7..bbb69c982 100644 --- a/test/libraryTests/testSolverReader.cpp +++ b/test/libraryTests/testSolverReader.cpp @@ -240,10 +240,8 @@ namespace { ASSERT_EQ(metadata.mSubstations.size(), 1); const auto& substation = metadata.mSubstations.at(20); EXPECT_EQ(substation.mName, "North Yard"); - ASSERT_TRUE(substation.mLatitude.has_value()); - ASSERT_TRUE(substation.mLongitude.has_value()); - EXPECT_DOUBLE_EQ(*substation.mLatitude, 35.1); - EXPECT_DOUBLE_EQ(*substation.mLongitude, -118.1); + EXPECT_EQ(substation.mLatitude, std::optional{35.1}); + EXPECT_EQ(substation.mLongitude, std::optional{-118.1}); ASSERT_EQ(metadata.mSuperAreas.size(), 1); EXPECT_EQ(metadata.mSuperAreas.front(), "Western System"); ASSERT_EQ(metadata.mBalancingAuthorities.size(), 1); @@ -255,10 +253,8 @@ namespace { EXPECT_EQ(firstBus.mOwnerNumber, 7); EXPECT_EQ(firstBus.mSubstationNumber, 20); EXPECT_EQ(firstBus.mBalancingAuthorityNumber, 9); - ASSERT_TRUE(firstBus.mLatitude.has_value()); - ASSERT_TRUE(firstBus.mLongitude.has_value()); - EXPECT_DOUBLE_EQ(*firstBus.mLatitude, 35.2); - EXPECT_DOUBLE_EQ(*firstBus.mLongitude, -118.2); + EXPECT_EQ(firstBus.mLatitude, std::optional{35.2}); + EXPECT_EQ(firstBus.mLongitude, std::optional{-118.2}); EXPECT_FALSE(metadata.mBuses.at(102).mLatitude.has_value()); } From c2ed1eae7123c19f9e6cf125a1789b45684ff3d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:31:15 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/fileInput/gridDynReadAUX.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index 2d3c15a83..1137d730e 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -1134,8 +1134,7 @@ namespace { curve.model = 2; curve.coefficients = {multiplier * pending->mFuel * pending->mIoC, multiplier * - ((pending->mFuel * pending->mIoB) + - pending->mVariableOm), + ((pending->mFuel * pending->mIoB) + pending->mVariableOm), multiplier * (pending->mFixed + (pending->mFuel * pending->mIoA))}; } else if (pending->mModel == "piecewise linear" && !pending->mBids.empty()) { From 226ebe4415bd37e6f7232d4931c4905f922c9f11 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 21 Oct 2026 06:39:49 -0700 Subject: [PATCH 6/6] more clang-tidy --- src/fileInput/gridDynReadAUX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fileInput/gridDynReadAUX.cpp b/src/fileInput/gridDynReadAUX.cpp index 2d3c15a83..dc025c987 100644 --- a/src/fileInput/gridDynReadAUX.cpp +++ b/src/fileInput/gridDynReadAUX.cpp @@ -313,7 +313,7 @@ namespace { message.append(subdataName); message.append("): "); message.append(error.what()); - throw std::runtime_error(std::move(message)); + throw std::runtime_error(message); } } continue;