From 61f5f649d220df55f6efbc29289b5fbf1ba220cf Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Wed, 28 Jan 2026 19:08:52 -0800 Subject: [PATCH 01/12] Improve power flow Q-limit handling and add island detection function --- .../components/pf_matrix/pf_components.cpp | 125 ++++++++--- .../components/pf_matrix/pf_components.hpp | 7 +- .../contingency_analysis/ca_driver.cpp | 37 +++- .../modules/powerflow/pf_app_module.cpp | 109 ++++++++-- .../modules/powerflow/pf_app_module.hpp | 11 +- .../modules/powerflow/pf_factory_module.cpp | 198 +++++++++++++++++- .../modules/powerflow/pf_factory_module.hpp | 23 ++ 7 files changed, 444 insertions(+), 66 deletions(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 93d9aa183..fdad8441f 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -262,48 +262,38 @@ bool gridpack::powerflow::PFBus::chkQlim(void) double qval = p_Qinj*p_sbase+ql; // If qval exceeds the total generator Q capacity, perform PV->PQ +// Generator remains online with Q clamped to limit. +// setSBus() will be called at start of next iteration to update p_Q0. // if (qval > qmax ) { - printf("\nWarning: Gen(s) at bus %d exceeds the QMAX %8.3f vs %8.3f, converted to PQ bus\n", getOriginalIndex(),qval, qmax); - ql = ql-qmax; + printf("\nWarning: Gen(s) at bus %d exceeds the QMAX %8.3f vs %8.3f, converted to PQ bus\n", getOriginalIndex(),qval, qmax); p_save2isPV = p_isPV; p_isPV = false; - *p_PV_ptr = false; - pl -= ppl; - //p_gstatus.clear(); + p_type = 1; // Change bus type from PV(2) to PQ(1) + if (p_PV_ptr) *p_PV_ptr = false; + // Generator stays online, clamp Q to limit for (int i=0; igetValue(BUS_TYPE, &p_type); + p_save_type = p_type; // Save original bus type for restoration after Q limit handling if (p_type == 3) { setReferenceBus(true); } @@ -503,6 +530,7 @@ void gridpack::powerflow::PFBus::load( p_pg.push_back(pg); p_savePg.push_back(pg); p_qg.push_back(qg); + p_saveQg.push_back(qg); p_qmax.push_back(qmax); p_qmin.push_back(qmin); p_qmax_orig.push_back(qmax); @@ -519,6 +547,7 @@ void gridpack::powerflow::PFBus::load( if (gstatus == 1) { p_v = vs; //reset initial PV voltage to set voltage + p_voltage = vs; // Also update p_voltage so resetVoltage() uses the same initial value as base case if (p_type == 2) p_isPV = true; } std::string id("-1"); @@ -538,6 +567,7 @@ void gridpack::powerflow::PFBus::load( } } p_saveisPV = p_isPV; + p_save2isPV = p_isPV; // Initialize for Q limit handling - ensures valid value if chkQlim() never called // Add load int lstatus; @@ -739,6 +769,11 @@ void gridpack::powerflow::PFBus::setGenStatus(std::string gen_id, bool status) int gsize = p_gstatus.size(); for (i=0; i data); @@ -456,6 +456,7 @@ class PFBus // newly added priavate variables: std::vector p_pg, p_qg, p_pFac; std::vector p_savePg; + std::vector p_saveQg; // Save original Q for restoration after Q limit handling std::vector p_gstatus; std::vector p_gstatus_save; std::vector p_qmax,p_qmin; @@ -477,6 +478,7 @@ class PFBus int p_ngen; int p_nload; int p_type; + int p_save_type; // Save original bus type for restoration after Q limit handling int p_area; int p_zone; bool p_source; @@ -516,6 +518,7 @@ class PFBus & p_angle & p_voltage & p_pg & p_qg & p_pFac & p_qmin & p_qmax & p_qmin_orig & p_qmax_orig & p_pFac_orig + & p_saveQg & p_gstatus & p_vs & p_gid & p_pt & p_pb @@ -527,7 +530,7 @@ class PFBus & p_vmin & p_vmax & p_isPV & p_saveisPV - & p_ngen & p_type & p_nload + & p_ngen & p_type & p_save_type & p_nload & p_area & p_zone & p_source & p_sink & p_rtpr_scale; diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 9a534423e..8328a2c0e 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -492,13 +492,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (print_calcs) pf_app.writeHeader(sbuf); // Reset all voltages back to their original values pf_app.resetVoltages(); + // Sync ghost bus data after voltage reset to ensure branches connected to + // ghost buses use the correct reset voltages in power flow calculation + pf_network->updateBuses(); // Set contingency pf_app.setContingency(events[task_id]); + // Check for islanding before attempting to solve + int islandCount = pf_app.getIslandCount(); + bool islandDetected = (islandCount > 1); // Solve power flow equations for this system #ifdef USE_SUCCESS contingency_idx.push_back(task_id); #endif - if (pf_app.solve()) { + if (!islandDetected && pf_app.solve()) { #ifdef USE_SUCCESS contingency_success.push_back(true); #endif @@ -518,16 +524,24 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) #ifdef USE_SUCCESS contingency_violation.push_back(1); #endif - } + } + // Report bus voltage violations if (!ok1) { sprintf(sbuf,"\nBus Violation for contingency %s\n", events[task_id].p_name.c_str()); + } else if (!ok) { + sprintf(sbuf,"\nNo Bus Violation for contingency %s\n", + events[task_id].p_name.c_str()); } if (print_calcs) pf_app.print(sbuf); if (print_calcs) pf_app.writeCABus(); + // Report branch overload violations if (!ok2) { sprintf(sbuf,"\nBranch Violation for contingency %s\n", events[task_id].p_name.c_str()); + } else if (!ok) { + sprintf(sbuf,"\nNo Branch Violation for contingency %s\n", + events[task_id].p_name.c_str()); } #ifdef USE_SUCCESS @@ -539,7 +553,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) contingency_violation.push_back(3); } #endif - + if (print_calcs) pf_app.print(sbuf); if (print_calcs) pf_app.writeCABranch(); // Get strings of data from power flow calculation and parse them to @@ -637,14 +651,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } timer->stop(t_store); #endif - if (check_Qlim) pf_app.clearQlimViolations(); + // Note: clearQlimViolations() moved after unSetContingency() below } else { #ifdef USE_SUCCESS contingency_success.push_back(false); contingency_violation.push_back(0); #endif - sprintf(sbuf,"\nDivergent for contingency %s\n", - events[task_id].p_name.c_str()); + if (islandDetected) { + sprintf(sbuf,"\nIslanding detected for contingency %s (%d islands)\n", + events[task_id].p_name.c_str(), islandCount); + } else { + sprintf(sbuf,"\nDivergent for contingency %s\n", + events[task_id].p_name.c_str()); + } if (print_calcs) pf_app.print(sbuf); // Add dummy values to StatBlock object. Mask value is set to 0 for all // network elements to indicate calculation failure @@ -732,9 +751,13 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } timer->stop(t_store); #endif - } + } // Return network to its original base case state pf_app.unSetContingency(events[task_id]); + // Clear Q limit violations AFTER unSetContingency so generators are restored first. + // This ensures clearQlim() sees the correct generator status when deciding + // whether to restore p_isPV (PV bus status). + if (check_Qlim) pf_app.clearQlimViolations(); // Close output file for this contingency if (print_calcs) pf_app.close(); } diff --git a/src/applications/modules/powerflow/pf_app_module.cpp b/src/applications/modules/powerflow/pf_app_module.cpp index e8b49c141..5bc04cfb3 100644 --- a/src/applications/modules/powerflow/pf_app_module.cpp +++ b/src/applications/modules/powerflow/pf_app_module.cpp @@ -139,6 +139,7 @@ void gridpack::powerflow::PFAppModule::readNetwork( p_tolerance = cursor->get("tolerance",1.0e-6); p_qlim = cursor->get("qlim",0); p_max_iteration = cursor->get("maxIteration",50); + p_max_qlim_iterations = cursor->get("maxQlimIterations",3); ComplexType tol; // Phase shift sign double phaseShiftSign = cursor->get("phaseShiftSign",1.0); @@ -345,9 +346,11 @@ bool gridpack::powerflow::PFAppModule::solve() bool repeat = true; int int_repeat = 0; while (repeat) { + ret = true; // Reset ret - if this iteration converges, we should return true iter = 0; tol = 2.0*p_tolerance; int_repeat ++; + bool qlim_handled_early = false; // Track if Q limits were handled during stagnation char ioBuf[128]; if (!p_no_print) { sprintf (ioBuf," repeat time = %d \n", int_repeat); @@ -474,7 +477,13 @@ bool gridpack::powerflow::PFAppModule::solve() if (!p_no_print) { sprintf(ioBuf,"\nIteration %d Tol: %12.6e\n",iter+1,real(tol)); p_busIO->header(ioBuf); - } + } + + // Variables for early stagnation detection + gridpack::ComplexType tol_prev = tol; + int stagnant_count = 0; + const int STAGNANT_THRESHOLD = 5; // Check Q limits after 5 stagnant iterations + const double STAGNANT_TOL = 1.0e-10; // Tolerance for detecting stagnation while (real(tol) > p_tolerance && iter < p_max_iteration) { // Push current values in X vector back into network components @@ -541,6 +550,29 @@ bool gridpack::powerflow::PFAppModule::solve() p_busIO->header(ioBuf); } iter++; + + // Early stagnation detection: if tolerance unchanged, check Q limits early + if (p_qlim != 0 && fabs(real(tol) - real(tol_prev)) < STAGNANT_TOL) { + stagnant_count++; + if (stagnant_count >= STAGNANT_THRESHOLD) { + if (!p_factory->checkQlimViolations()) { + // Q limit violations found - break to restart with PV->PQ changes + if (!p_no_print) { + sprintf(ioBuf,"Stagnation detected at iter %d, Qlim violations found\n", iter); + p_busIO->header(ioBuf); + } + qlim_handled_early = true; + break; + } else { + // No Q limit violations but still stagnant - likely numerical issue + stagnant_count = 0; // Reset and continue trying + } + } + } else { + stagnant_count = 0; // Reset if tolerance is changing + } + tol_prev = tol; + if (real(tol)> 100.0*real(tol_org)){ ret = false; if (!p_no_print) { @@ -554,27 +586,53 @@ bool gridpack::powerflow::PFAppModule::solve() if (iter >= p_max_iteration) ret = false; if (p_qlim == 0) { repeat = false; + } else if (qlim_handled_early) { + // Q limits were already handled during early stagnation detection + // Check if we've reached max Q-limit iterations + if (int_repeat >= p_max_qlim_iterations) { + if (!p_no_print) { + sprintf(ioBuf,"Max Q-limit iterations (%d) reached, accepting current solution\n", p_max_qlim_iterations); + p_busIO->header(ioBuf); + } + repeat = false; + } + // Otherwise repeat stays true to restart with new PV/PQ configuration } else { if (p_factory->checkQlimViolations()) { - repeat =false; + repeat = false; } else { - if (!p_no_print) { - sprintf (ioBuf,"There are Qlim violations at iter =%d\n", iter); - p_busIO->header(ioBuf); + // Check if we've reached max Q-limit iterations + if (int_repeat >= p_max_qlim_iterations) { + if (!p_no_print) { + sprintf(ioBuf,"Max Q-limit iterations (%d) reached, accepting current solution\n", p_max_qlim_iterations); + p_busIO->header(ioBuf); + } + repeat = false; + } else { + if (!p_no_print) { + sprintf (ioBuf,"There are Qlim violations at iter =%d\n", iter); + p_busIO->header(ioBuf); + } } } } - // Push final result back onto buses - timer->start(t_bmap); - p_factory->setMode(RHS); - vMap.mapToBus(X); - timer->stop(t_bmap); - - // Make sure that ghost buses have up-to-date values before printing out - // results - timer->start(t_updt); - p_network->updateBuses(); - timer->stop(t_updt); + // Push final result back onto buses, but ONLY if no Q limit violations. + // If there are Q limit violations, p_isPV changed for some buses after vMap + // was created. The vMap indexing is based on old dimensions, but setValues() + // uses the current p_isPV. This mismatch causes incorrect memory access. + // When repeat=true, a new iteration will start with a fresh vMap anyway. + if (!repeat) { + timer->start(t_bmap); + p_factory->setMode(RHS); + vMap.mapToBus(X); + timer->stop(t_bmap); + + // Make sure that ghost buses have up-to-date values before printing out + // results + timer->start(t_updt); + p_network->updateBuses(); + timer->stop(t_updt); + } } timer->stop(t_total); return ret; @@ -842,7 +900,7 @@ void gridpack::powerflow::PFAppModule::saveData() /** * Save results of powerflow calculation to data collection objects - * added by Renke, also modify the original bus mag, ang, + * added by Renke, also modify the original bus mag, ang, * and the original generator PG QG in the datacollection */ void gridpack::powerflow::PFAppModule::saveDataAlsotoOrg() @@ -870,8 +928,7 @@ bool gridpack::powerflow::PFAppModule::getPFSolutionSingleBus( if (nbus == 0) ret = false; for(ibus=0; ibus - (p_network->getBus(vec_busintidx[ibus]).get()); //->getOriginalIndex() - //printf("----renke debug PFAppModule::getPFSolutionSingleBus, \n"); + (p_network->getBus(vec_busintidx[ibus]).get()); bus_mag=bus->getVoltage(); double anglerads = bus->getPhase(); double pi = 4.0*atan(1.0); @@ -936,9 +993,19 @@ bool gridpack::powerflow::PFAppModule::setContingency( p_contingency_name.clear(); } p_factory->checkLoneBus(); + p_factory->detectIslands(); // Detect islands after applying contingency return ret; } +/** + * Get the number of islands detected after setting a contingency + * @return number of islands (1 = connected network, >1 = islanding) + */ +int gridpack::powerflow::PFAppModule::getIslandCount() +{ + return p_factory->getIslandCount(); +} + /** * Return system to the state before the contingency * @param event data describing location and type of contingency @@ -948,6 +1015,7 @@ bool gridpack::powerflow::PFAppModule::unSetContingency( gridpack::powerflow::Contingency &event) { p_factory->clearLoneBus(); + p_factory->clearIslands(); bool ret = true; if (event.p_type == Generator) { int ngen = event.p_busid.size(); @@ -1229,9 +1297,6 @@ std::vector gridpack::powerflow::PFAppModule::getContingencyFailure violations[i].bus1,violations[i].bus2,violations[i].tag); string = sbuf; } - if (!p_no_print) { - printf("DEBUG VIOLATION: (%s)\n",string.c_str()); - } ret.push_back(string); } return ret; diff --git a/src/applications/modules/powerflow/pf_app_module.hpp b/src/applications/modules/powerflow/pf_app_module.hpp index 16cdd663c..3569aee42 100644 --- a/src/applications/modules/powerflow/pf_app_module.hpp +++ b/src/applications/modules/powerflow/pf_app_module.hpp @@ -159,7 +159,7 @@ class PFAppModule /** * Save results of powerflow calculation to data collection objects - * added by Renke, also modify the original bus mag, ang, + * added by Renke, also modify the original bus mag, ang, * and the original generator PG QG in the datacollection */ void saveDataAlsotoOrg(); @@ -205,6 +205,12 @@ class PFAppModule */ bool unSetContingency(Contingency &event); + /** + * Get the number of islands detected after setting a contingency + * @return number of islands (1 = connected network, >1 = islanding) + */ + int getIslandCount(); + /** * Set voltage limits on all buses * @param Vmin lower bound on voltages @@ -874,6 +880,9 @@ class PFAppModule // qlim enforce flag int p_qlim; + // maximum number of Q-limit iterations + int p_max_qlim_iterations; + // pointer to bus IO module boost::shared_ptr > p_busIO; diff --git a/src/applications/modules/powerflow/pf_factory_module.cpp b/src/applications/modules/powerflow/pf_factory_module.cpp index 38d9a2981..b3ac30965 100644 --- a/src/applications/modules/powerflow/pf_factory_module.cpp +++ b/src/applications/modules/powerflow/pf_factory_module.cpp @@ -16,6 +16,8 @@ // ------------------------------------------------------------- #include +#include +#include #include "boost/smart_ptr/shared_ptr.hpp" #include "gridpack/parser/dictionary.hpp" #include "gridpack/parallel/global_vector.hpp" @@ -36,6 +38,7 @@ PFFactoryModule::PFFactoryModule(PFFactoryModule::NetworkPtr network) { p_network = network; p_rateB = false; + p_islandCount = 0; } /** @@ -225,6 +228,198 @@ void gridpack::powerflow::PFFactoryModule::clearLoneBus() } } +/** + * Detect islands (disconnected subnetworks) in the network using BFS. + * Mark buses in smaller islands as isolated to prevent singular Jacobian. + * @param stream optional stream pointer for printing island info + * @return number of islands found (1 = connected network, >1 = islanding) + */ +int gridpack::powerflow::PFFactoryModule::detectIslands(std::ofstream *stream) +{ + int numBus = p_network->numBuses(); + int numBranch = p_network->numBranches(); + int i, j, k; + char buf[256]; + + // Clear previous island isolated status + p_saveIslandIsolatedStatus.clear(); + + // Build mapping from bus local index to original index and vice versa + std::map origToLocal; // original bus ID -> local index + std::vector localToOrig; // local index -> original bus ID + std::vector busActive; // whether bus is active and not already isolated + + for (i = 0; i < numBus; i++) { + if (!p_network->getActiveBus(i)) continue; + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(i).get()); + if (bus->isIsolated()) continue; // Skip already isolated buses + + int origIdx = bus->getOriginalIndex(); + origToLocal[origIdx] = localToOrig.size(); + localToOrig.push_back(i); // Store local network index + busActive.push_back(true); + } + + int activeBusCount = localToOrig.size(); + if (activeBusCount == 0) { + p_islandCount = 0; + return 0; + } + + // Build adjacency list based on active branches + std::vector > adj(activeBusCount); + + for (i = 0; i < numBranch; i++) { + if (!p_network->getActiveBranch(i)) continue; + + gridpack::powerflow::PFBranch *branch = + dynamic_cast(p_network->getBranch(i).get()); + + // Check if branch has any active lines + std::vector status = branch->getLineStatus(); + bool branchActive = false; + for (k = 0; k < status.size(); k++) { + if (status[k]) { + branchActive = true; + break; + } + } + if (!branchActive) continue; + + // Get the two buses connected by this branch + int bus1Orig = branch->getBus1OriginalIndex(); + int bus2Orig = branch->getBus2OriginalIndex(); + + // Check if both buses are in our active set + std::map::iterator it1 = origToLocal.find(bus1Orig); + std::map::iterator it2 = origToLocal.find(bus2Orig); + + if (it1 != origToLocal.end() && it2 != origToLocal.end()) { + int idx1 = it1->second; + int idx2 = it2->second; + adj[idx1].push_back(idx2); + adj[idx2].push_back(idx1); + } + } + + // BFS to find connected components (islands) + std::vector islandId(activeBusCount, -1); + std::vector > islands; // Each island contains list of local indices + int currentIsland = 0; + + for (i = 0; i < activeBusCount; i++) { + if (islandId[i] >= 0) continue; // Already assigned to an island + + // BFS from bus i + std::vector currentIslandBuses; + std::queue q; + q.push(i); + islandId[i] = currentIsland; + + while (!q.empty()) { + int curr = q.front(); + q.pop(); + currentIslandBuses.push_back(curr); + + for (j = 0; j < adj[curr].size(); j++) { + int neighbor = adj[curr][j]; + if (islandId[neighbor] < 0) { + islandId[neighbor] = currentIsland; + q.push(neighbor); + } + } + } + + islands.push_back(currentIslandBuses); + currentIsland++; + } + + p_islandCount = islands.size(); + + // If only one island, network is connected + if (p_islandCount <= 1) { + return p_islandCount; + } + + // Find the largest island (main network) + int largestIsland = 0; + int largestSize = islands[0].size(); + for (i = 1; i < islands.size(); i++) { + if (islands[i].size() > largestSize) { + largestSize = islands[i].size(); + largestIsland = i; + } + } + + // Mark buses in smaller islands as isolated + p_islandIsolatedBusIndices.clear(); + for (i = 0; i < islands.size(); i++) { + if (i == largestIsland) continue; // Keep main island + + sprintf(buf, "\nIsland %d detected with %d buses (marking as isolated):\n", + i + 1, (int)islands[i].size()); + printf("%s", buf); + if (stream != NULL) *stream << buf; + + for (j = 0; j < islands[i].size(); j++) { + int localIdx = localToOrig[islands[i][j]]; // Get network local index + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(localIdx).get()); + + sprintf(buf, " Bus %d\n", bus->getOriginalIndex()); + printf("%s", buf); + if (stream != NULL) *stream << buf; + + // Save current isolated status and local index, then mark as isolated + p_saveIslandIsolatedStatus.push_back(bus->isIsolated()); + p_islandIsolatedBusIndices.push_back(localIdx); + bus->setIsolated(true); + } + } + + sprintf(buf, "\nNetwork split into %d islands. Main island has %d buses.\n", + p_islandCount, largestSize); + printf("%s", buf); + if (stream != NULL) *stream << buf; + + return p_islandCount; +} + +/** + * Get the number of islands detected in the last call to detectIslands + * @return number of islands (0 if detectIslands not called) + */ +int gridpack::powerflow::PFFactoryModule::getIslandCount() const +{ + return p_islandCount; +} + +/** + * Clear island detection state and restore isolated status of buses + * that were marked as isolated due to islanding + */ +void gridpack::powerflow::PFFactoryModule::clearIslands() +{ + if (p_islandIsolatedBusIndices.size() == 0) { + p_islandCount = 0; + p_saveIslandIsolatedStatus.clear(); + return; + } + + // Restore isolated status of buses that were marked during island detection + for (int i = 0; i < p_islandIsolatedBusIndices.size(); i++) { + int localIdx = p_islandIsolatedBusIndices[i]; + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(localIdx).get()); + bus->setIsolated(p_saveIslandIsolatedStatus[i]); + } + + p_saveIslandIsolatedStatus.clear(); + p_islandIsolatedBusIndices.clear(); + p_islandCount = 0; +} + /** * Set voltage limits on all buses * @param Vmin lower bound on voltages @@ -315,7 +510,8 @@ void gridpack::powerflow::PFFactoryModule::ignoreVoltageViolations() gridpack::powerflow::PFBus *bus = dynamic_cast (p_network->getBus(i).get()); - if (bus->checkVoltageViolation()) bus->setIgnore(true); + // Set ignore on buses WITH violations (checkVoltageViolation returns false when violated) + if (!bus->checkVoltageViolation()) bus->setIgnore(true); } } } diff --git a/src/applications/modules/powerflow/pf_factory_module.hpp b/src/applications/modules/powerflow/pf_factory_module.hpp index f35bafdec..0c14defe8 100644 --- a/src/applications/modules/powerflow/pf_factory_module.hpp +++ b/src/applications/modules/powerflow/pf_factory_module.hpp @@ -96,6 +96,26 @@ class PFFactoryModule */ void clearLoneBus(); + /** + * Detect islands (disconnected subnetworks) in the network using BFS. + * Mark buses in smaller islands as isolated to prevent singular Jacobian. + * @param stream optional stream pointer for printing island info + * @return number of islands found (1 = connected network, >1 = islanding) + */ + int detectIslands(std::ofstream *stream = NULL); + + /** + * Get the number of islands detected in the last call to detectIslands + * @return number of islands (0 if detectIslands not called) + */ + int getIslandCount() const; + + /** + * Clear island detection state and restore isolated status of buses + * that were marked as isolated due to islanding + */ + void clearIslands(); + /** * Set voltage limits on all buses * @param Vmin lower bound on voltages @@ -247,6 +267,9 @@ class PFFactoryModule NetworkPtr p_network; std::vector p_saveIsolatedStatus; + std::vector p_saveIslandIsolatedStatus; // For island detection + std::vector p_islandIsolatedBusIndices; // Local indices of buses isolated due to islanding + int p_islandCount; // Number of islands detected std::vector p_violations; From 7586a57322006d565613b4accb8d7a669f04c242 Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Wed, 28 Jan 2026 20:22:05 -0800 Subject: [PATCH 02/12] Use bool type for qlim parameter in power flow and CA for consistency --- src/applications/contingency_analysis/README.md | 4 ++-- src/applications/contingency_analysis/ca_driver.cpp | 7 ++----- src/applications/modules/powerflow/pf_app_module.cpp | 6 +++--- src/applications/modules/powerflow/pf_app_module.hpp | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/applications/contingency_analysis/README.md b/src/applications/contingency_analysis/README.md index 320e64662..84f5a2600 100644 --- a/src/applications/contingency_analysis/README.md +++ b/src/applications/contingency_analysis/README.md @@ -54,8 +54,8 @@ in this data. stored values all represent the phase angle at each bus. PV buses are included in this data. -**pq\_changed\_cnt.txt** This file is only created if the checkQLimit flag is -set to "true" in the input file. It counts the number of times a PV bused is +**pq\_changed\_cnt.txt** This file is only created if the qlim flag is +set to true in the input file. It counts the number of times a PV bus is changed to a PQ bus during the simulation. column 1: row index diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 8328a2c0e..4654b4075 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -199,11 +199,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (!cursor->get("maxVoltage",&Vmax)) { Vmax = 1.1; } - // Check for Q limit violations - bool check_Qlim; - if (!cursor->get("checkQLimit",&check_Qlim)) { - check_Qlim = false; - } + // Check for Q limit violations (qlim: true=enabled, false=disabled) + bool check_Qlim = cursor->get("qlim", false); gridpack::parallel::Communicator task_comm = world.divide(grp_size); // Keep track of failed calculations diff --git a/src/applications/modules/powerflow/pf_app_module.cpp b/src/applications/modules/powerflow/pf_app_module.cpp index 5bc04cfb3..157cf8caf 100644 --- a/src/applications/modules/powerflow/pf_app_module.cpp +++ b/src/applications/modules/powerflow/pf_app_module.cpp @@ -137,7 +137,7 @@ void gridpack::powerflow::PFAppModule::readNetwork( } // Convergence and iteration parameters p_tolerance = cursor->get("tolerance",1.0e-6); - p_qlim = cursor->get("qlim",0); + p_qlim = cursor->get("qlim",false); p_max_iteration = cursor->get("maxIteration",50); p_max_qlim_iterations = cursor->get("maxQlimIterations",3); ComplexType tol; @@ -552,7 +552,7 @@ bool gridpack::powerflow::PFAppModule::solve() iter++; // Early stagnation detection: if tolerance unchanged, check Q limits early - if (p_qlim != 0 && fabs(real(tol) - real(tol_prev)) < STAGNANT_TOL) { + if (p_qlim && fabs(real(tol) - real(tol_prev)) < STAGNANT_TOL) { stagnant_count++; if (stagnant_count >= STAGNANT_THRESHOLD) { if (!p_factory->checkQlimViolations()) { @@ -584,7 +584,7 @@ bool gridpack::powerflow::PFAppModule::solve() } if (iter >= p_max_iteration) ret = false; - if (p_qlim == 0) { + if (!p_qlim) { repeat = false; } else if (qlim_handled_early) { // Q limits were already handled during early stagnation detection diff --git a/src/applications/modules/powerflow/pf_app_module.hpp b/src/applications/modules/powerflow/pf_app_module.hpp index 3569aee42..ede4db151 100644 --- a/src/applications/modules/powerflow/pf_app_module.hpp +++ b/src/applications/modules/powerflow/pf_app_module.hpp @@ -877,8 +877,8 @@ class PFAppModule // convergence tolerance double p_tolerance; - // qlim enforce flag - int p_qlim; + // qlim enforce flag (true=enabled, false=disabled) + bool p_qlim; // maximum number of Q-limit iterations int p_max_qlim_iterations; From 4c3792afe2ca548aa95246ccf19e1bf35a75f8ef Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Wed, 28 Jan 2026 20:26:42 -0800 Subject: [PATCH 03/12] Add qlim configuration to CA input XML files (default: false) --- src/applications/data_sets/input/ca/input.euro.xml | 6 ++++-- src/applications/data_sets/input/ca/input.polish.xml | 6 ++++-- src/applications/data_sets/input/ca/input_118.xml | 6 ++++-- src/applications/data_sets/input/ca/input_14.xml | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/applications/data_sets/input/ca/input.euro.xml b/src/applications/data_sets/input/ca/input.euro.xml index 8368ae7c0..a9812f721 100644 --- a/src/applications/data_sets/input/ca/input.euro.xml +++ b/src/applications/data_sets/input/ca/input.euro.xml @@ -1,16 +1,18 @@ - true + true contingencies_euro.xml 1 1.1 0.9 + false - EuropeanOpenModel_v23.raw + EuropeanOpenModel_v23.raw 50 1.0e-3 + false + true + +``` + +**Option 2: Use a contingency list file** +```xml + + contingencies.xml + +``` + +**Option 3: Combine both** (auto-generate N-1 + custom N-K from file) +```xml + + true + true + custom_nk_contingencies.xml + +``` +When combined, duplicates from the file are automatically skipped. + +### Other Options + +| Option | Description | Default | +|--------|-------------|---------| +| `groupSize` | Number of MPI processes per contingency (parallelization) | 1 | +| `printCalcFiles` | Write detailed output for each contingency | true | +| `minVoltage` | Minimum voltage threshold for violations (p.u.) | 0.9 | +| `maxVoltage` | Maximum voltage threshold for violations (p.u.) | 1.1 | +| `qlim` | Enable reactive power limit enforcement (PV to PQ bus conversion) | false | + +### Contingency File Format + +See `contingencies_nk_example.xml` for examples of N-1, N-2, and N-3 contingency definitions. + +--- + +## Output Files + The contingency analysis calculation produces a number of files summarize the results of the entire set of individual contingency simulations. Only results for contingencies that ran to completion are included. Calculations that failed diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index ca6a009ff..a7ea0366e 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -176,7 +176,7 @@ std::vector // Create contingency for this branch gridpack::powerflow::Contingency contingency; char name_buf[64]; - sprintf(name_buf, "N1_BR_%d_%d_%s", from_bus, to_bus, + sprintf(name_buf, "BR_%d_%d_%s", from_bus, to_bus, utils.clean2Char(ckt_id).c_str()); contingency.p_name = name_buf; contingency.p_type = Branch; @@ -212,7 +212,7 @@ std::vector // Create contingency for this generator gridpack::powerflow::Contingency contingency; char name_buf[64]; - sprintf(name_buf, "N1_GEN_%d_%s", bus_id, + sprintf(name_buf, "GN_%d_%s", bus_id, utils.clean2Char(gen_id).c_str()); contingency.p_name = name_buf; contingency.p_type = Generator; @@ -377,6 +377,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) gridpack::parallel::GlobalVector ca_success(world); std::vector contingency_violation; gridpack::parallel::GlobalVector ca_violation(world); + std::vector contingency_isolated; + gridpack::parallel::GlobalVector ca_isolated(world); #endif // Create powerflow applications on each task communicator @@ -758,27 +760,51 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // ghost buses use the correct reset voltages in power flow calculation pf_network->updateBuses(); // Set contingency - pf_app.setContingency(events[task_id]); + bool contingencyFound = pf_app.setContingency(events[task_id]); + if (!contingencyFound) { + printf("WARNING: Contingency '%s' - elements not found or no valid slack bus\n", + events[task_id].p_name.c_str()); + } // Check for islanding before attempting to solve + // Note: lone bus isolation is handled separately as a warning, not a failure int islandCount = pf_app.getIslandCount(); + bool hasLoneBus = pf_app.hasLoneBus(); bool islandDetected = (islandCount > 1); // Solve power flow equations for this system #ifdef USE_SUCCESS contingency_idx.push_back(task_id); #endif - if (!islandDetected && pf_app.solve()) { -#ifdef USE_SUCCESS - contingency_success.push_back(true); -#endif + // Skip power flow if contingency setup failed (no valid slack) or islanding detected + bool slackCapacityOk = true; // Will be checked after solve + if (contingencyFound && !islandDetected && pf_app.solve()) { if (check_Qlim && !pf_app.checkQlimViolations()) { pf_app.solve(); } - // If power flow solution is successful, write out voltages and currents - if (print_calcs) pf_app.write(); - // Check for violations - bool ok1 = pf_app.checkVoltageViolations(); - bool ok2 = pf_app.checkLineOverloadViolations(); - bool ok = ok1 && ok2; + // Check if slack bus generator exceeds capacity + slackCapacityOk = pf_app.checkSlackCapacity(); + if (!slackCapacityOk) { + // Slack generator exceeds Pmax - insufficient generation capacity + // This is treated as a failure, similar to divergence +#ifdef USE_SUCCESS + contingency_success.push_back(false); + contingency_violation.push_back(0); + contingency_isolated.push_back(false); +#endif + sprintf(sbuf,"\nInsufficient generation capacity for contingency %s\n", + events[task_id].p_name.c_str()); + if (print_calcs) pf_app.print(sbuf); + } else { + // Power flow solved and slack within capacity +#ifdef USE_SUCCESS + contingency_success.push_back(true); + contingency_isolated.push_back(hasLoneBus); +#endif + // If power flow solution is successful, write out voltages and currents + if (print_calcs) pf_app.write(); + // Check for violations + bool ok1 = pf_app.checkVoltageViolations(); + bool ok2 = pf_app.checkLineOverloadViolations(); + bool ok = ok1 && ok2; // Include results of violation checks in output if (ok) { sprintf(sbuf,"\nNo violation for contingency %s\n", @@ -913,15 +939,20 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } timer->stop(t_store); #endif - // Note: clearQlimViolations() moved after unSetContingency() below + // Note: clearQlimViolations() moved after unSetContingency() below + } // end slackCapacityOk block } else { #ifdef USE_SUCCESS contingency_success.push_back(false); contingency_violation.push_back(0); + contingency_isolated.push_back(false); #endif if (islandDetected) { sprintf(sbuf,"\nIslanding detected for contingency %s (%d islands)\n", events[task_id].p_name.c_str(), islandCount); + } else if (!contingencyFound) { + sprintf(sbuf,"\nNo valid slack bus for contingency %s\n", + events[task_id].p_name.c_str()); } else { sprintf(sbuf,"\nDivergent for contingency %s\n", events[task_id].p_name.c_str()); @@ -1032,32 +1063,40 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (task_comm.rank() == 0) { ca_success.addElements(contingency_idx, contingency_success); ca_violation.addElements(contingency_idx, contingency_violation); + ca_isolated.addElements(contingency_idx, contingency_isolated); } ca_success.upload(); ca_violation.upload(); + ca_isolated.upload(); // Write out stats on successful calculations if (world.rank() == 0) { contingency_idx.clear(); contingency_success.clear(); contingency_violation.clear(); + contingency_isolated.clear(); for (i=0; i true + contingencies_118.xml + + + + 1 1.1 0.9 diff --git a/src/applications/data_sets/input/ca/input_14.xml b/src/applications/data_sets/input/ca/input_14.xml index abb151e01..5370be42d 100644 --- a/src/applications/data_sets/input/ca/input_14.xml +++ b/src/applications/data_sets/input/ca/input_14.xml @@ -2,7 +2,12 @@ true + contingencies_14.xml + + + + 2 1.1 0.9 diff --git a/src/applications/modules/powerflow/pf_app_module.cpp b/src/applications/modules/powerflow/pf_app_module.cpp index 157cf8caf..d5ec9bc6b 100644 --- a/src/applications/modules/powerflow/pf_app_module.cpp +++ b/src/applications/modules/powerflow/pf_app_module.cpp @@ -955,14 +955,25 @@ bool gridpack::powerflow::PFAppModule::setContingency( idx = event.p_busid[i]; std::string tag = event.p_genid[i]; std::vector lids = p_network->getLocalBusIndices(idx); - if (lids.size() == 0) ret = false; + if (lids.size() == 0) { + printf("WARNING: Bus %d not found for generator contingency\n", idx); + ret = false; + continue; + } gridpack::powerflow::PFBus *bus; + bool found = false; for (j=0; j( p_network->getBus(jdx).get()); event.p_saveGenStatus[i] = bus->getGenStatus(tag); - bus->setGenStatus(tag, false); + if (bus->setGenStatus(tag, false)) { + found = true; + } + } + if (!found) { + printf("WARNING: Generator '%s' not found on bus %d\n", tag.c_str(), idx); + ret = false; } } } else if (event.p_type == Branch) { @@ -974,14 +985,25 @@ bool gridpack::powerflow::PFAppModule::setContingency( from = event.p_from[i]; std::string tag = event.p_ckt[i]; std::vector lids = p_network->getLocalBranchIndices(from,to); - if (lids.size() == 0) ret = false; + if (lids.size() == 0) { + printf("WARNING: Branch %d-%d not found\n", from, to); + ret = false; + continue; + } gridpack::powerflow::PFBranch *branch; + bool found = false; for (j=0; j( p_network->getBranch(jdx).get()); event.p_saveLineStatus[i] = branch->getBranchStatus(tag); - branch->setBranchStatus(tag, false); + if (branch->setBranchStatus(tag, false)) { + found = true; + } + } + if (!found) { + printf("WARNING: Circuit ID '%s' not found on branch %d-%d\n", tag.c_str(), from, to); + ret = false; } } } else { @@ -993,6 +1015,12 @@ bool gridpack::powerflow::PFAppModule::setContingency( p_contingency_name.clear(); } p_factory->checkLoneBus(); + // Check if slack bus still has online generator, transfer if needed + bool slackOk = p_factory->checkAndTransferSlack(); + if (!slackOk) { + // No valid slack bus - system cannot be solved + ret = false; + } p_factory->detectIslands(); // Detect islands after applying contingency return ret; } @@ -1006,6 +1034,41 @@ int gridpack::powerflow::PFAppModule::getIslandCount() return p_factory->getIslandCount(); } +/** + * Check if any lone buses were found after setting a contingency + * @return true if at least one bus became isolated (no active branches) + */ +bool gridpack::powerflow::PFAppModule::hasLoneBus() +{ + return p_factory->hasLoneBus(); +} + +/** + * Check if slack bus has online generator, transfer if needed + * @return true if valid slack exists, false if no generation capacity + */ +bool gridpack::powerflow::PFAppModule::checkAndTransferSlack() +{ + return p_factory->checkAndTransferSlack(); +} + +/** + * Restore original slack bus after contingency + */ +void gridpack::powerflow::PFAppModule::restoreSlack() +{ + p_factory->restoreSlack(); +} + +/** + * Check if slack bus generator output exceeds capacity + * @return true if within limits, false if Pgen > Pmax + */ +bool gridpack::powerflow::PFAppModule::checkSlackCapacity() +{ + return p_factory->checkSlackCapacity(); +} + /** * Return system to the state before the contingency * @param event data describing location and type of contingency @@ -1016,6 +1079,7 @@ bool gridpack::powerflow::PFAppModule::unSetContingency( { p_factory->clearLoneBus(); p_factory->clearIslands(); + p_factory->restoreSlack(); // Restore original slack bus if it was transferred bool ret = true; if (event.p_type == Generator) { int ngen = event.p_busid.size(); diff --git a/src/applications/modules/powerflow/pf_app_module.hpp b/src/applications/modules/powerflow/pf_app_module.hpp index ede4db151..897accfe3 100644 --- a/src/applications/modules/powerflow/pf_app_module.hpp +++ b/src/applications/modules/powerflow/pf_app_module.hpp @@ -211,6 +211,29 @@ class PFAppModule */ int getIslandCount(); + /** + * Check if any lone buses were found after setting a contingency + * @return true if at least one bus became isolated (no active branches) + */ + bool hasLoneBus(); + + /** + * Check if slack bus has online generator, transfer if needed + * @return true if valid slack exists, false if no generation capacity + */ + bool checkAndTransferSlack(); + + /** + * Restore original slack bus after contingency + */ + void restoreSlack(); + + /** + * Check if slack bus generator output exceeds capacity + * @return true if within limits, false if Pgen > Pmax + */ + bool checkSlackCapacity(); + /** * Set voltage limits on all buses * @param Vmin lower bound on voltages diff --git a/src/applications/modules/powerflow/pf_factory_module.cpp b/src/applications/modules/powerflow/pf_factory_module.cpp index b3ac30965..db535090a 100644 --- a/src/applications/modules/powerflow/pf_factory_module.cpp +++ b/src/applications/modules/powerflow/pf_factory_module.cpp @@ -39,6 +39,10 @@ PFFactoryModule::PFFactoryModule(PFFactoryModule::NetworkPtr network) p_network = network; p_rateB = false; p_islandCount = 0; + p_hasLoneBus = false; + p_originalSlackBusIdx = -1; + p_currentSlackBusIdx = -1; + p_slackTransferred = false; } /** @@ -181,8 +185,9 @@ bool gridpack::powerflow::PFFactoryModule::checkLoneBus(std::ofstream *stream) } if (!ok) bus_ok = false; } - // Check whether bus_ok is true on all processors - return checkTrue(!bus_ok); + // Check whether bus_ok is true on all processors (lone bus found if bus_ok is false) + p_hasLoneBus = checkTrue(!bus_ok); + return p_hasLoneBus; } /** @@ -190,6 +195,7 @@ bool gridpack::powerflow::PFFactoryModule::checkLoneBus(std::ofstream *stream) */ void gridpack::powerflow::PFFactoryModule::clearLoneBus() { + p_hasLoneBus = false; if (p_saveIsolatedStatus.size() == 0) return; int numBus = p_network->numBuses(); int i, j, k; @@ -395,6 +401,156 @@ int gridpack::powerflow::PFFactoryModule::getIslandCount() const return p_islandCount; } +/** + * Check if any lone buses were found in the last call to checkLoneBus + * @return true if at least one lone bus was found + */ +bool gridpack::powerflow::PFFactoryModule::hasLoneBus() const +{ + return p_hasLoneBus; +} + +/** + * Check if the reference (slack) bus has an online generator. + * If not, transfer the slack function to the bus with the largest + * online generator capacity. + * @return true if a valid slack bus exists (or was transferred), + * false if no generator with real power capacity is available + */ +bool gridpack::powerflow::PFFactoryModule::checkAndTransferSlack() +{ + int numBus = p_network->numBuses(); + int i; + + // Find the current slack bus and check if it has an online generator + int slackBusIdx = -1; + bool slackHasOnlineGen = false; + + for (i = 0; i < numBus; i++) { + if (!p_network->getActiveBus(i)) continue; + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(i).get()); + if (bus->getReferenceBus()) { + slackBusIdx = i; + p_originalSlackBusIdx = i; // Save original slack bus + slackHasOnlineGen = bus->hasOnlineGenerator(); + break; + } + } + + if (slackBusIdx < 0) { + // No slack bus found - this shouldn't happen + printf("ERROR: No reference bus found in network\n"); + return false; + } + + // If slack bus has an online generator, we're good + if (slackHasOnlineGen) { + p_slackTransferred = false; + p_currentSlackBusIdx = slackBusIdx; + return true; + } + + // Slack bus generator is offline - find the best candidate for new slack + // Look for the bus with the largest online generator capacity + int bestCandidateIdx = -1; + double maxCapacity = 0.0; + + for (i = 0; i < numBus; i++) { + if (!p_network->getActiveBus(i)) continue; + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(i).get()); + + // Skip isolated buses + if (bus->isIsolated()) continue; + + // Check if this bus has online generators with real power capacity + double capacity = bus->getOnlineGenCapacity(); + if (capacity > maxCapacity) { + maxCapacity = capacity; + bestCandidateIdx = i; + } + } + + // If no bus with generation capacity found, system cannot be solved + if (bestCandidateIdx < 0 || maxCapacity <= 0.0) { + printf("WARNING: No generator with real power capacity available after contingency\n"); + printf(" Original slack bus has no online generator and no transfer candidate found\n"); + return false; + } + + // Transfer slack to the new bus + gridpack::powerflow::PFBus *oldSlack = + dynamic_cast(p_network->getBus(slackBusIdx).get()); + gridpack::powerflow::PFBus *newSlack = + dynamic_cast(p_network->getBus(bestCandidateIdx).get()); + + oldSlack->setReferenceBus(false); + newSlack->setReferenceBus(true); + + p_slackTransferred = true; + p_currentSlackBusIdx = bestCandidateIdx; + + printf("Slack bus transferred from bus %d to bus %d (capacity: %.1f MW)\n", + oldSlack->getOriginalIndex(), newSlack->getOriginalIndex(), maxCapacity); + + return true; +} + +/** + * Restore the original slack bus after a contingency. + */ +void gridpack::powerflow::PFFactoryModule::restoreSlack() +{ + if (!p_slackTransferred) return; + + // Restore the original slack bus + if (p_originalSlackBusIdx >= 0 && p_currentSlackBusIdx >= 0 && + p_originalSlackBusIdx != p_currentSlackBusIdx) { + gridpack::powerflow::PFBus *oldSlack = + dynamic_cast(p_network->getBus(p_currentSlackBusIdx).get()); + gridpack::powerflow::PFBus *origSlack = + dynamic_cast(p_network->getBus(p_originalSlackBusIdx).get()); + + oldSlack->setReferenceBus(false); + origSlack->setReferenceBus(true); + + printf("Slack bus restored to bus %d\n", origSlack->getOriginalIndex()); + } + + p_slackTransferred = false; + p_currentSlackBusIdx = p_originalSlackBusIdx; +} + +/** + * Check if slack bus generator output exceeds its capacity (Pmax). + * Should be called after power flow solve. + * @return true if within limits, false if Pgen > Pmax + */ +bool gridpack::powerflow::PFFactoryModule::checkSlackCapacity() +{ + int numBus = p_network->numBuses(); + + // Find the current slack bus + for (int i = 0; i < numBus; i++) { + if (!p_network->getActiveBus(i)) continue; + gridpack::powerflow::PFBus *bus = + dynamic_cast(p_network->getBus(i).get()); + if (bus->getReferenceBus()) { + bool withinLimits = bus->checkGenCapacity(); + if (!withinLimits) { + double pgen = bus->getTotalGenOutput(); + double pmax = bus->getOnlineGenCapacity(); + printf("WARNING: Slack bus %d generator output (%.1f MW) exceeds capacity (%.1f MW)\n", + bus->getOriginalIndex(), pgen, pmax); + } + return withinLimits; + } + } + // No slack bus found + return false; +} + /** * Clear island detection state and restore isolated status of buses * that were marked as isolated due to islanding diff --git a/src/applications/modules/powerflow/pf_factory_module.hpp b/src/applications/modules/powerflow/pf_factory_module.hpp index 0c14defe8..a71dc4b84 100644 --- a/src/applications/modules/powerflow/pf_factory_module.hpp +++ b/src/applications/modules/powerflow/pf_factory_module.hpp @@ -110,6 +110,34 @@ class PFFactoryModule */ int getIslandCount() const; + /** + * Check if any lone buses were found in the last call to checkLoneBus + * @return true if at least one lone bus was found + */ + bool hasLoneBus() const; + + /** + * Check if the reference (slack) bus has an online generator. + * If not, transfer the slack function to the bus with the largest + * online generator capacity. + * @return true if a valid slack bus exists (or was transferred), + * false if no generator with real power capacity is available + */ + bool checkAndTransferSlack(); + + /** + * Restore the original slack bus after a contingency. + * Called by clearIslands() or unSetContingency(). + */ + void restoreSlack(); + + /** + * Check if slack bus generator output exceeds its capacity (Pmax). + * Should be called after power flow solve. + * @return true if within limits, false if Pgen > Pmax + */ + bool checkSlackCapacity(); + /** * Clear island detection state and restore isolated status of buses * that were marked as isolated due to islanding @@ -270,6 +298,10 @@ class PFFactoryModule std::vector p_saveIslandIsolatedStatus; // For island detection std::vector p_islandIsolatedBusIndices; // Local indices of buses isolated due to islanding int p_islandCount; // Number of islands detected + bool p_hasLoneBus; // Whether any lone buses were found + int p_originalSlackBusIdx; // Local index of original slack bus + int p_currentSlackBusIdx; // Local index of current slack bus (may differ after transfer) + bool p_slackTransferred; // Whether slack was transferred during contingency std::vector p_violations; From a98b00d571cd11aa187f3656929d226934de1f36 Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Thu, 29 Jan 2026 09:13:20 -0800 Subject: [PATCH 07/12] Optimize CMake builds and add Q-limit test configurations --- .../contingency_analysis/CMakeLists.txt | 95 +++++++++------ .../contingency_analysis/README.md | 56 ++++++++- .../contingencies_nk_example.xml | 114 ++++++++++++++++++ .../data_sets/input/ca/input_14_auto_n1.xml | 28 +++++ .../data_sets/input/ca/input_14_qlim.xml | 28 +++++ .../data_sets/raw/IEEE14_PTIv33_rated.raw | 85 +++++++++++++ src/applications/powerflow/CMakeLists.txt | 69 ++++++----- 7 files changed, 399 insertions(+), 76 deletions(-) create mode 100644 src/applications/data_sets/contingencies/contingencies_nk_example.xml create mode 100644 src/applications/data_sets/input/ca/input_14_auto_n1.xml create mode 100644 src/applications/data_sets/input/ca/input_14_qlim.xml create mode 100644 src/applications/data_sets/raw/IEEE14_PTIv33_rated.raw diff --git a/src/applications/contingency_analysis/CMakeLists.txt b/src/applications/contingency_analysis/CMakeLists.txt index 1b5089fd5..1e8cee1a1 100644 --- a/src/applications/contingency_analysis/CMakeLists.txt +++ b/src/applications/contingency_analysis/CMakeLists.txt @@ -11,6 +11,7 @@ # ------------------------------------------------------------- # Created May 6, 2013 by William A. Perkins # Last Change: 2017-12-08 09:37:40 d3g096 +# Updated Jan. 29, 2026 by Yousu Chen # ------------------------------------------------------------- set(target_libraries @@ -96,58 +97,72 @@ add_custom_command( DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input.euro.xml" ) -add_custom_target(ca.x.input - - COMMAND ${CMAKE_COMMAND} -E copy +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_14_auto_n1.xml" + COMMAND ${CMAKE_COMMAND} + -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/ca/input_14_auto_n1.xml" + -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_14_auto_n1.xml" + -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" + -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" + DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input_14_auto_n1.xml" + ) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml" + COMMAND ${CMAKE_COMMAND} + -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/ca/input_14_qlim.xml" + -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml" + -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" + -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" + DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input_14_qlim.xml" + ) + +# Copy data files only when source changes (avoids unnecessary copies on every build) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/IEEE14_ca.raw" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/raw/IEEE14_ca.raw ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/raw/IEEE14_ca.raw" + ) - COMMAND ${CMAKE_COMMAND} -E copy +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/contingencies_14.xml" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/contingencies/contingencies_14.xml ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/contingencies/contingencies_14.xml" + ) - COMMAND ${CMAKE_COMMAND} -E copy +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/IEEE118.raw" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/raw/IEEE118.raw" + ) - COMMAND ${CMAKE_COMMAND} -E copy +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/contingencies_118.xml" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/contingencies/contingencies_118.xml ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/contingencies/contingencies_118.xml" + ) - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/Polish_model_v23.raw - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_polish.xml - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/EuropeanOpenModel_v23.raw - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_euro.xml - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/README.md - ${CMAKE_CURRENT_BINARY_DIR} +# Note: README.md and contingencies_nk_example.xml are static files that don't +# need processing. They are installed directly from source locations. +add_custom_target(ca.x.input DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/input_14.xml - ${GRIDPACK_DATA_DIR}/raw/IEEE14_ca.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_14.xml + ${CMAKE_CURRENT_BINARY_DIR}/input_14_auto_n1.xml + ${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml ${CMAKE_CURRENT_BINARY_DIR}/input_118.xml - ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_118.xml - ${CMAKE_CURRENT_BINARY_DIR}/input.polish.xml - ${GRIDPACK_DATA_DIR}/raw/Polish_model_v23.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_polish.xml - ${CMAKE_CURRENT_BINARY_DIR}/input.euro.xml - ${GRIDPACK_DATA_DIR}/raw/EuropeanOpenModel_v23.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_euro.xml - ${CMAKE_CURRENT_SOURCE_DIR}/README.md + ${CMAKE_CURRENT_BINARY_DIR}/IEEE14_ca.raw + ${CMAKE_CURRENT_BINARY_DIR}/IEEE118.raw + ${CMAKE_CURRENT_BINARY_DIR}/contingencies_14.xml + ${CMAKE_CURRENT_BINARY_DIR}/contingencies_118.xml ) add_dependencies(ca.x ca.x.input) @@ -157,11 +172,14 @@ add_dependencies(ca.x ca.x.input) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.install.in ${CMAKE_CURRENT_BINARY_DIR}/CMakeLists.txt @ONLY) -install(FILES +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/CMakeLists.txt ${CMAKE_CURRENT_BINARY_DIR}/input_14.xml + ${CMAKE_CURRENT_BINARY_DIR}/input_14_auto_n1.xml + ${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml ${GRIDPACK_DATA_DIR}/raw/IEEE14_ca.raw ${GRIDPACK_DATA_DIR}/contingencies/contingencies_14.xml + ${GRIDPACK_DATA_DIR}/contingencies/contingencies_nk_example.xml ${CMAKE_CURRENT_BINARY_DIR}/input_118.xml ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw ${GRIDPACK_DATA_DIR}/contingencies/contingencies_118.xml @@ -174,6 +192,7 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ca_driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ca_driver.hpp ${CMAKE_CURRENT_SOURCE_DIR}/ca_main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION share/gridpack/example/contingency_analysis ) @@ -184,4 +203,6 @@ install(TARGETS ca.x DESTINATION bin) # ------------------------------------------------------------- set(TIMEOUT 120.0) gridpack_add_run_test("contingency_analysis" ca.x input_14.xml) +gridpack_add_run_test("contingency_analysis_auto_n1" ca.x input_14_auto_n1.xml) +gridpack_add_run_test("contingency_analysis_qlim" ca.x input_14_qlim.xml) diff --git a/src/applications/contingency_analysis/README.md b/src/applications/contingency_analysis/README.md index 5d0445a45..f95934f29 100644 --- a/src/applications/contingency_analysis/README.md +++ b/src/applications/contingency_analysis/README.md @@ -47,6 +47,41 @@ See `contingencies_nk_example.xml` for examples of N-1, N-2, and N-3 contingency --- +## Advanced Features + +### Automatic Slack Bus Transfer + +When a generator contingency trips the slack bus generator, the application automatically transfers the slack bus role to the largest remaining online generator. This mimics commercial power flow tools (PSS/E, PowerWorld) behavior. + +**Example output:** +``` +Slack bus transferred from bus 69 to bus 80 (capacity: 577.0 MW) +``` + +After the contingency analysis completes, the slack bus is restored to its original location. + +### Slack Capacity Check + +After the power flow solves, the application checks if the slack bus generator output exceeds its Pmax rating. If the required generation exceeds capacity, the contingency is marked as failed with a warning message: + +``` +WARNING: Slack bus 80 generator output (475.3 MW) exceeds capacity (400.0 MW) +Insufficient generation capacity for contingency GN_69_1 +``` + +This ensures realistic results - a contingency that requires more generation than available capacity is properly flagged as a failure. + +### Island Detection + +The application detects network islands (disconnected portions) caused by branch contingencies: + +- **Lone bus**: A bus with no active branches is marked as isolated +- **Island**: A group of buses disconnected from the main network + +When isolation is detected, the isolated buses are excluded from the power flow solution, and a warning is added to the results. The main network portion is still solved if possible. + +--- + ## Output Files The contingency analysis calculation produces a number of files summarize the @@ -55,10 +90,23 @@ for contingencies that ran to completion are included. Calculations that failed either because of a numerical instability or because the calculations failed to converge are not included in the results. The output files are described below. -**success.txt**: This file summarizes that results of each contingency and -reports 1) whether the contingency calculation successfully ran to completion -and 2) whether a violation was found. If a violation is found, the calculation -reports on whether it was a on a bus, on a branch, or both. +**success.txt**: This file summarizes the results of each contingency and +reports 1) whether the contingency calculation successfully ran to completion, +2) whether a violation was found (bus, branch, or both), and 3) whether any +buses were isolated. + +Example output: +``` +contingency: 1 success: true violation: none +contingency: 2 success: true violation: branch +contingency: 3 success: true violation: none warning: isolated +contingency: 4 success: false +``` + +- `success: true` - Power flow converged and slack capacity is within limits +- `success: false` - Power flow failed, island detected, or slack capacity exceeded +- `violation: none/bus/branch` - Whether voltage or thermal limits were violated +- `warning: isolated` - One or more buses were isolated (lone bus or island) **vmag.txt**: This file contains the average value of the voltage magnitude for non-PV buses. It also contains the RMS fluctuations of the voltage magnitude diff --git a/src/applications/data_sets/contingencies/contingencies_nk_example.xml b/src/applications/data_sets/contingencies/contingencies_nk_example.xml new file mode 100644 index 000000000..6293b8cae --- /dev/null +++ b/src/applications/data_sets/contingencies/contingencies_nk_example.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + Line + BR_1_2_1 + 1 2 + 1 + + + + + + + + Generator + GN_1_1 + 1 + 1 + + + + + + + + + Line + BR_1_2_1+BR_1_2_2 + 1 2 1 2 + 1 2 + + + + + Line + BR_1_2_1+BR_3_4_1 + 1 2 3 4 + 1 1 + + + + + + + + Generator + GN_1_1+GN_1_2 + 1 1 + 1 2 + + + + + Generator + GN_1_1+GN_2_1 + 1 2 + 1 1 + + + + + + + Line + BR_1_2_1+BR_2_3_1+BR_3_4_1 + 1 2 2 3 3 4 + 1 1 1 + + + + + + + Generator + GN_1_1+GN_2_1+GN_3_1 + 1 2 3 + 1 1 1 + + + + + diff --git a/src/applications/data_sets/input/ca/input_14_auto_n1.xml b/src/applications/data_sets/input/ca/input_14_auto_n1.xml new file mode 100644 index 000000000..65577b442 --- /dev/null +++ b/src/applications/data_sets/input/ca/input_14_auto_n1.xml @@ -0,0 +1,28 @@ + + + + + false + true + true + 1 + 1.1 + 0.9 + false + + + IEEE14_ca.raw + 50 + 1.0e-3 + false + + pre_ + + -ksp_type richardson + -pc_type lu + -pc_factor_mat_solver_type superlu_dist + -ksp_max_it 1 + + + + diff --git a/src/applications/data_sets/input/ca/input_14_qlim.xml b/src/applications/data_sets/input/ca/input_14_qlim.xml new file mode 100644 index 000000000..52d38cdcf --- /dev/null +++ b/src/applications/data_sets/input/ca/input_14_qlim.xml @@ -0,0 +1,28 @@ + + + + + false + contingencies_14.xml + 1 + 1.1 + 0.9 + true + + + IEEE14_ca.raw + 50 + 1.0e-3 + true + 3 + + pre_ + + -ksp_type richardson + -pc_type lu + -pc_factor_mat_solver_type superlu_dist + -ksp_max_it 1 + + + + diff --git a/src/applications/data_sets/raw/IEEE14_PTIv33_rated.raw b/src/applications/data_sets/raw/IEEE14_PTIv33_rated.raw new file mode 100644 index 000000000..3aece0d23 --- /dev/null +++ b/src/applications/data_sets/raw/IEEE14_PTIv33_rated.raw @@ -0,0 +1,85 @@ + 0, 100.00, 33, 0, 0, 60.00 / IEEE 14-BUS WITH THERMAL RATINGS + IEEE 14-BUS SYSTEM WITH SYNTHETIC LINE RATINGS FOR CONTINGENCY ANALYSIS + + 1,'BUS-1 ', 100.0000,3, 1, 2, 1,1.06000, 0.0000 + 2,'BUS-2 ', 100.0000,2, 1, 2, 1,1.04500, -4.9800 + 3,'BUS-3 ', 100.0000,2, 1, 2, 1,1.01000, -12.7200 + 4,'BUS-4 ', 100.0000,1, 1, 2, 1,1.01900, -10.3300 + 5,'BUS-5 ', 100.0000,1, 1, 2, 1,1.02000, -8.7800 + 6,'BUS-6 ', 100.0000,2, 1, 2, 1,1.07000, -14.2200 + 7,'BUS-7 ', 100.0000,1, 1, 2, 1,1.06200, -13.3700 + 8,'BUS-8 ', 100.0000,2, 1, 2, 1,1.09000, -13.3600 + 9,'BUS-9 ', 100.0000,1, 1, 2, 1,1.05600, -14.9400 + 10,'BUS-10 ', 100.0000,1, 1, 2, 1,1.05100, -15.1000 + 11,'BUS-11 ', 100.0000,1, 1, 2, 1,1.05700, -14.7900 + 12,'BUS-12 ', 100.0000,1, 1, 2, 1,1.05500, -15.0700 + 13,'BUS-13 ', 100.0000,1, 1, 2, 1,1.05000, -15.1600 + 14,'BUS-14 ', 100.0000,1, 1, 2, 1,1.03600, -16.0400 +0 / END OF BUS DATA, BEGIN LOAD DATA + 2,'1 ',1, 1, 2, 21.700, 12.700, 0.000, 0.000, 0.000, -0.000, 1,1 + 3,'1 ',1, 1, 2, 94.200, 19.000, 0.000, 0.000, 0.000, -0.000, 1,1 + 4,'1 ',1, 1, 2, 47.800, -3.900, 0.000, 0.000, 0.000, -0.000, 1,1 + 5,'1 ',1, 1, 2, 7.600, 1.600, 0.000, 0.000, 0.000, -0.000, 1,1 + 6,'1 ',1, 1, 2, 11.200, 7.500, 0.000, 0.000, 0.000, -0.000, 1,1 + 9,'1 ',1, 1, 2, 29.500, 16.600, 0.000, 0.000, 0.000, -0.000, 1,1 + 10,'1 ',1, 1, 2, 9.000, 5.800, 0.000, 0.000, 0.000, -0.000, 1,1 + 11,'1 ',1, 1, 2, 3.500, 1.800, 0.000, 0.000, 0.000, -0.000, 1,1 + 12,'1 ',1, 1, 2, 6.100, 1.600, 0.000, 0.000, 0.000, -0.000, 1,1 + 13,'1 ',1, 1, 2, 13.500, 5.800, 0.000, 0.000, 0.000, -0.000, 1,1 + 14,'1 ',1, 1, 2, 14.900, 5.000, 0.000, 0.000, 0.000, -0.000, 1,1 +0 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA + 9,' 1', 1, 0.000, 19.000 +0 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA + 1,'1 ', 232.400, -16.900, 99990.002, -9999.000,1.06000, 0, 100.000, 0.00000, 1.00000, 0.00000, 0.00000,1.00000,1, 100.0, 500.000, 50.000, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,0, 1.0000 + 2,'1 ', 40.000, 42.400, 50.000, -40.000,1.04500, 0, 100.000, 0.00000, 1.00000, 0.00000, 0.00000,1.00000,1, 100.0, 100.000, 20.000, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,0, 1.0000 + 3,'1 ', 0.000, 23.400, 40.000, 0.000,1.01000, 0, 100.000, 0.00000, 1.00000, 0.00000, 0.00000,1.00000,1, 100.0, 50.000, 0.000, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,0, 1.0000 + 6,'1 ', 0.000, 12.200, 24.000, -6.000,1.07000, 0, 100.000, 0.00000, 1.00000, 0.00000, 0.00000,1.00000,1, 100.0, 50.000, 0.000, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,0, 1.0000 + 8,'1 ', 0.000, 17.400, 24.000, -6.000,1.09000, 0, 100.000, 0.00000, 1.00000, 0.00000, 0.00000,1.00000,1, 100.0, 50.000, 0.000, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,0, 1.0000 +0 / END OF GENERATOR DATA, BEGIN BRANCH DATA + 1, 2,'BL', 0.01938, 0.05917,0.05280, 200.00, 200.00, 200.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 1, 5,'BL', 0.05403, 0.22304,0.04920, 90.00, 90.00, 90.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 2, 3,'BL', 0.04699, 0.19797,0.04380, 90.00, 90.00, 90.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 2, 4,'BL', 0.05811, 0.17632,0.03400, 70.00, 70.00, 70.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 2, 5,'BL', 0.05695, 0.17388,0.03460, 70.00, 70.00, 70.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 3, 4,'BL', 0.06701, 0.17103,0.01280, 70.00, 70.00, 70.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 4, 5,'BL', 0.01335, 0.04211,0.00000, 130.00, 130.00, 130.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 6, 11,'BL', 0.09498, 0.19890,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 6, 12,'BL', 0.12291, 0.25581,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 6, 13,'BL', 0.06615, 0.13027,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 7, 8,'BL', 0.00000, 0.17615,0.00000, 60.00, 60.00, 60.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 7, 9,'BL', 0.00000, 0.11001,0.00000, 60.00, 60.00, 60.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 9, 10,'BL', 0.03181, 0.08450,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 9, 14,'BL', 0.12711, 0.27038,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 10, 11,'BL', 0.08205, 0.19207,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 12, 13,'BL', 0.22092, 0.19988,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 13, 14,'BL', 0.17093, 0.34802,0.00000, 45.00, 45.00, 45.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.0, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 +0 / END OF BRANCH DATA, BEGIN TRANSFORMER DATA + 4, 7, 0,'BL',1,1,1, 0.00000, 0.00000,2,' ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 0.00000, 0.20912, 100.00 +0.97800,100.000, 0.000, 60.00, 60.00, 60.00,0, 0, 1.50000, 0.51000, 1.50000, 0.51000,159, 0, 0.00000, 0.00000 +1.00000,100.000 + 4, 9, 0,'BL',1,1,1, 0.00000, 0.00000,2,' ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 0.00000, 0.55618, 100.00 +0.96900,100.000, 0.000, 60.00, 60.00, 60.00,0, 0, 1.50000, 0.51000, 1.50000, 0.51000,159, 0, 0.00000, 0.00000 +1.00000,100.000 + 5, 6, 0,'BL',1,1,1, 0.00000, 0.00000,2,' ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000 + 0.00000, 0.25202, 100.00 +0.93200,100.000, 0.000, 60.00, 60.00, 60.00,0, 0, 1.50000, 0.51000, 1.50000, 0.51000,159, 0, 0.00000, 0.00000 +1.00000,100.000 +0 / END OF TRANSFORMER DATA, BEGIN AREA DATA + 1, 0, 0.000, 3.000,' ' +0 / END OF AREA DATA, BEGIN TWO-TERMINAL DC DATA +0 / END OF TWO-TERMINAL DC DATA, BEGIN VOLTAGE SOURCE CONVERTER DATA +0 / END OF VOLTAGE SOURCE CONVERTER DATA, BEGIN IMPEDANCE CORRECTION DATA +0 / END OF IMPEDANCE CORRECTION DATA, BEGIN MULTI-TERMINAL DC DATA +0 / END OF MULTI-TERMINAL DC DATA, BEGIN MULTI-SECTION LINE DATA +0 / END OF MULTI-SECTION LINE DATA, BEGIN ZONE DATA + 2,'ZONE_2 ' +0 / END OF ZONE DATA, BEGIN INTER-AREA TRANSFER DATA +0 / END OF INTER-AREA TRANSFER DATA, BEGIN OWNER DATA + 1,'1' +0 / END OF OWNER DATA, BEGIN FACTS CONTROL DEVICE DATA +0 / END OF FACTS CONTROL DEVICE DATA, BEGIN SWITCHED SHUNT DATA +0 /END OF SWITCHED SHUNT DATA, BEGIN GNE DEVICE DATA +0 /END OF GNE DEVICE DATA +Q diff --git a/src/applications/powerflow/CMakeLists.txt b/src/applications/powerflow/CMakeLists.txt index 9cce65da3..e04a522f2 100644 --- a/src/applications/powerflow/CMakeLists.txt +++ b/src/applications/powerflow/CMakeLists.txt @@ -11,6 +11,7 @@ # ------------------------------------------------------------- # Created May 6, 2013 by William A. Perkins # Last Change: 2017-12-07 08:54:26 d3g096 +# Updated Jan. 29, 2026 by Yousu Chen # ------------------------------------------------------------- set(target_libraries @@ -119,52 +120,49 @@ add_custom_command( DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_240_v36.xml" ) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml" + COMMAND ${CMAKE_COMMAND} + -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/powerflow/input_14_qlim.xml" + -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml" + -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" + -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" + DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_14_qlim.xml" + ) -add_custom_target(pf.x.input - - COMMAND ${CMAKE_COMMAND} -E copy +# Copy raw files only when source changes (avoids unnecessary copies on every build) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/IEEE14.raw" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/raw/IEEE14.raw ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/raw/IEEE14.raw" + ) - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/gridpack.petscrc - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/IEEE118.raw" + COMMAND ${CMAKE_COMMAND} -E copy ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${GRIDPACK_DATA_DIR}/raw/IEEE118.raw" + ) - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/Polish_model_v23.raw - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/EuropeanOpenModel_v23.raw - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/IEEE39bus_original_Modified_v36_1_ibr.raw - ${CMAKE_CURRENT_BINARY_DIR} - - COMMAND ${CMAKE_COMMAND} -E copy - ${GRIDPACK_DATA_DIR}/raw/240busWECC_2018_PSS_PQLoad_fixedshunt_noremotebus_yuan_v36.raw +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gridpack.petscrc" + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/gridpack.petscrc ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/gridpack.petscrc" + ) - DEPENDS +add_custom_target(pf.x.input + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/input_14.xml - ${GRIDPACK_DATA_DIR}/raw/IEEE14.raw - ${CMAKE_CURRENT_SOURCE_DIR}/gridpack.petscrc + ${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml ${CMAKE_CURRENT_BINARY_DIR}/input_118.xml - ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw - ${CMAKE_CURRENT_BINARY_DIR}/input_polish.xml - ${GRIDPACK_DATA_DIR}/raw/Polish_model_v23.raw - ${CMAKE_CURRENT_BINARY_DIR}/input_european.xml - ${GRIDPACK_DATA_DIR}/raw/EuropeanOpenModel_v23.raw - ${CMAKE_CURRENT_BINARY_DIR}/input_ieee39_v36.xml - ${GRIDPACK_DATA_DIR}/raw/IEEE39bus_original_Modified_v36_1_ibr.raw - ${CMAKE_CURRENT_BINARY_DIR}/input_240_v36.xml - ${GRIDPACK_DATA_DIR}/raw/240busWECC_2018_PSS_PQLoad_fixedshunt_noremotebus_yuan_v36.raw - + ${CMAKE_CURRENT_BINARY_DIR}/IEEE14.raw + ${CMAKE_CURRENT_BINARY_DIR}/IEEE118.raw + ${CMAKE_CURRENT_BINARY_DIR}/gridpack.petscrc ) add_dependencies(pf.x pf.x.input) @@ -178,4 +176,5 @@ install(TARGETS pf.x DESTINATION bin) # Create simple test that runs powerflow code # ------------------------------------------------------------- gridpack_add_run_test("powerflow" pf.x "input_14.xml") +gridpack_add_run_test("powerflow_qlim" pf.x "input_14_qlim.xml") From 2cc4814b216efd3c1d726c6247831eb36aeef09c Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Thu, 29 Jan 2026 14:00:40 -0800 Subject: [PATCH 08/12] Fix trailing space in contingency output filenames --- src/applications/contingency_analysis/ca_driver.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index a7ea0366e..43a11ccdf 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -722,7 +722,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // calculation runs out of task, nextTask will return false. while (taskmgr.nextTask(task_comm, &task_id)) { printf("Executing task %d on process %d\n",task_id,world.rank()); - sprintf(sbuf,"%s.out",events[task_id].p_name.c_str()); + // Trim trailing spaces from contingency name for filename + std::string fname = events[task_id].p_name; + size_t end = fname.find_last_not_of(' '); + if (end != std::string::npos) fname = fname.substr(0, end + 1); + sprintf(sbuf,"%s.out",fname.c_str()); // Open a new file, based on the contingency name, to store results from // this particular contingency calculation if (print_calcs) pf_app.open(sbuf); From 2ae978d16dfb6047bfe9946539861356df07a6dc Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Thu, 29 Jan 2026 16:57:55 -0800 Subject: [PATCH 09/12] added missing input file --- .../input/powerflow/input_14_qlim.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/applications/data_sets/input/powerflow/input_14_qlim.xml diff --git a/src/applications/data_sets/input/powerflow/input_14_qlim.xml b/src/applications/data_sets/input/powerflow/input_14_qlim.xml new file mode 100644 index 000000000..154f9d3fd --- /dev/null +++ b/src/applications/data_sets/input/powerflow/input_14_qlim.xml @@ -0,0 +1,19 @@ + + + + + IEEE14.raw + 50 + 1.0e-6 + true + 3 + + + -ksp_type richardson + -pc_type lu + -pc_factor_mat_solver_type superlu_dist + -ksp_max_it 1 + + + + From 8c4a8e57fabe587794d526d3ac3de1179677fdfd Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Thu, 29 Jan 2026 17:33:19 -0800 Subject: [PATCH 10/12] Remove large model references from CMake files --- .../contingency_analysis/CMakeLists.txt | 26 ------------ src/applications/powerflow/CMakeLists.txt | 40 ------------------- 2 files changed, 66 deletions(-) diff --git a/src/applications/contingency_analysis/CMakeLists.txt b/src/applications/contingency_analysis/CMakeLists.txt index 1e8cee1a1..8b9e8f59b 100644 --- a/src/applications/contingency_analysis/CMakeLists.txt +++ b/src/applications/contingency_analysis/CMakeLists.txt @@ -77,26 +77,6 @@ add_custom_command( DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input_118.xml" ) -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input.polish.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/ca/input.polish.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input.polish.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input.polish.xml" - ) - -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input.euro.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/ca/input.euro.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input.euro.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/ca/input.euro.xml" - ) - add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_14_auto_n1.xml" COMMAND ${CMAKE_COMMAND} @@ -183,12 +163,6 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/input_118.xml ${GRIDPACK_DATA_DIR}/raw/IEEE118.raw ${GRIDPACK_DATA_DIR}/contingencies/contingencies_118.xml - ${CMAKE_CURRENT_BINARY_DIR}/input.polish.xml - ${GRIDPACK_DATA_DIR}/raw/Polish_model_v23.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_polish.xml - ${CMAKE_CURRENT_BINARY_DIR}/input.euro.xml - ${GRIDPACK_DATA_DIR}/raw/EuropeanOpenModel_v23.raw - ${GRIDPACK_DATA_DIR}/contingencies/contingencies_euro.xml ${CMAKE_CURRENT_SOURCE_DIR}/ca_driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ca_driver.hpp ${CMAKE_CURRENT_SOURCE_DIR}/ca_main.cpp diff --git a/src/applications/powerflow/CMakeLists.txt b/src/applications/powerflow/CMakeLists.txt index e04a522f2..095decf33 100644 --- a/src/applications/powerflow/CMakeLists.txt +++ b/src/applications/powerflow/CMakeLists.txt @@ -80,46 +80,6 @@ add_custom_command( DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_118.xml" ) -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_polish.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/powerflow/input_polish.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_polish.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_polish.xml" - ) - -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_european.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/powerflow/input_european.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_european.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_european.xml" - ) - -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_ieee39_v36.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/powerflow/input_ieee39_v36.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_ieee39_v36.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_ieee39_v36.xml" - ) - -add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_240_v36.xml" - COMMAND ${CMAKE_COMMAND} - -D INPUT:PATH="${GRIDPACK_DATA_DIR}/input/powerflow/input_240_v36.xml" - -D OUTPUT:PATH="${CMAKE_CURRENT_BINARY_DIR}/input_240_v36.xml" - -D PKG:STRING="${GRIDPACK_MATSOLVER_PKG}" - -P "${PROJECT_SOURCE_DIR}/cmake-modules/set_lu_solver_pkg.cmake" - DEPENDS "${GRIDPACK_DATA_DIR}/input/powerflow/input_240_v36.xml" - ) - add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/input_14_qlim.xml" COMMAND ${CMAKE_COMMAND} From 6030b9555edac36ead043b061fc17570f64db620 Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Fri, 30 Jan 2026 13:31:20 -0800 Subject: [PATCH 11/12] increase buffer size for priting out more than 10 generators at the same bus --- src/applications/modules/powerflow/pf_app_module.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/modules/powerflow/pf_app_module.cpp b/src/applications/modules/powerflow/pf_app_module.cpp index d5ec9bc6b..c4788c668 100644 --- a/src/applications/modules/powerflow/pf_app_module.cpp +++ b/src/applications/modules/powerflow/pf_app_module.cpp @@ -253,7 +253,8 @@ void gridpack::powerflow::PFAppModule::readNetwork( timer->stop(t_pti); // Create serial IO object to export data from buses - p_busIO.reset(new gridpack::serial_io::SerialBusIO(512,network)); + // Increased buffer size from 512 to 2048 to handle buses with many generators + p_busIO.reset(new gridpack::serial_io::SerialBusIO(2048,network)); // Create serial IO object to export data from branches // Increased buffer size from 512 to 2048 to handle branches with many parallel lines From bf0548f6cc4f48f77ffe18af466babf8705c9d52 Mon Sep 17 00:00:00 2001 From: "yousu.chen@pnnl.gov" Date: Sat, 31 Jan 2026 13:16:29 -0800 Subject: [PATCH 12/12] Add RMPCT-based Q distribution for multi-generator buses --- .../components/pf_matrix/pf_components.cpp | 242 ++++++++++++------ .../components/pf_matrix/pf_components.hpp | 18 +- .../contingency_analysis/ca_driver.cpp | 6 + .../contingency_analysis/ca_driver.hpp | 6 + 4 files changed, 195 insertions(+), 77 deletions(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 1b3c15540..15be26c29 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -13,7 +13,14 @@ * Conversion of constant current, constant admittance values from raw file * to constant power * @date 2022-12-23 - + * + * @updated Yousu Chen + * - Improved Q-limit handling with iterative PV-PQ conversion + * - Added island detection function + * - Automatic slack bus transfer for contingency analysis + * - RMPCT-based reactive power distribution for multi-generator buses + * @date 2026-01-31 + * * @brief Methods used in power flow application * * @@ -225,86 +232,169 @@ bool gridpack::powerflow::PFBus::vectorValues(RealType *values) } /** - * Check QLIM - * @return false: violations exist - * @return true: no violations - * + * Check QLIM with RMPCT-based iterative Q distribution (PSS/E style) + * + * Algorithm: + * 1. Calculate required Q for the bus + * 2. Distribute Q among generators using RMPCT ratios + * 3. Check each generator against its limits + * 4. If any generator exceeds limits, clamp it and redistribute to others + * 5. Only convert PV->PQ if all generators are at limits and Q_req still not met + * + * @return false: violations exist (Q limits hit, bus converted to PQ) + * @return true: no violations (all generators within limits) */ bool gridpack::powerflow::PFBus::chkQlim(void) { - if (p_isPV) { - double qmax, qmin, ppl; - qmax = 0.0; - qmin = 0.0; - ppl = 0.0; - for (int i=0; i at_limit(ngen, false); + std::vector q_assigned(ngen, 0.0); + + // Save original state for restoration + p_save2isPV = p_isPV; + + // Iterative distribution loop + const int MAX_ITER = 20; // Prevent infinite loops + bool converged = false; + + for (int iter = 0; iter < MAX_ITER && !converged; iter++) { + // Calculate total RMPCT for active (non-limited) generators + double total_rmpct = 0.0; + double total_qmax = 0.0; + int active_count = 0; + + for (int i = 0; i < ngen; i++) { + if (p_gstatus[i] == 1 && !at_limit[i]) { + total_rmpct += p_rmpct[i]; + total_qmax += p_qmax[i]; + active_count++; } } - std::vector > branches; - getNeighborBranches(branches); - int size = branches.size(); - double P, Q, p, q; - int ngen=p_pFac.size(); - double pl =0.0; - double ql =0.0; - for (int i=0; iPQ -// Generator remains online with Q clamped to limit. -// setSBus() will be called at start of next iteration to update p_Q0. -// - if (qval > qmax ) { - printf("\nWarning: Gen(s) at bus %d exceeds the QMAX %8.3f vs %8.3f, converted to PQ bus\n", getOriginalIndex(),qval, qmax); - p_save2isPV = p_isPV; - p_isPV = false; - p_type = 1; // Change bus type from PV(2) to PQ(1) - if (p_PV_ptr) *p_PV_ptr = false; - // Generator stays online, clamp Q to limit - for (int i=0; i p_qmax[i]) { + q_assigned[i] = p_qmax[i]; + at_limit[i] = true; + converged = false; // Need another iteration + } else if (share < p_qmin[i]) { + q_assigned[i] = p_qmin[i]; + at_limit[i] = true; + converged = false; // Need another iteration + } else { + q_assigned[i] = share; } - return true; - } else if (qval < qmin) { - printf("\nWarning: Gen(s) at bus %d exceeds the QMIN %8.3f vs %8.3f, converted to PQ bus\n", getOriginalIndex(),qval, qmin); - p_save2isPV = p_isPV; - p_isPV = false; - p_type = 1; // Change bus type from PV(2) to PQ(1) - if (p_PV_ptr) *p_PV_ptr = false; - // Generator stays online, clamp Q to limit - for (int i=0; i Q_max_total) { + // Exceeds total Qmax - need to convert to PQ + printf("\nWarning: Bus %d Q requirement (%8.3f) exceeds total QMAX (%8.3f), converting to PQ\n", + getOriginalIndex(), Q_required, Q_max_total); + need_pv_to_pq = true; + // Clamp all generators to Qmax + for (int i = 0; i < ngen; i++) { + if (p_gstatus[i] == 1) { + q_assigned[i] = p_qmax[i]; } - return true; - } else { - if (p_PV_ptr) *p_PV_ptr = p_isPV; - return false; } - } else { - if (p_PV_ptr) *p_PV_ptr = p_isPV; - return false; - } + } else if (Q_required < Q_min_total) { + // Below total Qmin - need to convert to PQ + printf("\nWarning: Bus %d Q requirement (%8.3f) below total QMIN (%8.3f), converting to PQ\n", + getOriginalIndex(), Q_required, Q_min_total); + need_pv_to_pq = true; + // Clamp all generators to Qmin + for (int i = 0; i < ngen; i++) { + if (p_gstatus[i] == 1) { + q_assigned[i] = p_qmin[i]; + } + } + } + + // Apply the Q assignments to generators + for (int i = 0; i < ngen; i++) { + p_gstatus_save.push_back(p_gstatus[i]); + if (p_gstatus[i] == 1) { + p_qg[i] = q_assigned[i]; + } + } + + // Convert PV to PQ if needed + if (need_pv_to_pq) { + p_isPV = false; + p_type = 1; // Change bus type from PV(2) to PQ(1) + if (p_PV_ptr) *p_PV_ptr = false; + return true; // Violation found + } + + // Bus stays PV - Q was successfully distributed within limits if (p_PV_ptr) *p_PV_ptr = p_isPV; - return false; + return false; // No violation } /** @@ -465,6 +555,7 @@ void gridpack::powerflow::PFBus::load( p_qmax.clear(); p_qmin_orig.clear(); p_qmax_orig.clear(); + p_rmpct.clear(); p_gid.clear(); p_pt.clear(); p_pb.clear(); @@ -517,8 +608,12 @@ void gridpack::powerflow::PFBus::load( lgen = lgen && data->getValue(GENERATOR_QMIN, &qmin,i); double pt = 0.0; double pb = 0.0; + double rmpct = 100.0; // Default RMPCT value per PSS/E ok = data->getValue(GENERATOR_PMAX,&pt,i); ok = data->getValue(GENERATOR_PMIN,&pb,i); + // Try to get RMPCT, use default 100.0 if not available + data->getValue(GENERATOR_RMPCT, &rmpct, i); + if (rmpct < 0.0) rmpct = 0.0; // Treat negative as zero if (lgen) { p_gstatus.push_back(gstatus); if (gstatus == 0) { @@ -537,6 +632,7 @@ void gridpack::powerflow::PFBus::load( p_qmin_orig.push_back(qmin); p_pt.push_back(pt); p_pb.push_back(pb); + p_rmpct.push_back(gstatus ? rmpct : 0.0); // Zero RMPCT for offline generators if(gstatus) { p_pFac.push_back(pt - pb); pcaptot += pt - pb; @@ -1201,7 +1297,8 @@ bool gridpack::powerflow::PFBus::serialWrite(char *string, const int bufsize, } else if (p_isPV) { if(p_gstatus[i]) { pval = p_pg[i]; - qval = p_pFac[i]*(p_Qinj*p_sbase+ql); + // Use p_qg which is set by chkQlim() with RMPCT-based distribution + qval = p_qg[i]; } else { pval = 0.0; qval = 0.0; @@ -1404,7 +1501,8 @@ void gridpack::powerflow::PFBus::saveData( if (!data->setValue("GENERATOR_PF_PGEN",p_pg[i]/p_sbase,i)) { data->addValue("GENERATOR_PF_PGEN",p_pg[i]/p_sbase,i); } - rval = p_pFac[i]*(p_Qinj+ql/p_sbase); + // Use p_qg which is set by chkQlim() with RMPCT-based distribution + rval = p_qg[i]/p_sbase; if (!data->setValue("GENERATOR_PF_QGEN",rval,i)) { data->addValue("GENERATOR_PF_QGEN",rval,i); } diff --git a/src/applications/components/pf_matrix/pf_components.hpp b/src/applications/components/pf_matrix/pf_components.hpp index 51e2c1f69..3ee1d9dd2 100644 --- a/src/applications/components/pf_matrix/pf_components.hpp +++ b/src/applications/components/pf_matrix/pf_components.hpp @@ -8,10 +8,17 @@ * @file pf_components.hpp * @author Bruce Palmer * @date 2016-07-14 13:27:00 d3g096 - * - * @brief - * - * + * + * @updated Yousu Chen + * - Improved Q-limit handling with iterative PV-PQ conversion + * - Added island detection function + * - Automatic slack bus transfer for contingency analysis + * - RMPCT-based reactive power distribution for multi-generator buses + * @date 2026-01-31 + * + * @brief + * + * */ // ------------------------------------------------------------- @@ -492,6 +499,7 @@ class PFBus std::vector p_gstatus_save; std::vector p_qmax,p_qmin; std::vector p_qmax_orig, p_qmin_orig, p_pFac_orig; + std::vector p_rmpct; // RMPCT: reactive power participation factor (PSS/E) std::vector p_vs; std::vector p_gid; std::vector p_pt; @@ -548,7 +556,7 @@ class PFBus & p_P0 & p_Q0 & p_angle & p_voltage & p_pg & p_qg & p_pFac & p_qmin & p_qmax - & p_qmin_orig & p_qmax_orig & p_pFac_orig + & p_qmin_orig & p_qmax_orig & p_pFac_orig & p_rmpct & p_saveQg & p_gstatus & p_vs & p_gid diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 43a11ccdf..3817f23cf 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -9,6 +9,12 @@ * @author Bruce Palmer * @date 2017-12-08 13:12:46 d3g096 * + * @updated Yousu Chen + * - N-1 auto-generation for branch and generator contingencies + * - Automatic slack bus transfer and capacity check + * - Q-limit support integration + * @date 2026-01-31 + * * @brief Driver for contingency analysis calculation that make use of the * powerflow module to implement individual power flow simulations for * each contingency. The different contingencies are distributed across diff --git a/src/applications/contingency_analysis/ca_driver.hpp b/src/applications/contingency_analysis/ca_driver.hpp index 68904bc08..141b802a7 100644 --- a/src/applications/contingency_analysis/ca_driver.hpp +++ b/src/applications/contingency_analysis/ca_driver.hpp @@ -9,6 +9,12 @@ * @author Bruce Palmer * @date February 10, 2014 * + * @updated Yousu Chen + * - N-1 auto-generation for branch and generator contingencies + * - Automatic slack bus transfer and capacity check + * - Q-limit support integration + * @date 2026-01-31 + * * @brief * *