From 7a7aa325cb7b5cf3d3bafb3eee3379def3a9d8c9 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Tue, 16 Jun 2026 22:10:08 -0700 Subject: [PATCH 01/31] Make StatBlock summary output a runtime opt-out via writeStats XML option --- .../contingency_analysis/ca_driver.cpp | 647 +++++++++--------- 1 file changed, 314 insertions(+), 333 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 88d38520..dedd9659 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -29,10 +29,14 @@ #include "gridpack/utilities/results_exporter.hpp" #include "ca_driver.hpp" +#include #include #define USE_SUCCESS -#define USE_STATBLOCK +// Statistical-summary output (vmag.txt, pflow.txt, etc.) used to be controlled +// by a USE_STATBLOCK build-time macro; it is now a runtime XML option, +// `Configuration.Contingency_analysis.writeStats`, defaulting to true to +// preserve existing behavior. // Sets up multiple communicators so that individual contingency calculations // can be run concurrently @@ -366,6 +370,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) print_calcs = true; } } + // Statistical-summary output (vmag.txt, pflow.txt, etc. via StatBlock). + // Default true to preserve existing behavior; set false to skip the + // per-case StatBlock work and the 13 post-loop global writes. + bool write_stats = true; + if (cursor->get("writeStats",&tmp_bool)) { + util.toLower(tmp_bool); + write_stats = (tmp_bool != "false"); + } if (!cursor->get("groupSize",&grp_size)) { grp_size = 1; } @@ -579,164 +591,153 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) int nbus = pf_network->totalBuses(); // Get bus voltage information for base case int i, j; -#ifdef USE_STATBLOCK + // StatBlock objects and the per-case scratch vectors live across the + // contingency loop so they are declared up here, regardless of whether + // statistics output is enabled. + boost::scoped_ptr vmag_stats; + boost::scoped_ptr vang_stats; + boost::scoped_ptr pgen_stats; + boost::scoped_ptr qgen_stats; + boost::scoped_ptr pflow_stats; + boost::scoped_ptr qflow_stats; + boost::scoped_ptr perf_stats; + std::vector v_vals; + int nsize = 0; + std::vector vmag, vang, pgen, qgen, pflow, qflow, perf; + std::vector mask, mag_mask; int t_store = timer->createCategory("Store Statistics"); - timer->start(t_store); - std::vector v_vals = pf_app.writeBusString("vr_str"); - int nsize = v_vals.size(); - std::vector mag_ids; - std::vector ids; - std::vector branch_ids; - std::vector mag_tags; - std::vector tags; - std::vector vmag; - std::vector vang; - std::vector mag_mask; - std::vector mask; - // Find bus IDs and create a dummy tag label and get voltage magnitude - // and angle for base case - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - int not_isolated = atoi(tokens[3].c_str()); - if (not_isolated == 1) { - mag_ids.push_back(atoi(tokens[0].c_str())); - mag_tags.push_back("1 "); - vmag.push_back(atof(tokens[2].c_str())); - if (atoi(tokens[4].c_str()) != 0) { - mag_mask.push_back(2); - } else { - mag_mask.push_back(1); + if (write_stats) { + timer->start(t_store); + v_vals = pf_app.writeBusString("vr_str"); + nsize = v_vals.size(); + std::vector mag_ids; + std::vector ids; + std::vector mag_tags; + std::vector tags; + // Find bus IDs and create a dummy tag label and get voltage magnitude + // and angle for base case + for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + int not_isolated = atoi(tokens[3].c_str()); + if (not_isolated == 1) { + mag_ids.push_back(atoi(tokens[0].c_str())); + mag_tags.push_back("1 "); + vmag.push_back(atof(tokens[2].c_str())); + if (atoi(tokens[4].c_str()) != 0) { + mag_mask.push_back(2); + } else { + mag_mask.push_back(1); + } } - } - ids.push_back(atoi(tokens[0].c_str())); - tags.push_back("1 "); - vang.push_back(atof(tokens[1].c_str())); - mask.push_back(1); - } - int nmags = vmag.size(); - world.max(&nmags,1); - world.max(&nbus,1); -#endif - // Create StatBlock objects for voltage magnitude and angles and add - // bus IDs to it -#ifdef USE_STATBLOCK - gridpack::analysis::StatBlock vmag_stats(world,nmags,ntasks+1); - gridpack::analysis::StatBlock vang_stats(world,nbus,ntasks+1); -#endif - // Add bus IDs and tags to StatBlock objects as well as base case values of - // voltage magnitude and angle -#ifdef USE_STATBLOCK - if (world.rank() == 0) { - vmag_stats.addRowLabels(mag_ids, mag_tags); - vang_stats.addRowLabels(ids, tags); - vmag_stats.addColumnValues(0,vmag,mag_mask); - vang_stats.addColumnValues(0,vang,mask); - } -#endif - // Get generator power information -#ifdef USE_STATBLOCK - v_vals.clear(); - ids.clear(); - tags.clear(); - mask.clear(); - std::vector pgen; - std::vector qgen; - v_vals = pf_app.writeBusString("power"); - nsize = v_vals.size(); - // Find bus IDs and tags for generators and eveluate Pg and Qg for base case - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%4 != 0) { - printf("Incorrect generator listing\n"); - continue; - } - int ngen = tokens.size()/4; - for (j=0; j id1; - std::vector id2; - std::vector pmin, pmax; - std::vector pflow; - std::vector qflow; - std::vector perf; - v_vals = pf_app.writeBranchString("flow_str"); - nsize = v_vals.size(); - // Parse branch line endpoints as well as line IDs and values of P and Q for - // base case - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%8 != 0) { - printf("Incorrect branch power flow listing\n"); - continue; + int nmags = vmag.size(); + world.max(&nmags,1); + world.max(&nbus,1); + // Create StatBlock objects for voltage magnitude and angles and add + // bus IDs to it as well as base case values + vmag_stats.reset(new gridpack::analysis::StatBlock(world,nmags,ntasks+1)); + vang_stats.reset(new gridpack::analysis::StatBlock(world,nbus,ntasks+1)); + if (world.rank() == 0) { + vmag_stats->addRowLabels(mag_ids, mag_tags); + vang_stats->addRowLabels(ids, tags); + vmag_stats->addColumnValues(0,vmag,mag_mask); + vang_stats->addColumnValues(0,vang,mask); } - int nline = tokens.size()/8; - for (j=0; j tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%4 != 0) { + printf("Incorrect generator listing\n"); + continue; + } + int ngen = tokens.size()/4; + for (j=0; jaddRowLabels(ids, tags); + qgen_stats->addRowLabels(ids, tags); + pgen_stats->addColumnValues(0,pgen,mask); + qgen_stats->addColumnValues(0,qgen,mask); + } + + // Find flow parameters for all branch lines + v_vals.clear(); + ids.clear(); + tags.clear(); + mask.clear(); + std::vector id1; + std::vector id2; + std::vector pmin, pmax; + v_vals = pf_app.writeBranchString("flow_str"); + nsize = v_vals.size(); + // Parse branch line endpoints as well as line IDs and values of P and Q for + // base case + for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%8 != 0) { + printf("Incorrect branch power flow listing\n"); + continue; + } + int nline = tokens.size()/8; + for (j=0; jaddRowLabels(id1, id2, tags); + qflow_stats->addRowLabels(id1, id2, tags); + perf_stats->addRowLabels(id1, id2, tags); + pflow_stats->addColumnValues(0,pflow,mask); + qflow_stats->addColumnValues(0,qflow,mask); + perf_stats->addColumnValues(0,perf,mask); + pflow_stats->addRowMinValue(pmin); + qflow_stats->addRowMinValue(pmin); + pflow_stats->addRowMaxValue(pmax); + qflow_stats->addRowMaxValue(pmax); + } + timer->stop(t_store); } - nsize = pflow.size(); - world.max(&nsize,1); -#endif - // Create StatBlock objects for flow parameters and add labels and base case - // values -#ifdef USE_STATBLOCK - gridpack::analysis::StatBlock pflow_stats(world,nsize,ntasks+1); - gridpack::analysis::StatBlock qflow_stats(world,nsize,ntasks+1); - gridpack::analysis::StatBlock perf_stats(world,nsize,ntasks+1); - if (world.rank() == 0) { - pflow_stats.addRowLabels(id1, id2, tags); - qflow_stats.addRowLabels(id1, id2, tags); - perf_stats.addRowLabels(id1, id2, tags); - pflow_stats.addColumnValues(0,pflow,mask); - qflow_stats.addColumnValues(0,qflow,mask); - perf_stats.addColumnValues(0,perf,mask); - pflow_stats.addRowMinValue(pmin); - qflow_stats.addRowMinValue(pmin); - pflow_stats.addRowMaxValue(pmax); - qflow_stats.addRowMaxValue(pmax); - } - timer->stop(t_store); -#endif if (check_Qlim) pf_app.clearQlimViolations(); // Clear any Q limit warnings from base case before starting contingencies gridpack::powerflow::PFBus::clearQlimWarnings(); @@ -919,98 +920,88 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Get strings of data from power flow calculation and parse them to // extract numerical values. Store these values in vectors and then // add them to StatBlock objects -#ifdef USE_STATBLOCK - timer->start(t_store); - vmag.clear(); - vang.clear(); - mask.clear(); - mag_mask.clear(); - v_vals.clear(); - v_vals = pf_app.writeBusString("vr_str"); - nsize = v_vals.size(); - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - int not_isolated = atoi(tokens[3].c_str()); - if (not_isolated == 1) { - vmag.push_back(atof(tokens[2].c_str())); - if (atoi(tokens[4].c_str()) != 0) { - mag_mask.push_back(2); - } else { - mag_mask.push_back(1); + if (write_stats) { + timer->start(t_store); + vmag.clear(); + vang.clear(); + mask.clear(); + mag_mask.clear(); + v_vals.clear(); + v_vals = pf_app.writeBusString("vr_str"); + nsize = v_vals.size(); + for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + int not_isolated = atoi(tokens[3].c_str()); + if (not_isolated == 1) { + vmag.push_back(atof(tokens[2].c_str())); + if (atoi(tokens[4].c_str()) != 0) { + mag_mask.push_back(2); + } else { + mag_mask.push_back(1); + } } - } - vang.push_back(atof(tokens[1].c_str())); - mask.push_back(1); - } -#endif -#ifdef USE_STATBLOCK - if (task_comm.rank() == 0) { - vmag_stats.addColumnValues(task_id+1,vmag,mag_mask); - vang_stats.addColumnValues(task_id+1,vang,mask); - } -#endif -#ifdef USE_STATBLOCK - pgen.clear(); - qgen.clear(); - mask.clear(); - v_vals.clear(); - v_vals = pf_app.writeBusString("power"); - nsize = v_vals.size(); - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%4 != 0) { - printf("Incorrect generator listing\n"); - continue; - } - int ngen = tokens.size()/4; - for (j=0; j tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%8 != 0) { - printf("Incorrect branch power flow listing\n"); - continue; + if (task_comm.rank() == 0) { + vmag_stats->addColumnValues(task_id+1,vmag,mag_mask); + vang_stats->addColumnValues(task_id+1,vang,mask); } - int nline = tokens.size()/8; - for (j=0; j tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%4 != 0) { + printf("Incorrect generator listing\n"); + continue; + } + int ngen = tokens.size()/4; + for (j=0; jaddColumnValues(task_id+1,pgen,mask); + qgen_stats->addColumnValues(task_id+1,qgen,mask); + } + pflow.clear(); + qflow.clear(); + perf.clear(); + mask.clear(); + v_vals.clear(); + v_vals = pf_app.writeBranchString("flow_str"); + nsize = v_vals.size(); + for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%8 != 0) { + printf("Incorrect branch power flow listing\n"); + continue; + } + int nline = tokens.size()/8; + for (j=0; jaddColumnValues(task_id+1,pflow,mask); + qflow_stats->addColumnValues(task_id+1,qflow,mask); + perf_stats->addColumnValues(task_id+1,perf,mask); + } + timer->stop(t_store); } -#endif -#ifdef USE_STATBLOCK - if (task_comm.rank() == 0) { - pflow_stats.addColumnValues(task_id+1,pflow,mask); - qflow_stats.addColumnValues(task_id+1,qflow,mask); - perf_stats.addColumnValues(task_id+1,perf,mask); - } - timer->stop(t_store); -#endif // Note: clearQlimViolations() moved after unSetContingency() below } // end slackCapacityOk block } else { @@ -1041,90 +1032,80 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) 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 -#ifdef USE_STATBLOCK - timer->start(t_store); - vmag.clear(); - vang.clear(); - mask.clear(); - mag_mask.clear(); - v_vals.clear(); - v_vals = pf_app.writeBusString("vfail_str"); - nsize = v_vals.size(); - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - int not_isolated = atoi(tokens[3].c_str()); - if (not_isolated == 1) { - vmag.push_back(0.0); - mag_mask.push_back(0); + if (write_stats) { + timer->start(t_store); + vmag.clear(); + vang.clear(); + mask.clear(); + mag_mask.clear(); + v_vals.clear(); + v_vals = pf_app.writeBusString("vfail_str"); + nsize = v_vals.size(); + for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + int not_isolated = atoi(tokens[3].c_str()); + if (not_isolated == 1) { + vmag.push_back(0.0); + mag_mask.push_back(0); + } + vang.push_back(0.0); + mask.push_back(0); } - vang.push_back(0.0); - mask.push_back(0); - } -#endif -#ifdef USE_STATBLOCK - if (task_comm.rank() == 0) { - vmag_stats.addColumnValues(task_id+1,vmag,mag_mask); - vang_stats.addColumnValues(task_id+1,vang,mask); - } -#endif -#ifdef USE_STATBLOCK - pgen.clear(); - qgen.clear(); - mask.clear(); - v_vals.clear(); - v_vals = pf_app.writeBusString("pfail_str"); - nsize = v_vals.size(); - for (i=0; i tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%4 != 0) { - printf("Incorrect generator listing\n"); - continue; + if (task_comm.rank() == 0) { + vmag_stats->addColumnValues(task_id+1,vmag,mag_mask); + vang_stats->addColumnValues(task_id+1,vang,mask); } - int ngen = tokens.size()/4; - for (j=0; j tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%4 != 0) { + printf("Incorrect generator listing\n"); + continue; + } + int ngen = tokens.size()/4; + for (j=0; j tokens = util.blankTokenizer(v_vals[i]); - if (tokens.size()%8 != 0) { - printf("Incorrect branch power flow listing\n"); - continue; + if (task_comm.rank() == 0) { + pgen_stats->addColumnValues(task_id+1,pgen,mask); + qgen_stats->addColumnValues(task_id+1,qgen,mask); } - int nline = tokens.size()/8; - for (j=0; j tokens = util.blankTokenizer(v_vals[i]); + if (tokens.size()%8 != 0) { + printf("Incorrect branch power flow listing\n"); + continue; + } + int nline = tokens.size()/8; + for (j=0; jaddColumnValues(task_id+1,pflow,mask); + qflow_stats->addColumnValues(task_id+1,qflow,mask); + perf_stats->addColumnValues(task_id+1,perf,mask); + } + timer->stop(t_store); } -#endif -#ifdef USE_STATBLOCK - if (task_comm.rank() == 0) { - pflow_stats.addColumnValues(task_id+1,pflow,mask); - qflow_stats.addColumnValues(task_id+1,qflow,mask); - perf_stats.addColumnValues(task_id+1,perf,mask); - } - timer->stop(t_store); -#endif } // Return network to its original base case state pf_app.unSetContingency(events[task_id]); @@ -1407,27 +1388,27 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } // Print out statistics on contingencies -#ifdef USE_STATBLOCK - int t_stats = timer->createCategory("Write Statistics"); - timer->start(t_stats); - vmag_stats.writeMeanAndRMS("vmag.txt",1,false); - vmag_stats.writeMinAndMax("vmag_mm.txt",1,false); - if (check_Qlim) vmag_stats.writeMaskValueCount("pq_change_cnt.txt",2,false); - vang_stats.writeMeanAndRMS("vang.txt",1,false); - vang_stats.writeMinAndMax("vang_mm.txt",1,false); - pgen_stats.writeMeanAndRMS("pgen.txt",1); - pgen_stats.writeMinAndMax("pgen_mm.txt",1); - qgen_stats.writeMeanAndRMS("qgen.txt",1); - qgen_stats.writeMinAndMax("qgen_mm.txt",1); - pflow_stats.writeMeanAndRMS("pflow.txt",1); - pflow_stats.writeMinAndMax("pflow_mm.txt",1); - pflow_stats.writeMaskValueCount("line_flt_cnt.txt",2); - qflow_stats.writeMeanAndRMS("qflow.txt",1); - qflow_stats.writeMinAndMax("qflow_mm.txt",1); - perf_stats.writeMinAndMax("perf_mm.txt",1); - perf_stats.sumColumnValues("perf_sum.txt",1); - timer->stop(t_stats); -#endif + if (write_stats) { + int t_stats = timer->createCategory("Write Statistics"); + timer->start(t_stats); + vmag_stats->writeMeanAndRMS("vmag.txt",1,false); + vmag_stats->writeMinAndMax("vmag_mm.txt",1,false); + if (check_Qlim) vmag_stats->writeMaskValueCount("pq_change_cnt.txt",2,false); + vang_stats->writeMeanAndRMS("vang.txt",1,false); + vang_stats->writeMinAndMax("vang_mm.txt",1,false); + pgen_stats->writeMeanAndRMS("pgen.txt",1); + pgen_stats->writeMinAndMax("pgen_mm.txt",1); + qgen_stats->writeMeanAndRMS("qgen.txt",1); + qgen_stats->writeMinAndMax("qgen_mm.txt",1); + pflow_stats->writeMeanAndRMS("pflow.txt",1); + pflow_stats->writeMinAndMax("pflow_mm.txt",1); + pflow_stats->writeMaskValueCount("line_flt_cnt.txt",2); + qflow_stats->writeMeanAndRMS("qflow.txt",1); + qflow_stats->writeMinAndMax("qflow_mm.txt",1); + perf_stats->writeMinAndMax("perf_mm.txt",1); + perf_stats->sumColumnValues("perf_sum.txt",1); + timer->stop(t_stats); + } timer->stop(t_total); // If all processors executed at least one task, then print out timing // statistics (this printout does not work if some processors do not define From aa1bce9512681f506b2a4c8abdd3bf839d714088 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Wed, 17 Jun 2026 07:48:52 -0700 Subject: [PATCH 02/31] Gate per-case printfs and dedup auto-generation banner to rank 0 --- .../contingency_analysis/ca_driver.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index dedd9659..86ecda60 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -203,7 +203,9 @@ std::vector } } - printf("Auto-generated %d N-1 branch contingencies\n", branch_count); + if (gridpack::parallel::Communicator().rank() == 0) { + printf("Auto-generated %d N-1 branch contingencies\n", branch_count); + } } // Generate N-1 generator contingencies @@ -238,7 +240,9 @@ std::vector } } - printf("Auto-generated %d N-1 generator contingencies\n", gen_count); + if (gridpack::parallel::Communicator().rank() == 0) { + printf("Auto-generated %d N-1 generator contingencies\n", gen_count); + } } return ret; @@ -751,7 +755,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // nextTask returns the same task_id on all processors in task_comm. When the // 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()); + if (print_calcs) printf("Executing task %d on process %d\n",task_id,world.rank()); // Trim trailing spaces from contingency name for filename std::string fname = events[task_id].p_name; size_t end = fname.find_last_not_of(' '); @@ -771,7 +775,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) sprintf(sbuf," Line: (from) %d (to) %d (line) \'%s\'\n", events[task_id].p_from[j],events[task_id].p_to[j], events[task_id].p_ckt[j].c_str()); - printf("p[%d] Line: (from) %d (to) %d (line) \'%s\'\n", + if (print_calcs) printf("p[%d] Line: (from) %d (to) %d (line) \'%s\'\n", pf_network->communicator().rank(), events[task_id].p_from[j],events[task_id].p_to[j], events[task_id].p_ckt[j].c_str()); @@ -782,7 +786,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) for (j=0; jcommunicator().rank(), events[task_id].p_busid[j],events[task_id].p_genid[j].c_str()); } From 0024b3bff18f0a87728d76fe681c6b23a7e67042 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Thu, 18 Jun 2026 17:01:50 -0700 Subject: [PATCH 03/31] Communicator::divide: skip GA_Pgroup_create when result equals source --- src/applications/contingency_analysis/ca_driver.cpp | 4 ++-- src/parallel/communicator.cpp | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 86ecda60..222a766f 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -561,8 +561,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) printf("==================================================================\n\n"); } - // Print contingency details - if (world.rank() == 0) { + // Print contingency details (gated on printCalcFiles; noisy for large lists) + if (print_calcs && world.rank() == 0) { int idx; for (idx = 0; idx < events.size(); idx++) { printf("Name: %s\n",events[idx].p_name.c_str()); diff --git a/src/parallel/communicator.cpp b/src/parallel/communicator.cpp index bd3d2ae9..25aa1775 100644 --- a/src/parallel/communicator.cpp +++ b/src/parallel/communicator.cpp @@ -186,6 +186,13 @@ Communicator::divide(int nsize) const { int nprocs(size()); int me(rank()); + // Fast path: when the result would contain every rank in this comm, return + // a copy that shares the existing GA process group handle. Skips a redundant + // GA_Pgroup_create (whose handle dispatches through a slower path than GA's + // built-in world group on some MPI stacks). + if (nsize >= nprocs) { + return *this; + } // find out how many communicators need to be created int ngrp = nprocs/nsize; if (ngrp*nsize < nprocs) ngrp++; From 81159329b8f5ac1930dffab010d2b47b12260b9f Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Thu, 18 Jun 2026 20:26:37 -0700 Subject: [PATCH 04/31] Wire ZoneParser33 and OwnerParser33 to store zone/owner numbers and names --- src/parser/PTI33_parser.hpp | 4 +-- src/parser/PTI34_parser.hpp | 4 +-- src/parser/PTI35_parser.hpp | 4 +-- src/parser/PTI36_parser.hpp | 4 +-- src/parser/block_parsers/owner_parser33.cpp | 25 ++++++++++--------- src/parser/block_parsers/owner_parser33.hpp | 6 +++-- src/parser/block_parsers/zone_parser33.cpp | 27 ++++++++++++++++++--- src/parser/block_parsers/zone_parser33.hpp | 6 +++-- src/parser/variable_defs/misc_defs.hpp | 18 +++++++++++++- 9 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/parser/PTI33_parser.hpp b/src/parser/PTI33_parser.hpp index 59116e6a..2315fc2f 100644 --- a/src/parser/PTI33_parser.hpp +++ b/src/parser/PTI33_parser.hpp @@ -273,13 +273,13 @@ class PTI33_parser : public BasePTIParser<_network> multi_section_parser.parse(p_istream,p_branchData); gridpack::parser::ZoneParser33 zone_parser(&p_busMap, &p_nameMap, &p_branchMap); - zone_parser.parse(p_istream); + zone_parser.parse(p_istream,p_network_data); gridpack::parser::InterAreaParser33 interarea_parser(&p_busMap, &p_nameMap, &p_branchMap); interarea_parser.parse(p_istream); gridpack::parser::OwnerParser33 owner_parser(&p_busMap, &p_nameMap, &p_branchMap); - owner_parser.parse(p_istream); + owner_parser.parse(p_istream,p_network_data); gridpack::parser::FACTSParser33 facts_parser(&p_busMap, &p_nameMap, &p_branchMap); facts_parser.parse(p_istream); diff --git a/src/parser/PTI34_parser.hpp b/src/parser/PTI34_parser.hpp index 248218f5..5d918a02 100644 --- a/src/parser/PTI34_parser.hpp +++ b/src/parser/PTI34_parser.hpp @@ -286,13 +286,13 @@ class PTI34_parser : public BasePTIParser<_network> multi_section_parser.parse(p_istream,p_branchData); gridpack::parser::ZoneParser33 zone_parser(&p_busMap, &p_nameMap, &p_branchMap); - zone_parser.parse(p_istream); + zone_parser.parse(p_istream,p_network_data); gridpack::parser::InterAreaParser33 interarea_parser(&p_busMap, &p_nameMap, &p_branchMap); interarea_parser.parse(p_istream); gridpack::parser::OwnerParser33 owner_parser(&p_busMap, &p_nameMap, &p_branchMap); - owner_parser.parse(p_istream); + owner_parser.parse(p_istream,p_network_data); gridpack::parser::FACTSParser33 facts_parser(&p_busMap, &p_nameMap, &p_branchMap); facts_parser.parse(p_istream); diff --git a/src/parser/PTI35_parser.hpp b/src/parser/PTI35_parser.hpp index d14dbc89..b2368297 100644 --- a/src/parser/PTI35_parser.hpp +++ b/src/parser/PTI35_parser.hpp @@ -281,13 +281,13 @@ class PTI35_parser : public BasePTIParser<_network> multi_section_parser.parse(p_istream,p_branchData); gridpack::parser::ZoneParser33 zone_parser(&p_busMap, &p_nameMap, &p_branchMap); - zone_parser.parse(p_istream); + zone_parser.parse(p_istream,p_network_data); gridpack::parser::InterAreaParser33 interarea_parser(&p_busMap, &p_nameMap, &p_branchMap); interarea_parser.parse(p_istream); gridpack::parser::OwnerParser33 owner_parser(&p_busMap, &p_nameMap, &p_branchMap); - owner_parser.parse(p_istream); + owner_parser.parse(p_istream,p_network_data); gridpack::parser::FACTSParser33 facts_parser(&p_busMap, &p_nameMap, &p_branchMap); facts_parser.parse(p_istream); diff --git a/src/parser/PTI36_parser.hpp b/src/parser/PTI36_parser.hpp index 3b5e7d47..d90735cf 100644 --- a/src/parser/PTI36_parser.hpp +++ b/src/parser/PTI36_parser.hpp @@ -289,13 +289,13 @@ class PTI36_parser : public BasePTIParser<_network> multi_section_parser.parse(p_istream,p_branchData); gridpack::parser::ZoneParser33 zone_parser(&p_busMap, &p_nameMap, &p_branchMap); - zone_parser.parse(p_istream); + zone_parser.parse(p_istream,p_network_data); gridpack::parser::InterAreaParser33 interarea_parser(&p_busMap, &p_nameMap, &p_branchMap); interarea_parser.parse(p_istream); gridpack::parser::OwnerParser33 owner_parser(&p_busMap, &p_nameMap, &p_branchMap); - owner_parser.parse(p_istream); + owner_parser.parse(p_istream,p_network_data); gridpack::parser::FACTSParser33 facts_parser(&p_busMap, &p_nameMap, &p_branchMap); facts_parser.parse(p_istream); diff --git a/src/parser/block_parsers/owner_parser33.cpp b/src/parser/block_parsers/owner_parser33.cpp index e7ed5eb7..807b1823 100644 --- a/src/parser/block_parsers/owner_parser33.cpp +++ b/src/parser/block_parsers/owner_parser33.cpp @@ -34,34 +34,37 @@ gridpack::parser::OwnerParser33::~OwnerParser33(void) } /** - * parse owner block. Currently does not store data + * parse owner block * @param stream input stream that feeds lines from RAW file + * @param p_network_data data collection object to store parameters from RAW file */ void gridpack::parser::OwnerParser33::parse( - gridpack::stream::InputStream &stream) + gridpack::stream::InputStream &stream, + boost::shared_ptr &p_network_data) { std::string line; stream.nextLine(line); //this should be the first line of the block + int ncnt = 0; while(test_end(line)) { -#if 0 std::vector split_line; if (check_comment(line)) { stream.nextLine(line); continue; } + this->cleanComment(line); split_line = this->splitPSSELine(line); - std::vector owner_instance; - gridpack::component::DataCollection data; - data.addValue(OWNER_NUMBER, atoi(split_line[0].c_str())); - owner_instance.push_back(data); + if (split_line.size() >= 2) { + // OWNER_NUMBER "I" integer + p_network_data->addValue(OWNER_NUMBER, atoi(split_line[0].c_str()), ncnt); - data.addValue(OWNER_NAME, split_line[1].c_str()); - owner_instance.push_back(data); + // OWNER_NAME "OWNAM" string + p_network_data->addValue(OWNER_NAME, split_line[1].c_str(), ncnt); + ncnt++; + } - owner.push_back(owner_instance); -#endif stream.nextLine(line); } + p_network_data->addValue(OWNER_TOTAL, ncnt); } diff --git a/src/parser/block_parsers/owner_parser33.hpp b/src/parser/block_parsers/owner_parser33.hpp index 05b7f19f..7c510f9e 100644 --- a/src/parser/block_parsers/owner_parser33.hpp +++ b/src/parser/block_parsers/owner_parser33.hpp @@ -35,11 +35,13 @@ class OwnerParser33 : public BaseBlockParser { virtual ~OwnerParser33(void); /** - * parse owner block. Currently does not store data + * parse owner block * @param stream input stream that feeds lines from RAW file + * @param p_network_data data collection object to store parameters */ void parse( - gridpack::stream::InputStream &stream); + gridpack::stream::InputStream &stream, + boost::shared_ptr &p_network_data); }; } // parser diff --git a/src/parser/block_parsers/zone_parser33.cpp b/src/parser/block_parsers/zone_parser33.cpp index 20cccaad..a8f511eb 100644 --- a/src/parser/block_parsers/zone_parser33.cpp +++ b/src/parser/block_parsers/zone_parser33.cpp @@ -34,19 +34,38 @@ gridpack::parser::ZoneParser33::~ZoneParser33(void) } /** - * parse zone block. Currently does not store data + * parse zone block * @param stream input stream that feeds lines from RAW file + * @param p_network_data data collection object to store parameters from RAW file */ void gridpack::parser::ZoneParser33::parse( - gridpack::stream::InputStream &stream) + gridpack::stream::InputStream &stream, + boost::shared_ptr &p_network_data) { std::string line; stream.nextLine(line); //this should be the first line of the block + int ncnt = 0; while(test_end(line)) { - // TODO: parse something here + std::vector split_line; + if (check_comment(line)) { + stream.nextLine(line); + continue; + } + this->cleanComment(line); + split_line = this->splitPSSELine(line); + + if (split_line.size() >= 2) { + // ZONE_NUMBER "I" integer + p_network_data->addValue(ZONE_NUMBER, atoi(split_line[0].c_str()), ncnt); + + // ZONE_NAME "ZONAME" string + p_network_data->addValue(ZONE_NAME, split_line[1].c_str(), ncnt); + ncnt++; + } + stream.nextLine(line); } - + p_network_data->addValue(ZONE_TOTAL, ncnt); } diff --git a/src/parser/block_parsers/zone_parser33.hpp b/src/parser/block_parsers/zone_parser33.hpp index 3d6e836b..25ec5c79 100644 --- a/src/parser/block_parsers/zone_parser33.hpp +++ b/src/parser/block_parsers/zone_parser33.hpp @@ -35,11 +35,13 @@ class ZoneParser33 : public BaseBlockParser { virtual ~ZoneParser33(void); /** - * parse zone block. Currently does not store data + * parse zone block * @param stream input stream that feeds lines from RAW file + * @param p_network_data data collection object to store parameters */ void parse( - gridpack::stream::InputStream &stream); + gridpack::stream::InputStream &stream, + boost::shared_ptr &p_network_data); }; } // parser diff --git a/src/parser/variable_defs/misc_defs.hpp b/src/parser/variable_defs/misc_defs.hpp index 146021c6..fa547838 100644 --- a/src/parser/variable_defs/misc_defs.hpp +++ b/src/parser/variable_defs/misc_defs.hpp @@ -152,15 +152,23 @@ // ZONE DATA +/** + * Total number of zone fields + * type: integer + */ +#define ZONE_TOTAL "ZONE_TOTAL" + /** * Zone Number * type: integer + * indexed */ #define ZONE_NUMBER "ZONE_NUMBER" /** * Zone Name * type: string + * indexed */ #define ZONE_NAME "ZONE_NAME" @@ -193,15 +201,23 @@ // OWNER +/** + * Total number of owner fields + * type: integer + */ +#define OWNER_TOTAL "OWNER_TOTAL" + /** * Owner number * type: integer + * indexed */ #define OWNER_NUMBER "OWNER_NUMBER" /** * Owner name - * type: integer + * type: string + * indexed */ #define OWNER_NAME "OWNER_NAME" From ed29a66cf34449a18f25975a00676ea1930c5075 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Thu, 18 Jun 2026 20:50:13 -0700 Subject: [PATCH 05/31] PFBus: add getBusName, getBaseKV, getOwner accessors --- .../components/pf_matrix/pf_components.cpp | 33 +++++++++++++++++++ .../components/pf_matrix/pf_components.hpp | 18 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index df8efd8b..32b36cbe 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -2495,6 +2495,39 @@ int gridpack::powerflow::PFBus::getZone() return p_zone; } +/** + * Get owner number for bus + * @return bus owner number (0 if not set) + */ +int gridpack::powerflow::PFBus::getOwner() +{ + int owner = 0; + if (p_data) p_data->getValue(BUS_OWNER, &owner); + return owner; +} + +/** + * Get base voltage for bus in kV + * @return base kV (0.0 if not set) + */ +double gridpack::powerflow::PFBus::getBaseKV() +{ + double basekv = 0.0; + if (p_data) p_data->getValue(BUS_BASEKV, &basekv); + return basekv; +} + +/** + * Get bus name string + * @return bus name (empty if not set) + */ +std::string gridpack::powerflow::PFBus::getBusName() +{ + std::string name; + if (p_data) p_data->getValue(BUS_NAME, &name); + return name; +} + /** * Evaluate diagonal block of Jacobian for power flow calculation and * return result as an array of real values diff --git a/src/applications/components/pf_matrix/pf_components.hpp b/src/applications/components/pf_matrix/pf_components.hpp index 72d250b8..3238c560 100644 --- a/src/applications/components/pf_matrix/pf_components.hpp +++ b/src/applications/components/pf_matrix/pf_components.hpp @@ -481,6 +481,24 @@ class PFBus */ int getZone(); + /** + * Get owner number for bus + * @return bus owner number (0 if not set) + */ + int getOwner(); + + /** + * Get base voltage for bus in kV + * @return base kV (0.0 if not set) + */ + double getBaseKV(); + + /** + * Get bus name string + * @return bus name (empty if not set) + */ + std::string getBusName(); + /** * Evaluate diagonal block of Jacobian for power flow calculation and return * result as an array of real values From 268b0a19a9a4fbdb3815c8c9bb2221902c18103a Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Fri, 19 Jun 2026 04:55:46 -0700 Subject: [PATCH 06/31] Capture per-(branch,contingency) flat rows under outputFormat=csv_flat --- .../contingency_analysis/ca_driver.cpp | 164 +++++++++++++++++- 1 file changed, 159 insertions(+), 5 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 222a766f..4b631c99 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -31,6 +31,9 @@ #include #include +#include +#include +#include #define USE_SUCCESS // Statistical-summary output (vmag.txt, pflow.txt, etc.) used to be controlled @@ -426,6 +429,80 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) pf_app.readNetwork(pf_network,config); // Finish initializing the network pf_app.initialize(); + + // Build (number -> name) lookup tables for area, zone, owner. + // Keyed on the PSS/E-assigned number (not contiguous), used when + // emitting per-(branch,contingency) CSV rows so each row carries + // human-readable area/zone/owner names alongside the numbers. + std::map area_name_by_num; + std::map zone_name_by_num; + std::map owner_name_by_num; + { + boost::shared_ptr netdata = + pf_network->getNetworkData(); + int aT = 0, zT = 0, oT = 0; + netdata->getValue(AREA_TOTAL, &aT); + netdata->getValue(ZONE_TOTAL, &zT); + netdata->getValue(OWNER_TOTAL, &oT); + for (int i = 0; i < aT; i++) { + int n = 0; std::string s; + netdata->getValue(AREAINTG_NUMBER, &n, i); + netdata->getValue(AREAINTG_NAME, &s, i); + area_name_by_num[n] = s; + } + for (int i = 0; i < zT; i++) { + int n = 0; std::string s; + netdata->getValue(ZONE_NUMBER, &n, i); + netdata->getValue(ZONE_NAME, &s, i); + zone_name_by_num[n] = s; + } + for (int i = 0; i < oT; i++) { + int n = 0; std::string s; + netdata->getValue(OWNER_NUMBER, &n, i); + netdata->getValue(OWNER_NAME, &s, i); + owner_name_by_num[n] = s; + } + } + + // Per-(branch,contingency) flat-row capture used by outputFormat=csv_flat. + // One row per branch per converged contingency, looked up against per-bus + // metadata gathered from the local network (covers both active and ghost + // buses so all branch endpoints resolve on rank 0). + struct FlatRow { + int event_idx; + int branch_from, branch_to; + char ckt[4]; + double p_mw, q_mvar, flow_mva, rate_a_mva, loading_pct; + int viol; + double v_from, v_to, ang_from_deg, ang_to_deg; + int area_from, zone_from, owner_from; + int area_to, zone_to, owner_to; + double basekv_from, basekv_to; + }; + struct BusMeta { + std::string name; + double basekv; + int area, zone, owner; + }; + std::map bus_meta; + if (outputFormat == "csv_flat") { + int nBus = pf_network->numBuses(); + for (int i = 0; i < nBus; i++) { + gridpack::powerflow::PFBus *bus = + dynamic_cast(pf_network->getBus(i).get()); + if (!bus) continue; + int orig = pf_network->getOriginalBusIndex(i); + BusMeta m; + m.name = bus->getBusName(); + m.basekv = bus->getBaseKV(); + m.area = bus->getArea(); + m.zone = bus->getZone(); + m.owner = bus->getOwner(); + bus_meta[orig] = m; + } + } + std::vector localFlatRows; + // Set minimum and maximum voltage limits on all buses pf_app.setVoltageLimits(Vmin, Vmax); // Solve the base power flow calculation. This calculation is replicated on @@ -439,9 +516,10 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // buses to ignore voltage violations on them. pf_app.ignoreVoltageViolations(); - // Collect base case results for export + // Collect base case results for export. csv_flat captures rows directly + // in the hot loop and skips the heavyweight collectResults() path. gridpack::utility::PowerFlowResults baseCaseResults; - if (outputFormat != "text") { + if (outputFormat == "json" || outputFormat == "csv") { baseCaseResults = pf_app.collectResults(); } @@ -848,7 +926,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) contingency_violation.push_back(0); contingency_isolated.push_back(false); #endif - if (outputFormat != "text") { + if (outputFormat == "json" || outputFormat == "csv") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; @@ -873,7 +951,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) bool ok2 = pf_app.checkLineOverloadViolations(); bool ok = ok1 && ok2; // Collect results for JSON/CSV export - if (outputFormat != "text") { + if (outputFormat == "json" || outputFormat == "csv") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; @@ -882,6 +960,60 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) ctResult.solution = pf_app.collectResults(); localContingencies.push_back(ctResult); } + if (outputFormat == "csv_flat" && task_comm.rank() == 0) { + std::vector v_strs = pf_app.writeBusString("vr_str"); + std::vector b_strs = pf_app.writeBranchString("flow_str"); + std::map > vbymag_ang; + for (size_t vi = 0; vi < v_strs.size(); vi++) { + int bus_id = 0, use_vmag = 0, changed = 0; + double angle = 0.0, vmag = 0.0; + if (sscanf(v_strs[vi].c_str(), "%d %lf %lf %d %d", + &bus_id, &angle, &vmag, &use_vmag, &changed) == 5) { + vbymag_ang[bus_id] = std::make_pair(vmag, angle); + } + } + for (size_t bi = 0; bi < b_strs.size(); bi++) { + FlatRow r; + char ckt[16] = {0}; + int viol = 0; + double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; + int from = 0, to = 0; + if (sscanf(b_strs[bi].c_str(), + "%d %d %15s %lf %lf %lf %lf %d", + &from, &to, ckt, &p, &q, &perf, &ratea, &viol) != 8) { + continue; + } + r.event_idx = task_id + 1; + r.branch_from = from; + r.branch_to = to; + std::strncpy(r.ckt, ckt, 3); r.ckt[3] = '\0'; + r.p_mw = p; + r.q_mvar = q; + r.flow_mva = std::sqrt(p*p + q*q); + r.rate_a_mva = ratea; + r.loading_pct = (ratea > 0.0) ? (r.flow_mva / ratea) * 100.0 : 0.0; + r.viol = viol; + std::map >::const_iterator vf = + vbymag_ang.find(from); + std::map >::const_iterator vt = + vbymag_ang.find(to); + r.v_from = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; + r.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; + r.v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; + r.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; + std::map::const_iterator mf = bus_meta.find(from); + std::map::const_iterator mt = bus_meta.find(to); + r.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; + r.zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; + r.owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; + r.basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + r.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; + r.zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; + r.owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; + r.basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + localFlatRows.push_back(r); + } + } // Include results of violation checks in output if (ok) { sprintf(sbuf,"\nNo violation for contingency %s\n", @@ -1014,7 +1146,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) contingency_violation.push_back(0); contingency_isolated.push_back(false); #endif - if (outputFormat != "text") { + if (outputFormat == "json" || outputFormat == "csv") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; @@ -1122,6 +1254,28 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Close output file for this contingency if (print_calcs) pf_app.close(); } + // csv_flat smoke test: rank 0 prints local row count and first 3 rows. + // Final CSV emit + cross-rank gather is Phase E. + if (outputFormat == "csv_flat" && world.rank() == 0) { + printf("[csv_flat] rank 0 captured %zu rows\n", localFlatRows.size()); + size_t n = localFlatRows.size(); + if (n > 3) n = 3; + for (size_t i = 0; i < n; i++) { + const FlatRow &r = localFlatRows[i]; + printf("[csv_flat] event=%d %d->%d ckt=%s P=%.3f Q=%.3f flow=%.3f" + " rateA=%.3f load%%=%.2f viol=%d Vfrom=%.4f Vto=%.4f" + " areas=%d/%d zones=%d/%d owners=%d/%d basekv=%.1f/%.1f" + " names=%s|%s\n", + r.event_idx, r.branch_from, r.branch_to, r.ckt, + r.p_mw, r.q_mvar, r.flow_mva, r.rate_a_mva, r.loading_pct, + r.viol, r.v_from, r.v_to, + r.area_from, r.area_to, r.zone_from, r.zone_to, + r.owner_from, r.owner_to, r.basekv_from, r.basekv_to, + area_name_by_num[r.area_from].c_str(), + area_name_by_num[r.area_to].c_str()); + } + } + // Print statistics from task manager describing the number of tasks performed // per processor taskmgr.printStats(); From 7154536b4ed454a835de005c505ac94d42ffb6b2 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Fri, 19 Jun 2026 06:21:16 -0700 Subject: [PATCH 07/31] csv_flat: gather rows across ranks and write outputFile_flat.csv --- .../contingency_analysis/ca_driver.cpp | 250 +++++++++++++----- 1 file changed, 179 insertions(+), 71 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 4b631c99..a547c42c 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -31,6 +31,9 @@ #include #include +#include +#include +#include #include #include #include @@ -470,6 +473,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // buses so all branch endpoints resolve on rank 0). struct FlatRow { int event_idx; + char ct_name[24]; int branch_from, branch_to; char ckt[4]; double p_mw, q_mvar, flow_mva, rate_a_mva, loading_pct; @@ -503,6 +507,68 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } std::vector localFlatRows; + // Lambda: parse current solved flow_str/vr_str into FlatRow records and + // append to localFlatRows. Called once per converged case (base + each + // contingency) on rank 0 of the task communicator. Caller decides + // event_idx/name (0/"base_case" for the base case). + auto captureFlatRows = [&](int event_idx, const std::string &name) { + std::vector v_strs = pf_app.writeBusString("vr_str"); + std::vector b_strs = pf_app.writeBranchString("flow_str"); + if (task_comm.rank() != 0) return; + std::map > vbymag_ang; + for (size_t vi = 0; vi < v_strs.size(); vi++) { + int bus_id = 0, use_vmag = 0, changed = 0; + double angle = 0.0, vmag = 0.0; + if (sscanf(v_strs[vi].c_str(), "%d %lf %lf %d %d", + &bus_id, &angle, &vmag, &use_vmag, &changed) == 5) { + vbymag_ang[bus_id] = std::make_pair(vmag, angle); + } + } + for (size_t bi = 0; bi < b_strs.size(); bi++) { + FlatRow r; + char ckt[16] = {0}; + int viol = 0; + double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; + int from = 0, to = 0; + if (sscanf(b_strs[bi].c_str(), + "%d %d %15s %lf %lf %lf %lf %d", + &from, &to, ckt, &p, &q, &perf, &ratea, &viol) != 8) { + continue; + } + r.event_idx = event_idx; + std::strncpy(r.ct_name, name.c_str(), sizeof(r.ct_name) - 1); + r.ct_name[sizeof(r.ct_name) - 1] = '\0'; + r.branch_from = from; + r.branch_to = to; + std::strncpy(r.ckt, ckt, 3); r.ckt[3] = '\0'; + r.p_mw = p; + r.q_mvar = q; + r.flow_mva = std::sqrt(p*p + q*q); + r.rate_a_mva = ratea; + r.loading_pct = (ratea > 0.0) ? (r.flow_mva / ratea) * 100.0 : 0.0; + r.viol = viol; + std::map >::const_iterator vf = + vbymag_ang.find(from); + std::map >::const_iterator vt = + vbymag_ang.find(to); + r.v_from = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; + r.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; + r.v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; + r.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; + std::map::const_iterator mf = bus_meta.find(from); + std::map::const_iterator mt = bus_meta.find(to); + r.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; + r.zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; + r.owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; + r.basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + r.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; + r.zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; + r.owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; + r.basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + localFlatRows.push_back(r); + } + }; + // Set minimum and maximum voltage limits on all buses pf_app.setVoltageLimits(Vmin, Vmax); // Solve the base power flow calculation. This calculation is replicated on @@ -522,6 +588,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (outputFormat == "json" || outputFormat == "csv") { baseCaseResults = pf_app.collectResults(); } + if (outputFormat == "csv_flat") { + // The base case is replicated on every task communicator. captureFlatRows + // calls writeBusString/writeBranchString which use task_comm collectives, + // so every task_comm participates -- but only world rank 0 keeps the + // resulting rows so the base case isn't duplicated in the final file. + captureFlatRows(0, std::string("base_case")); + if (world.rank() != 0) localFlatRows.clear(); + } // Check if auto-generation of N-1 contingencies is enabled // FullBranchN1: generate N-1 contingencies for all branches @@ -960,59 +1034,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) ctResult.solution = pf_app.collectResults(); localContingencies.push_back(ctResult); } - if (outputFormat == "csv_flat" && task_comm.rank() == 0) { - std::vector v_strs = pf_app.writeBusString("vr_str"); - std::vector b_strs = pf_app.writeBranchString("flow_str"); - std::map > vbymag_ang; - for (size_t vi = 0; vi < v_strs.size(); vi++) { - int bus_id = 0, use_vmag = 0, changed = 0; - double angle = 0.0, vmag = 0.0; - if (sscanf(v_strs[vi].c_str(), "%d %lf %lf %d %d", - &bus_id, &angle, &vmag, &use_vmag, &changed) == 5) { - vbymag_ang[bus_id] = std::make_pair(vmag, angle); - } - } - for (size_t bi = 0; bi < b_strs.size(); bi++) { - FlatRow r; - char ckt[16] = {0}; - int viol = 0; - double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; - int from = 0, to = 0; - if (sscanf(b_strs[bi].c_str(), - "%d %d %15s %lf %lf %lf %lf %d", - &from, &to, ckt, &p, &q, &perf, &ratea, &viol) != 8) { - continue; - } - r.event_idx = task_id + 1; - r.branch_from = from; - r.branch_to = to; - std::strncpy(r.ckt, ckt, 3); r.ckt[3] = '\0'; - r.p_mw = p; - r.q_mvar = q; - r.flow_mva = std::sqrt(p*p + q*q); - r.rate_a_mva = ratea; - r.loading_pct = (ratea > 0.0) ? (r.flow_mva / ratea) * 100.0 : 0.0; - r.viol = viol; - std::map >::const_iterator vf = - vbymag_ang.find(from); - std::map >::const_iterator vt = - vbymag_ang.find(to); - r.v_from = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; - r.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; - r.v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; - r.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; - std::map::const_iterator mf = bus_meta.find(from); - std::map::const_iterator mt = bus_meta.find(to); - r.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; - r.zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; - r.owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; - r.basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - r.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; - r.zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; - r.owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; - r.basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; - localFlatRows.push_back(r); - } + if (outputFormat == "csv_flat") { + captureFlatRows(task_id + 1, events[task_id].p_name); } // Include results of violation checks in output if (ok) { @@ -1254,25 +1277,110 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Close output file for this contingency if (print_calcs) pf_app.close(); } - // csv_flat smoke test: rank 0 prints local row count and first 3 rows. - // Final CSV emit + cross-rank gather is Phase E. - if (outputFormat == "csv_flat" && world.rank() == 0) { - printf("[csv_flat] rank 0 captured %zu rows\n", localFlatRows.size()); - size_t n = localFlatRows.size(); - if (n > 3) n = 3; - for (size_t i = 0; i < n; i++) { + // csv_flat: serialize each rank's localFlatRows to a CSV-text fragment + // (resolving area/zone/owner names against the rank-local lookup tables), + // gather to world rank 0 via point-to-point MPI send/recv, and write a + // single output file with header. + if (outputFormat == "csv_flat") { + // Strip a single layer of surrounding single quotes (PSS/E style) and + // collapse leading/trailing whitespace inside. + auto trim_quoted = [](const std::string &in) -> std::string { + std::string s = in; + size_t a = s.find_first_not_of(" \t"); + size_t b = s.find_last_not_of(" \t"); + if (a == std::string::npos) return std::string(); + s = s.substr(a, b - a + 1); + if (s.size() >= 2 && s.front() == '\'' && s.back() == '\'') { + s = s.substr(1, s.size() - 2); + } + a = s.find_first_not_of(" \t"); + b = s.find_last_not_of(" \t"); + return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1); + }; + auto bus_name_lookup = [&](int orig) -> std::string { + std::map::const_iterator it = bus_meta.find(orig); + if (it == bus_meta.end()) return std::string(); + return trim_quoted(it->second.name); + }; + auto lookup_name = [&](const std::map &m, int n) -> std::string { + std::map::const_iterator it = m.find(n); + if (it == m.end()) return std::string(); + return trim_quoted(it->second); + }; + std::ostringstream local; + local << std::fixed; + for (size_t i = 0; i < localFlatRows.size(); i++) { const FlatRow &r = localFlatRows[i]; - printf("[csv_flat] event=%d %d->%d ckt=%s P=%.3f Q=%.3f flow=%.3f" - " rateA=%.3f load%%=%.2f viol=%d Vfrom=%.4f Vto=%.4f" - " areas=%d/%d zones=%d/%d owners=%d/%d basekv=%.1f/%.1f" - " names=%s|%s\n", - r.event_idx, r.branch_from, r.branch_to, r.ckt, - r.p_mw, r.q_mvar, r.flow_mva, r.rate_a_mva, r.loading_pct, - r.viol, r.v_from, r.v_to, - r.area_from, r.area_to, r.zone_from, r.zone_to, - r.owner_from, r.owner_to, r.basekv_from, r.basekv_to, - area_name_by_num[r.area_from].c_str(), - area_name_by_num[r.area_to].c_str()); + local << r.event_idx << "," << r.ct_name << "," + << r.branch_from << "," << r.branch_to << "," << r.ckt << "," + << std::setprecision(4) << r.p_mw << "," + << std::setprecision(4) << r.q_mvar << "," + << std::setprecision(4) << r.flow_mva << "," + << std::setprecision(4) << r.rate_a_mva << "," + << std::setprecision(2) << r.loading_pct << "," + << r.viol << "," + << std::setprecision(6) << r.v_from << "," + << std::setprecision(6) << r.v_to << "," + << std::setprecision(4) << r.ang_from_deg << "," + << std::setprecision(4) << r.ang_to_deg << "," + << r.area_from << "," << r.zone_from << "," << r.owner_from << "," + << r.area_to << "," << r.zone_to << "," << r.owner_to << "," + << std::setprecision(2) << r.basekv_from << "," + << std::setprecision(2) << r.basekv_to << "," + << bus_name_lookup(r.branch_from) << "," + << bus_name_lookup(r.branch_to) << "," + << lookup_name(area_name_by_num, r.area_from) << "," + << lookup_name(area_name_by_num, r.area_to) << "," + << lookup_name(zone_name_by_num, r.zone_from) << "," + << lookup_name(zone_name_by_num, r.zone_to) << "," + << lookup_name(owner_name_by_num, r.owner_from) << "," + << lookup_name(owner_name_by_num, r.owner_to) + << "\n"; + } + std::string localCSV = local.str(); + + MPI_Comm mpi_comm = static_cast(world); + std::vector allCSV(world.size()); + allCSV[0] = (world.rank() == 0) ? localCSV : std::string(); + if (world.rank() == 0) { + for (int p = 1; p < world.size(); p++) { + int len = 0; + MPI_Recv(&len, 1, MPI_INT, p, 10, mpi_comm, MPI_STATUS_IGNORE); + allCSV[p].resize(len); + if (len > 0) { + MPI_Recv(&allCSV[p][0], len, MPI_CHAR, p, 11, mpi_comm, + MPI_STATUS_IGNORE); + } + } + } else { + int len = static_cast(localCSV.size()); + MPI_Send(&len, 1, MPI_INT, 0, 10, mpi_comm); + if (len > 0) { + MPI_Send(const_cast(localCSV.c_str()), len, MPI_CHAR, 0, 11, + mpi_comm); + } + } + + if (world.rank() == 0) { + std::string flatFile = outputFile + "_flat.csv"; + std::ofstream fout(flatFile.c_str()); + fout << "event_idx,contingency,from_bus,to_bus,circuit_id," + "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," + "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg," + "area_from,zone_from,owner_from," + "area_to,zone_to,owner_to,basekv_from,basekv_to," + "bus_name_from,bus_name_to,area_name_from,area_name_to," + "zone_name_from,zone_name_to,owner_name_from,owner_name_to\n"; + size_t total_rows = 0; + for (int p = 0; p < world.size(); p++) { + fout << allCSV[p]; + if (!allCSV[p].empty()) { + total_rows += static_cast( + std::count(allCSV[p].begin(), allCSV[p].end(), '\n')); + } + } + fout.close(); + printf("[csv_flat] wrote %zu rows to %s\n", total_rows, flatFile.c_str()); } } From 08f0938764feab16de1fa463b2ab28ebbd945163 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Fri, 19 Jun 2026 07:59:04 -0700 Subject: [PATCH 08/31] csv_flat: stream rows to per-rank files; bound rank-0 memory --- .../contingency_analysis/ca_driver.cpp | 273 +++++++++--------- 1 file changed, 129 insertions(+), 144 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index a547c42c..c4fd8f62 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -468,21 +468,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } // Per-(branch,contingency) flat-row capture used by outputFormat=csv_flat. - // One row per branch per converged contingency, looked up against per-bus - // metadata gathered from the local network (covers both active and ghost - // buses so all branch endpoints resolve on rank 0). - struct FlatRow { - int event_idx; - char ct_name[24]; - int branch_from, branch_to; - char ckt[4]; - double p_mw, q_mvar, flow_mva, rate_a_mva, loading_pct; - int viol; - double v_from, v_to, ang_from_deg, ang_to_deg; - int area_from, zone_from, owner_from; - int area_to, zone_to, owner_to; - double basekv_from, basekv_to; - }; + // Streams one row per branch per converged contingency directly to a + // per-rank file (outputFile + "_flat..part") so memory stays bounded + // regardless of contingency count. After the loop, world rank 0 writes + // the header to outputFile + "_flat.csv" and concatenates each rank's + // .part file into it (in rank order), then unlinks them. struct BusMeta { std::string name; double basekv; @@ -505,16 +495,55 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) bus_meta[orig] = m; } } - std::vector localFlatRows; - // Lambda: parse current solved flow_str/vr_str into FlatRow records and - // append to localFlatRows. Called once per converged case (base + each - // contingency) on rank 0 of the task communicator. Caller decides - // event_idx/name (0/"base_case" for the base case). - auto captureFlatRows = [&](int event_idx, const std::string &name) { + // Strip surrounding single quotes (PSS/E style) and outer whitespace. + auto trim_quoted = [](const std::string &in) -> std::string { + std::string s = in; + size_t a = s.find_first_not_of(" \t"); + size_t b = s.find_last_not_of(" \t"); + if (a == std::string::npos) return std::string(); + s = s.substr(a, b - a + 1); + if (s.size() >= 2 && s.front() == '\'' && s.back() == '\'') { + s = s.substr(1, s.size() - 2); + } + a = s.find_first_not_of(" \t"); + b = s.find_last_not_of(" \t"); + return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1); + }; + auto bus_name_lookup = [&](int orig) -> std::string { + std::map::const_iterator it = bus_meta.find(orig); + if (it == bus_meta.end()) return std::string(); + return trim_quoted(it->second.name); + }; + auto lookup_name = [&](const std::map &m, int n) -> std::string { + std::map::const_iterator it = m.find(n); + if (it == m.end()) return std::string(); + return trim_quoted(it->second); + }; + + // Per-rank streaming output file. Opened on first row written so non-csv_flat + // runs and ranks that produce no rows leave nothing behind. + std::string flatPartPath; + if (outputFormat == "csv_flat") { + std::ostringstream oss; + oss << outputFile << "_flat." << world.rank() << ".part"; + flatPartPath = oss.str(); + } + std::ofstream flatPart; + size_t flatRowCount = 0; + + // Lambda: parse current solved flow_str/vr_str and stream one CSV row + // per branch into the rank's .part file. Called once per converged case + // (base + each contingency) on every task communicator; non-rank-0 + // task_comm members short-circuit after the collective. + auto captureFlatRows = [&](int event_idx, const std::string &name, bool emit) { std::vector v_strs = pf_app.writeBusString("vr_str"); std::vector b_strs = pf_app.writeBranchString("flow_str"); - if (task_comm.rank() != 0) return; + if (!emit || task_comm.rank() != 0) return; + if (!flatPart.is_open()) { + flatPart.open(flatPartPath.c_str(), std::ios::out | std::ios::trunc); + flatPart << std::fixed; + } std::map > vbymag_ang; for (size_t vi = 0; vi < v_strs.size(); vi++) { int bus_id = 0, use_vmag = 0, changed = 0; @@ -524,48 +553,67 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) vbymag_ang[bus_id] = std::make_pair(vmag, angle); } } + char ct_name[24]; + std::strncpy(ct_name, name.c_str(), sizeof(ct_name) - 1); + ct_name[sizeof(ct_name) - 1] = '\0'; for (size_t bi = 0; bi < b_strs.size(); bi++) { - FlatRow r; - char ckt[16] = {0}; + char ckt_buf[16] = {0}; int viol = 0; double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; int from = 0, to = 0; if (sscanf(b_strs[bi].c_str(), "%d %d %15s %lf %lf %lf %lf %d", - &from, &to, ckt, &p, &q, &perf, &ratea, &viol) != 8) { + &from, &to, ckt_buf, &p, &q, &perf, &ratea, &viol) != 8) { continue; } - r.event_idx = event_idx; - std::strncpy(r.ct_name, name.c_str(), sizeof(r.ct_name) - 1); - r.ct_name[sizeof(r.ct_name) - 1] = '\0'; - r.branch_from = from; - r.branch_to = to; - std::strncpy(r.ckt, ckt, 3); r.ckt[3] = '\0'; - r.p_mw = p; - r.q_mvar = q; - r.flow_mva = std::sqrt(p*p + q*q); - r.rate_a_mva = ratea; - r.loading_pct = (ratea > 0.0) ? (r.flow_mva / ratea) * 100.0 : 0.0; - r.viol = viol; + char ckt[4]; + std::strncpy(ckt, ckt_buf, 3); ckt[3] = '\0'; + double flow_mva = std::sqrt(p*p + q*q); + double loading_pct = (ratea > 0.0) ? (flow_mva / ratea) * 100.0 : 0.0; std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = vbymag_ang.find(to); - r.v_from = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; - r.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; - r.v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; - r.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; + double v_from = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; + double ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; + double v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; + double ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; std::map::const_iterator mf = bus_meta.find(from); std::map::const_iterator mt = bus_meta.find(to); - r.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; - r.zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; - r.owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; - r.basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - r.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; - r.zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; - r.owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; - r.basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; - localFlatRows.push_back(r); + int area_from = (mf != bus_meta.end()) ? mf->second.area : 0; + int zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; + int owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; + double basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + int area_to = (mt != bus_meta.end()) ? mt->second.area : 0; + int zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; + int owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; + double basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + flatPart << event_idx << "," << ct_name << "," + << from << "," << to << "," << ckt << "," + << std::setprecision(4) << p << "," + << std::setprecision(4) << q << "," + << std::setprecision(4) << flow_mva << "," + << std::setprecision(4) << ratea << "," + << std::setprecision(2) << loading_pct << "," + << viol << "," + << std::setprecision(6) << v_from << "," + << std::setprecision(6) << v_to << "," + << std::setprecision(4) << ang_from_deg << "," + << std::setprecision(4) << ang_to_deg << "," + << area_from << "," << zone_from << "," << owner_from << "," + << area_to << "," << zone_to << "," << owner_to << "," + << std::setprecision(2) << basekv_from << "," + << std::setprecision(2) << basekv_to << "," + << bus_name_lookup(from) << "," + << bus_name_lookup(to) << "," + << lookup_name(area_name_by_num, area_from) << "," + << lookup_name(area_name_by_num, area_to) << "," + << lookup_name(zone_name_by_num, zone_from) << "," + << lookup_name(zone_name_by_num, zone_to) << "," + << lookup_name(owner_name_by_num, owner_from) << "," + << lookup_name(owner_name_by_num, owner_to) + << "\n"; + flatRowCount++; } }; @@ -590,11 +638,10 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } if (outputFormat == "csv_flat") { // The base case is replicated on every task communicator. captureFlatRows - // calls writeBusString/writeBranchString which use task_comm collectives, - // so every task_comm participates -- but only world rank 0 keeps the - // resulting rows so the base case isn't duplicated in the final file. - captureFlatRows(0, std::string("base_case")); - if (world.rank() != 0) localFlatRows.clear(); + // calls writeBusString/writeBranchString which are task_comm collectives, + // so every task_comm participates -- but only world rank 0 emits rows so + // the base case isn't duplicated in the final file. + captureFlatRows(0, std::string("base_case"), world.rank() == 0); } // Check if auto-generation of N-1 contingencies is enabled @@ -1035,7 +1082,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) localContingencies.push_back(ctResult); } if (outputFormat == "csv_flat") { - captureFlatRows(task_id + 1, events[task_id].p_name); + captureFlatRows(task_id + 1, events[task_id].p_name, true); } // Include results of violation checks in output if (ok) { @@ -1277,93 +1324,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Close output file for this contingency if (print_calcs) pf_app.close(); } - // csv_flat: serialize each rank's localFlatRows to a CSV-text fragment - // (resolving area/zone/owner names against the rank-local lookup tables), - // gather to world rank 0 via point-to-point MPI send/recv, and write a - // single output file with header. + // csv_flat: each rank streamed its rows to outputFile_flat..part + // during the loop. Close per-rank files, then on world rank 0 write the + // header to the final file and concatenate each rank's part file in + // rank order, unlinking each as it goes. if (outputFormat == "csv_flat") { - // Strip a single layer of surrounding single quotes (PSS/E style) and - // collapse leading/trailing whitespace inside. - auto trim_quoted = [](const std::string &in) -> std::string { - std::string s = in; - size_t a = s.find_first_not_of(" \t"); - size_t b = s.find_last_not_of(" \t"); - if (a == std::string::npos) return std::string(); - s = s.substr(a, b - a + 1); - if (s.size() >= 2 && s.front() == '\'' && s.back() == '\'') { - s = s.substr(1, s.size() - 2); - } - a = s.find_first_not_of(" \t"); - b = s.find_last_not_of(" \t"); - return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1); - }; - auto bus_name_lookup = [&](int orig) -> std::string { - std::map::const_iterator it = bus_meta.find(orig); - if (it == bus_meta.end()) return std::string(); - return trim_quoted(it->second.name); - }; - auto lookup_name = [&](const std::map &m, int n) -> std::string { - std::map::const_iterator it = m.find(n); - if (it == m.end()) return std::string(); - return trim_quoted(it->second); - }; - std::ostringstream local; - local << std::fixed; - for (size_t i = 0; i < localFlatRows.size(); i++) { - const FlatRow &r = localFlatRows[i]; - local << r.event_idx << "," << r.ct_name << "," - << r.branch_from << "," << r.branch_to << "," << r.ckt << "," - << std::setprecision(4) << r.p_mw << "," - << std::setprecision(4) << r.q_mvar << "," - << std::setprecision(4) << r.flow_mva << "," - << std::setprecision(4) << r.rate_a_mva << "," - << std::setprecision(2) << r.loading_pct << "," - << r.viol << "," - << std::setprecision(6) << r.v_from << "," - << std::setprecision(6) << r.v_to << "," - << std::setprecision(4) << r.ang_from_deg << "," - << std::setprecision(4) << r.ang_to_deg << "," - << r.area_from << "," << r.zone_from << "," << r.owner_from << "," - << r.area_to << "," << r.zone_to << "," << r.owner_to << "," - << std::setprecision(2) << r.basekv_from << "," - << std::setprecision(2) << r.basekv_to << "," - << bus_name_lookup(r.branch_from) << "," - << bus_name_lookup(r.branch_to) << "," - << lookup_name(area_name_by_num, r.area_from) << "," - << lookup_name(area_name_by_num, r.area_to) << "," - << lookup_name(zone_name_by_num, r.zone_from) << "," - << lookup_name(zone_name_by_num, r.zone_to) << "," - << lookup_name(owner_name_by_num, r.owner_from) << "," - << lookup_name(owner_name_by_num, r.owner_to) - << "\n"; - } - std::string localCSV = local.str(); - - MPI_Comm mpi_comm = static_cast(world); - std::vector allCSV(world.size()); - allCSV[0] = (world.rank() == 0) ? localCSV : std::string(); - if (world.rank() == 0) { - for (int p = 1; p < world.size(); p++) { - int len = 0; - MPI_Recv(&len, 1, MPI_INT, p, 10, mpi_comm, MPI_STATUS_IGNORE); - allCSV[p].resize(len); - if (len > 0) { - MPI_Recv(&allCSV[p][0], len, MPI_CHAR, p, 11, mpi_comm, - MPI_STATUS_IGNORE); - } - } - } else { - int len = static_cast(localCSV.size()); - MPI_Send(&len, 1, MPI_INT, 0, 10, mpi_comm); - if (len > 0) { - MPI_Send(const_cast(localCSV.c_str()), len, MPI_CHAR, 0, 11, - mpi_comm); - } - } - + if (flatPart.is_open()) flatPart.close(); + world.sync(); if (world.rank() == 0) { std::string flatFile = outputFile + "_flat.csv"; - std::ofstream fout(flatFile.c_str()); + std::ofstream fout(flatFile.c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); fout << "event_idx,contingency,from_bus,to_bus,circuit_id," "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg," @@ -1372,12 +1343,26 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) "bus_name_from,bus_name_to,area_name_from,area_name_to," "zone_name_from,zone_name_to,owner_name_from,owner_name_to\n"; size_t total_rows = 0; + const size_t BUFSZ = 1 << 20; + std::vector buf(BUFSZ); for (int p = 0; p < world.size(); p++) { - fout << allCSV[p]; - if (!allCSV[p].empty()) { - total_rows += static_cast( - std::count(allCSV[p].begin(), allCSV[p].end(), '\n')); + std::ostringstream oss; + oss << outputFile << "_flat." << p << ".part"; + std::string part = oss.str(); + std::ifstream fin(part.c_str(), std::ios::in | std::ios::binary); + if (!fin) continue; + while (fin) { + fin.read(&buf[0], BUFSZ); + std::streamsize got = fin.gcount(); + if (got > 0) { + fout.write(&buf[0], got); + for (std::streamsize k = 0; k < got; k++) { + if (buf[k] == '\n') total_rows++; + } + } } + fin.close(); + std::remove(part.c_str()); } fout.close(); printf("[csv_flat] wrote %zu rows to %s\n", total_rows, flatFile.c_str()); From 95975d36287c0b1187f3df254be8b04f81c06bb0 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Fri, 19 Jun 2026 10:13:05 -0700 Subject: [PATCH 09/31] split bus metadata into sidecar _buses.csv --- .../contingency_analysis/ca_driver.cpp | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index c4fd8f62..2afcd23d 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #define USE_SUCCESS // Statistical-summary output (vmag.txt, pflow.txt, etc.) used to be controlled @@ -510,17 +512,36 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) b = s.find_last_not_of(" \t"); return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1); }; - auto bus_name_lookup = [&](int orig) -> std::string { - std::map::const_iterator it = bus_meta.find(orig); - if (it == bus_meta.end()) return std::string(); - return trim_quoted(it->second.name); - }; auto lookup_name = [&](const std::map &m, int n) -> std::string { std::map::const_iterator it = m.find(n); if (it == m.end()) return std::string(); return trim_quoted(it->second); }; + // Per-rank bus metadata sidecar. Each rank's bus_meta covers active + ghost + // buses, so the same bus_id appears on multiple ranks. World rank 0 dedupes + // these into the final outputFile_buses.csv after the contingency loop. + if (outputFormat == "csv_flat") { + std::ostringstream oss; + oss << outputFile << "_buses." << world.rank() << ".part"; + std::ofstream fbus(oss.str().c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); + fbus << std::fixed; + for (std::map::const_iterator it = bus_meta.begin(); + it != bus_meta.end(); ++it) { + const BusMeta &m = it->second; + fbus << it->first << "," + << trim_quoted(m.name) << "," + << std::setprecision(2) << m.basekv << "," + << m.area << "," << m.zone << "," << m.owner << "," + << lookup_name(area_name_by_num, m.area) << "," + << lookup_name(zone_name_by_num, m.zone) << "," + << lookup_name(owner_name_by_num, m.owner) + << "\n"; + } + fbus.close(); + } + // Per-rank streaming output file. Opened on first row written so non-csv_flat // runs and ranks that produce no rows leave nothing behind. std::string flatPartPath; @@ -578,16 +599,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; double v_to = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; double ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; - std::map::const_iterator mf = bus_meta.find(from); - std::map::const_iterator mt = bus_meta.find(to); - int area_from = (mf != bus_meta.end()) ? mf->second.area : 0; - int zone_from = (mf != bus_meta.end()) ? mf->second.zone : 0; - int owner_from = (mf != bus_meta.end()) ? mf->second.owner : 0; - double basekv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - int area_to = (mt != bus_meta.end()) ? mt->second.area : 0; - int zone_to = (mt != bus_meta.end()) ? mt->second.zone : 0; - int owner_to = (mt != bus_meta.end()) ? mt->second.owner : 0; - double basekv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; flatPart << event_idx << "," << ct_name << "," << from << "," << to << "," << ckt << "," << std::setprecision(4) << p << "," @@ -599,19 +610,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << std::setprecision(6) << v_from << "," << std::setprecision(6) << v_to << "," << std::setprecision(4) << ang_from_deg << "," - << std::setprecision(4) << ang_to_deg << "," - << area_from << "," << zone_from << "," << owner_from << "," - << area_to << "," << zone_to << "," << owner_to << "," - << std::setprecision(2) << basekv_from << "," - << std::setprecision(2) << basekv_to << "," - << bus_name_lookup(from) << "," - << bus_name_lookup(to) << "," - << lookup_name(area_name_by_num, area_from) << "," - << lookup_name(area_name_by_num, area_to) << "," - << lookup_name(zone_name_by_num, zone_from) << "," - << lookup_name(zone_name_by_num, zone_to) << "," - << lookup_name(owner_name_by_num, owner_from) << "," - << lookup_name(owner_name_by_num, owner_to) + << std::setprecision(4) << ang_to_deg << "\n"; flatRowCount++; } @@ -1337,11 +1336,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ios::out | std::ios::trunc | std::ios::binary); fout << "event_idx,contingency,from_bus,to_bus,circuit_id," "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," - "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg," - "area_from,zone_from,owner_from," - "area_to,zone_to,owner_to,basekv_from,basekv_to," - "bus_name_from,bus_name_to,area_name_from,area_name_to," - "zone_name_from,zone_name_to,owner_name_from,owner_name_to\n"; + "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg\n"; size_t total_rows = 0; const size_t BUFSZ = 1 << 20; std::vector buf(BUFSZ); @@ -1366,6 +1361,40 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } fout.close(); printf("[csv_flat] wrote %zu rows to %s\n", total_rows, flatFile.c_str()); + + // Bus metadata sidecar: each rank wrote its bus_meta to a .part file + // covering its active+ghost buses, so the same bus_id appears on + // multiple ranks. Read each part, dedupe by bus_id (first writer wins), + // then emit one row per unique bus to outputFile_buses.csv. + std::string busFile = outputFile + "_buses.csv"; + std::ofstream bout(busFile.c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); + bout << "bus_id,bus_name,base_kv,area,zone,owner," + "area_name,zone_name,owner_name\n"; + std::set seen_bus; + size_t bus_rows = 0; + for (int p = 0; p < world.size(); p++) { + std::ostringstream oss; + oss << outputFile << "_buses." << p << ".part"; + std::string part = oss.str(); + std::ifstream fin(part.c_str()); + if (!fin) continue; + std::string line; + while (std::getline(fin, line)) { + if (line.empty()) continue; + size_t comma = line.find(','); + if (comma == std::string::npos) continue; + int bus_id = std::atoi(line.substr(0, comma).c_str()); + if (seen_bus.insert(bus_id).second) { + bout << line << "\n"; + bus_rows++; + } + } + fin.close(); + std::remove(part.c_str()); + } + bout.close(); + printf("[csv_flat] wrote %zu rows to %s\n", bus_rows, busFile.c_str()); } } From c1b1ebb11a0d173a05a0f2bb09947e3503002081 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Fri, 19 Jun 2026 23:58:48 -0400 Subject: [PATCH 10/31] Fix warm-start Q-limit clamp direction for inconsistent QG --- .../components/pf_matrix/pf_components.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 32b36cbe..abadb9df 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -1094,6 +1094,20 @@ void gridpack::powerflow::PFBus::load( const double Q_init_tol = 0.1; // Mvar — matches PW convergence tolerance if (total_qg >= total_qmax - Q_init_tol || total_qg <= total_qmin + Q_init_tol) { p_isPV = false; + // Pick the saturated limit from V vs VS (V>VS => QMIN, V QMAX), not + // scheduled QG which can be inconsistent. Local regulation only. + if (p_ireg_remote_bus == 0) { + double vset = 0.0; int nv = 0; + for (i = 0; i < p_ngen; i++) + if (p_gstatus[i] == 1) { vset += p_vs[i]; nv++; } + if (nv > 0) vset /= nv; + const double V_init_tol = 1.0e-4; + if (p_voltage > vset + V_init_tol) { + for (i = 0; i < p_ngen; i++) if (p_gstatus[i] == 1) p_qg[i] = p_qmin[i]; + } else if (p_voltage < vset - V_init_tol) { + for (i = 0; i < p_ngen; i++) if (p_gstatus[i] == 1) p_qg[i] = p_qmax[i]; + } + } } } From 7e62c947055982ccd2caa0414fab9d1fe68cf5c8 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 20 Jun 2026 10:02:14 -0700 Subject: [PATCH 11/31] add qlimDeadband option, default = 0.1 --- .../components/pf_matrix/pf_components.cpp | 18 ++++++++++-------- .../components/pf_matrix/pf_components.hpp | 2 ++ .../contingency_analysis/ca_driver.cpp | 2 ++ src/applications/powerflow/pf_main.cpp | 2 ++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index abadb9df..0aecbd33 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -69,6 +69,7 @@ // Static member initialization gridpack::powerflow::InitStartMode gridpack::powerflow::PFBus::p_initStartMode = INIT_START_WARM; bool gridpack::powerflow::PFBus::p_qlim = true; +double gridpack::powerflow::PFBus::p_qlim_deadband = 0.1; std::vector gridpack::powerflow::PFBus::p_qlimWarnings; /** @@ -87,6 +88,11 @@ void gridpack::powerflow::PFBus::setQlim(bool qlim) p_qlim = qlim; } +void gridpack::powerflow::PFBus::setQlimDeadband(double db) +{ + p_qlim_deadband = db; +} + /** * Clear accumulated Q limit warning messages */ @@ -453,7 +459,7 @@ bool gridpack::powerflow::PFBus::chkQlim(double q_deadband) // Check if Q requirement can be met. // q_deadband avoids switching buses that are only marginally over their Q // limit due to floating-point differences. Configurable via XML qlimDeadband - // (default 0.1 Mvar, matching PW's 0.1 MVA convergence tolerance). + // (default 0.1 Mvar) bool need_pv_to_pq = false; char warnBuf[256]; if (Q_required > Q_max_total + q_deadband) { @@ -1077,11 +1083,7 @@ void gridpack::powerflow::PFBus::load( } } } - // Warm-start Q-limit pre-saturation: if scheduled QG is already at QMAX or QMIN - // (within 0.1 Mvar), start this bus as PQ immediately. This matches PW behavior - // where generators already at their limits in the input data are treated as PQ, - // preventing IREG from driving them to impossible Q requirements. - // Only applies for warm start with qlim enabled. + // Warm-start Q-limit pre-saturation: scheduled QG at QMAX/QMIN -> start as PQ. if (p_isPV && p_qlim && p_initStartMode != INIT_START_FLAT) { double total_qg = 0.0, total_qmax = 0.0, total_qmin = 0.0; for (i = 0; i < p_ngen; i++) { @@ -1091,8 +1093,8 @@ void gridpack::powerflow::PFBus::load( total_qmin += p_qmin[i]; } } - const double Q_init_tol = 0.1; // Mvar — matches PW convergence tolerance - if (total_qg >= total_qmax - Q_init_tol || total_qg <= total_qmin + Q_init_tol) { + if (total_qg >= total_qmax - p_qlim_deadband + || total_qg <= total_qmin + p_qlim_deadband) { p_isPV = false; // Pick the saturated limit from V vs VS (V>VS => QMIN, V QMAX), not // scheduled QG which can be inconsistent. Local regulation only. diff --git a/src/applications/components/pf_matrix/pf_components.hpp b/src/applications/components/pf_matrix/pf_components.hpp index 3238c560..56e7b69d 100644 --- a/src/applications/components/pf_matrix/pf_components.hpp +++ b/src/applications/components/pf_matrix/pf_components.hpp @@ -628,6 +628,7 @@ class PFBus */ static void setInitStartMode(InitStartMode mode); static void setQlim(bool qlim); + static void setQlimDeadband(double db); /** * Clear accumulated Q limit warning messages @@ -709,6 +710,7 @@ class PFBus static std::vector p_qlimWarnings; static InitStartMode p_initStartMode; static bool p_qlim; + static double p_qlim_deadband; double p_shunt_gs; double p_shunt_bs; bool p_shunt; diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 2afcd23d..18e2d747 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -401,6 +401,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } // Check for Q limit violations (qlim: true=enabled, false=disabled) bool check_Qlim = cursor->get("qlim", true); + double qlim_deadband = cursor->get("qlimDeadband", 0.1); // Output format: "json", "csv", or "text" (default) std::string outputFormat = "text"; cursor->get("outputFormat", &outputFormat); @@ -411,6 +412,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // - When check_Qlim = false: output uses calculated Q from p_Qinj // - When check_Qlim = true: output uses p_qg (set by chkQlim()) gridpack::powerflow::PFBus::setQlim(check_Qlim); + gridpack::powerflow::PFBus::setQlimDeadband(qlim_deadband); gridpack::parallel::Communicator task_comm = world.divide(grp_size); // Keep track of failed calculations diff --git a/src/applications/powerflow/pf_main.cpp b/src/applications/powerflow/pf_main.cpp index 28b9f4c8..f3a08d39 100644 --- a/src/applications/powerflow/pf_main.cpp +++ b/src/applications/powerflow/pf_main.cpp @@ -76,6 +76,7 @@ int main(int argc, char **argv) // Parse qlim flag (default: true - enforce reactive power limits) bool qlim = cursor->get("qlim", true); + double qlim_deadband = cursor->get("qlimDeadband", 0.1); // Set flags BEFORE creating network // This must be called before readNetwork() for it to take effect @@ -85,6 +86,7 @@ int main(int argc, char **argv) gridpack::powerflow::PFBus::setInitStartMode(gridpack::powerflow::INIT_START_WARM); } gridpack::powerflow::PFBus::setQlim(qlim); + gridpack::powerflow::PFBus::setQlimDeadband(qlim_deadband); // setup and run powerflow calculation boost::shared_ptr From c0e117c1d811670b08ccf995d310e770b88196ea Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 20 Jun 2026 14:54:28 -0700 Subject: [PATCH 12/31] add universal _convergence.csv --- .../contingency_analysis/ca_driver.cpp | 311 ++++++++++-------- .../modules/powerflow/pf_factory_module.cpp | 44 +-- .../modules/powerflow/pf_factory_module.hpp | 1 + 3 files changed, 178 insertions(+), 178 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 18e2d747..7192cc17 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -40,7 +40,6 @@ #include #include -#define USE_SUCCESS // Statistical-summary output (vmag.txt, pflow.txt, etc.) used to be controlled // by a USE_STATBLOCK build-time macro; it is now a runtime XML option, // `Configuration.Contingency_analysis.writeStats`, defaulting to true to @@ -415,17 +414,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) gridpack::powerflow::PFBus::setQlimDeadband(qlim_deadband); gridpack::parallel::Communicator task_comm = world.divide(grp_size); - // Keep track of failed calculations -#ifdef USE_SUCCESS - std::vector contingency_idx; - std::vector contingency_success; - 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 boost::shared_ptr pf_network(new gridpack::powerflow::PFNetwork(task_comm)); @@ -555,6 +543,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ofstream flatPart; size_t flatRowCount = 0; + // Convergence sidecar rows. + struct ConvRow { + int event_idx; + std::string name; + std::string type; + gridpack::utility::ConvergenceSummary cs; + std::string status; + }; + std::vector localConvRows; + bool emitConv = (outputFormat == "csv" || + outputFormat == "csv_flat" || + outputFormat == "csv_delta"); + // Lambda: parse current solved flow_str/vr_str and stream one CSV row // per branch into the rank's .part file. Called once per converged case // (base + each contingency) on every task communicator; non-rank-0 @@ -620,15 +621,40 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Set minimum and maximum voltage limits on all buses pf_app.setVoltageLimits(Vmin, Vmax); - // Solve the base power flow calculation. This calculation is replicated on - // all task communicators - pf_app.solve(); - // Check for Qlimit violations - if (check_Qlim && !pf_app.checkQlimViolations()) { - pf_app.solve(); + // Solve the base power flow on every task communicator. Abort if it fails. + bool baseSolveOk = false; + try { + baseSolveOk = pf_app.solve(); + if (baseSolveOk && check_Qlim && !pf_app.checkQlimViolations()) { + baseSolveOk = pf_app.solve(); + } + } catch (const std::exception &e) { + if (world.rank() == 0) { + printf("ERROR: base-case solve threw exception: %s\n", e.what()); + } + baseSolveOk = false; + } catch (...) { + if (world.rank() == 0) { + printf("ERROR: base-case solve threw unknown exception\n"); + } + baseSolveOk = false; + } + if (!baseSolveOk) { + if (world.rank() == 0) { + gridpack::utility::ConvergenceSummary cs = pf_app.getConvergence(); + printf("ERROR: base case did not converge " + "(iterations=%d, final_tol=%.6e, " + "max_p_bus=%d max_p_mismatch=%.4f, " + "max_q_bus=%d max_q_mismatch=%.4f). " + "Aborting contingency analysis.\n", + cs.iterations, cs.finalTolerance, + cs.finalMismatch.maxPBus, cs.finalMismatch.maxPMismatch, + cs.finalMismatch.maxQBus, cs.finalMismatch.maxQMismatch); + } + world.barrier(); + MPI_Abort(static_cast(world), 1); } - // Some buses may violate the voltage limits in the base problem. Flag these - // buses to ignore voltage violations on them. + // Suppress voltage violations already present at base. pf_app.ignoreVoltageViolations(); // Collect base case results for export. csv_flat captures rows directly @@ -949,6 +975,20 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Local contingency results storage for JSON/CSV export std::vector localContingencies; + // Convergence row recorder; indexes events[task_id]. + auto recordConv = [&](int task_id, const char *status, + const std::string &) { + if (!emitConv) return; + if (task_comm.rank() != 0) return; + ConvRow r; + r.event_idx = task_id + 1; + r.name = events[task_id].p_name; + r.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; + r.cs = pf_app.getConvergence(); + r.status = status; + localConvRows.push_back(r); + }; + // Evaluate contingencies using the task manager int task_id; char sbuf[128]; @@ -1008,10 +1048,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) 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 // Skip power flow if contingency setup failed (no valid slack) or islanding detected bool slackCapacityOk = true; // Will be checked after solve bool solveOk = false; @@ -1043,29 +1079,22 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) 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 if (outputFormat == "json" || outputFormat == "csv") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; ctResult.hasVoltageViolation = false; ctResult.hasBranchViolation = false; + ctResult.solution.convergence = pf_app.getConvergence(); ctResult.solution.convergence.converged = false; localContingencies.push_back(ctResult); } + recordConv(task_id, "SLACK_OVERLOAD", std::string()); 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 @@ -1085,13 +1114,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (outputFormat == "csv_flat") { captureFlatRows(task_id + 1, events[task_id].p_name, true); } + recordConv(task_id, "OK", std::string()); // Include results of violation checks in output if (ok) { sprintf(sbuf,"\nNo violation for contingency %s\n", events[task_id].p_name.c_str()); -#ifdef USE_SUCCESS - contingency_violation.push_back(1); -#endif } // Report bus voltage violations if (!ok1) { @@ -1112,16 +1139,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) events[task_id].p_name.c_str()); } -#ifdef USE_SUCCESS - if (!ok1 && !ok2) { - contingency_violation.push_back(4); - } else if (!ok1) { - contingency_violation.push_back(2); - } else if (!ok2) { - 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 @@ -1212,20 +1229,27 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // 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 (outputFormat == "json" || outputFormat == "csv") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; ctResult.hasVoltageViolation = false; ctResult.hasBranchViolation = false; + ctResult.solution.convergence = pf_app.getConvergence(); ctResult.solution.convergence.converged = false; localContingencies.push_back(ctResult); } + { + const char *st; + if (islandDetected) { + st = "ISLANDED"; + } else if (!contingencyFound) { + st = "NO_SLACK"; + } else { + st = "DIVERGED"; + } + recordConv(task_id, st, std::string()); + } if (islandDetected) { sprintf(sbuf,"\nIslanding detected for contingency %s (%d islands)\n", events[task_id].p_name.c_str(), islandCount); @@ -1404,55 +1428,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // per processor taskmgr.printStats(); - // Gather stats on successful contingency calculations -#ifdef USE_SUCCESS - 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(); - // All processes call getData to ensure GA progress (NGA_Gather requires - // remote process participation for one-sided communication). - contingency_idx.clear(); - contingency_success.clear(); - contingency_violation.clear(); - contingency_isolated.clear(); - for (i=0; i(world); std::vector allBus(world.size()), allBranch(world.size()); - std::vector allGen(world.size()), allConv(world.size()); + std::vector allGen(world.size()); allBus[0] = localBus.str(); allBranch[0] = localBranch.str(); allGen[0] = localGen.str(); - allConv[0] = localConv.str(); if (world.rank() == 0) { for (int p = 1; p < world.size(); p++) { - int lens[4]; - MPI_Recv(lens, 4, MPI_INT, p, 0, mpi_comm, MPI_STATUS_IGNORE); + int lens[3]; + MPI_Recv(lens, 3, MPI_INT, p, 0, mpi_comm, MPI_STATUS_IGNORE); allBus[p].resize(lens[0]); allBranch[p].resize(lens[1]); allGen[p].resize(lens[2]); - allConv[p].resize(lens[3]); if (lens[0] > 0) MPI_Recv(&allBus[p][0], lens[0], MPI_CHAR, p, 1, mpi_comm, MPI_STATUS_IGNORE); @@ -1603,16 +1566,12 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (lens[2] > 0) MPI_Recv(&allGen[p][0], lens[2], MPI_CHAR, p, 3, mpi_comm, MPI_STATUS_IGNORE); - if (lens[3] > 0) - MPI_Recv(&allConv[p][0], lens[3], MPI_CHAR, p, 4, mpi_comm, - MPI_STATUS_IGNORE); } } else { std::string sBus = localBus.str(), sBranch = localBranch.str(); - std::string sGen = localGen.str(), sConv = localConv.str(); - int lens[4] = {(int)sBus.size(), (int)sBranch.size(), - (int)sGen.size(), (int)sConv.size()}; - MPI_Send(lens, 4, MPI_INT, 0, 0, mpi_comm); + std::string sGen = localGen.str(); + int lens[3] = {(int)sBus.size(), (int)sBranch.size(), (int)sGen.size()}; + MPI_Send(lens, 3, MPI_INT, 0, 0, mpi_comm); if (lens[0] > 0) MPI_Send(const_cast(sBus.c_str()), lens[0], MPI_CHAR, 0, 1, mpi_comm); @@ -1622,9 +1581,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (lens[2] > 0) MPI_Send(const_cast(sGen.c_str()), lens[2], MPI_CHAR, 0, 3, mpi_comm); - if (lens[3] > 0) - MPI_Send(const_cast(sConv.c_str()), lens[3], MPI_CHAR, 0, 4, - mpi_comm); } // Rank 0 writes the CSV files @@ -1645,27 +1601,90 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ofstream out((outputFile + "_generators.csv").c_str(), std::ios::app); for (size_t p = 0; p < allGen.size(); p++) out << allGen[p]; } - { - std::ofstream out((outputFile + "_convergence.csv").c_str(), std::ios::app); - for (size_t p = 0; p < allConv.size(); p++) out << allConv[p]; + } + } + + // Universal convergence sidecar: gather, sort by event_idx, write. + if (emitConv) { + auto formatRow = [](std::ostringstream &os, const ConvRow &r) { + os << r.event_idx << "," + << r.name << "," + << r.type << "," + << (r.cs.converged ? "true" : "false") << "," + << r.cs.iterations << "," + << std::scientific << r.cs.finalTolerance << "," + << std::fixed + << r.cs.finalMismatch.maxPBus << "," + << std::setprecision(4) << r.cs.finalMismatch.maxPMismatch << "," + << r.cs.finalMismatch.maxQBus << "," + << std::setprecision(4) << r.cs.finalMismatch.maxQMismatch << "," + << r.status << "\n"; + }; + + std::vector idx; + std::ostringstream localStream; + localStream << std::fixed; + std::vector localOffsets; + localOffsets.reserve(localConvRows.size() + 1); + for (size_t i = 0; i < localConvRows.size(); i++) { + localOffsets.push_back(static_cast(localStream.tellp())); + formatRow(localStream, localConvRows[i]); + idx.push_back(localConvRows[i].event_idx); + } + localOffsets.push_back(static_cast(localStream.tellp())); + std::string localStr = localStream.str(); + + MPI_Comm conv_comm = static_cast(world); + if (world.rank() == 0) { + std::vector > all; + for (size_t i = 0; i < idx.size(); i++) { + std::string row = localStr.substr(localOffsets[i], + localOffsets[i+1] - localOffsets[i]); + all.push_back(std::make_pair(idx[i], row)); } -#ifdef USE_SUCCESS - // Write summary CSV - std::string summaryFile = outputFile + "_summary.csv"; - std::ofstream sout(summaryFile.c_str()); - sout << "contingency,type,converged,has_voltage_violation,has_branch_violation\n"; - for (int ci = 0; ci < ntasks; ci++) { - bool converged = (contingency_violation[ci] > 0); - sout << events[ci].p_name << "," - << (events[ci].p_type == Branch ? "branch" : "generator") << "," - << (converged ? "true" : "false") << "," - << ((contingency_violation[ci] == 2 || contingency_violation[ci] == 4) - ? "true" : "false") << "," - << ((contingency_violation[ci] == 3 || contingency_violation[ci] == 4) - ? "true" : "false") << "\n"; + for (int p = 1; p < world.size(); p++) { + int n = 0; + MPI_Recv(&n, 1, MPI_INT, p, 10, conv_comm, MPI_STATUS_IGNORE); + if (n <= 0) continue; + std::vector remIdx(n), remOff(n + 1); + MPI_Recv(&remIdx[0], n, MPI_INT, p, 11, conv_comm, MPI_STATUS_IGNORE); + MPI_Recv(&remOff[0], n + 1, MPI_INT, p, 12, conv_comm, + MPI_STATUS_IGNORE); + int total = remOff[n]; + std::string buf(total, '\0'); + if (total > 0) { + MPI_Recv(&buf[0], total, MPI_CHAR, p, 13, conv_comm, + MPI_STATUS_IGNORE); + } + for (int i = 0; i < n; i++) { + all.push_back(std::make_pair( + remIdx[i], + buf.substr(remOff[i], remOff[i+1] - remOff[i]))); + } + } + std::sort(all.begin(), all.end()); + std::string convFile = outputFile + "_convergence.csv"; + std::ofstream cout(convFile.c_str(), + std::ios::out | std::ios::trunc); + cout << "event_idx,contingency,type,converged,iterations," + "final_tolerance,max_p_bus,max_p_mismatch,max_q_bus," + "max_q_mismatch,status_code\n"; + for (size_t i = 0; i < all.size(); i++) cout << all[i].second; + cout.close(); + printf("[convergence] wrote %zu rows to %s\n", + all.size(), convFile.c_str()); + } else { + int n = static_cast(idx.size()); + MPI_Send(&n, 1, MPI_INT, 0, 10, conv_comm); + if (n > 0) { + MPI_Send(&idx[0], n, MPI_INT, 0, 11, conv_comm); + MPI_Send(&localOffsets[0], n + 1, MPI_INT, 0, 12, conv_comm); + int total = localOffsets[n]; + if (total > 0) { + MPI_Send(const_cast(localStr.c_str()), total, MPI_CHAR, 0, 13, + conv_comm); + } } - sout.close(); -#endif } } diff --git a/src/applications/modules/powerflow/pf_factory_module.cpp b/src/applications/modules/powerflow/pf_factory_module.cpp index d2597ec6..a18d8624 100644 --- a/src/applications/modules/powerflow/pf_factory_module.cpp +++ b/src/applications/modules/powerflow/pf_factory_module.cpp @@ -171,11 +171,15 @@ bool gridpack::powerflow::PFFactoryModule::checkLoneBus(std::ofstream *stream) bool bus_ok = true; char buf[128]; p_saveIsolatedStatus.clear(); + p_loneBusIndices.clear(); for (i=0; igetActiveBus(i)) continue; gridpack::powerflow::PFBus *bus = dynamic_cast (p_network->getBus(i).get()); + // Skip already-isolated buses (e.g. PSS/E type-4) so they are not + // re-flagged as lone on every call. + if (bus->isIsolated()) continue; std::vector > branches; bus->getNeighborBranches(branches); int size = branches.size(); @@ -200,6 +204,7 @@ bool gridpack::powerflow::PFFactoryModule::checkLoneBus(std::ofstream *stream) if (!ok) { sprintf(buf,"\nLone bus %d found\n",bus->getOriginalIndex()); p_saveIsolatedStatus.push_back(bus->isIsolated()); + p_loneBusIndices.push_back(i); bus->setIsolated(true); printf("%s",buf); if (stream != NULL) *stream << buf; @@ -217,42 +222,17 @@ 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; - int ncount = 0; - for (i=0; igetActiveBus(i)) continue; + // Restore status of buses marked by the last checkLoneBus call. + for (size_t k = 0; k < p_loneBusIndices.size(); k++) { + int i = p_loneBusIndices[k]; gridpack::powerflow::PFBus *bus = dynamic_cast (p_network->getBus(i).get()); - std::vector > branches; - bus->getNeighborBranches(branches); - int size = branches.size(); - bool ok = true; - if (size == 0) { - ok = false; - } - if (ok) { - ok = false; - for (j=0; j status = - dynamic_cast - (branches[j].get())->getLineStatus(); - int nlines = status.size(); - for (k=0; kgetOriginalIndex()); - bus->setIsolated(p_saveIsolatedStatus[ncount]); - ncount++; - } + printf("\nLone bus %d reset\n", bus->getOriginalIndex()); + bus->setIsolated(p_saveIsolatedStatus[k]); } + p_loneBusIndices.clear(); + p_saveIsolatedStatus.clear(); } /** diff --git a/src/applications/modules/powerflow/pf_factory_module.hpp b/src/applications/modules/powerflow/pf_factory_module.hpp index 88085517..a1b783fd 100644 --- a/src/applications/modules/powerflow/pf_factory_module.hpp +++ b/src/applications/modules/powerflow/pf_factory_module.hpp @@ -369,6 +369,7 @@ class PFFactoryModule NetworkPtr p_network; std::vector p_saveIsolatedStatus; + std::vector p_loneBusIndices; 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 From 9c6321e571d1ff99405d18dbe970f6709702e97f Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 20 Jun 2026 17:31:17 -0700 Subject: [PATCH 13/31] add csv_delta option for CA outputs --- .../contingency_analysis/ca_driver.cpp | 325 +++++++++++++++--- 1 file changed, 281 insertions(+), 44 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 7192cc17..8fb19c92 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -459,19 +459,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } } - // Per-(branch,contingency) flat-row capture used by outputFormat=csv_flat. - // Streams one row per branch per converged contingency directly to a - // per-rank file (outputFile + "_flat..part") so memory stays bounded - // regardless of contingency count. After the loop, world rank 0 writes - // the header to outputFile + "_flat.csv" and concatenates each rank's - // .part file into it (in rank order), then unlinks them. + // Per-rank bus metadata + wide/long branch-row outputs for csv_flat + // (long-form one row per branch per case) and csv_delta (wide-form one + // row per branch per case joining base and cont state). Both share the + // bus_meta load, the buses sidecar, and the per-rank .part-file gather. + bool wantBusSidecar = (outputFormat == "csv_flat" || + outputFormat == "csv_delta"); struct BusMeta { std::string name; double basekv; int area, zone, owner; }; std::map bus_meta; - if (outputFormat == "csv_flat") { + if (wantBusSidecar) { int nBus = pf_network->numBuses(); for (int i = 0; i < nBus; i++) { gridpack::powerflow::PFBus *bus = @@ -508,10 +508,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) return trim_quoted(it->second); }; - // Per-rank bus metadata sidecar. Each rank's bus_meta covers active + ghost - // buses, so the same bus_id appears on multiple ranks. World rank 0 dedupes - // these into the final outputFile_buses.csv after the contingency loop. - if (outputFormat == "csv_flat") { + // Per-rank bus metadata sidecar (deduped by world rank 0 after the loop). + if (wantBusSidecar) { std::ostringstream oss; oss << outputFile << "_buses." << world.rank() << ".part"; std::ofstream fbus(oss.str().c_str(), @@ -543,6 +541,35 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ofstream flatPart; size_t flatRowCount = 0; + // csv_delta wide-row state. Per-rank .part file streamed during the + // contingency loop; base_cache holds base-case branch state (populated + // once after base solve) keyed by (from, to, ckt). + struct BranchKey { + int from, to; + std::string ckt; + bool operator<(const BranchKey &o) const { + if (from != o.from) return from < o.from; + if (to != o.to ) return to < o.to; + return ckt < o.ckt; + } + }; + struct BaseFlow { + double p_mw, q_mvar, mva, loading_pct, rate_a; + double v_from_pu, v_to_pu, ang_from_deg, ang_to_deg; + double base_kv_from, base_kv_to; + int area_from, area_to; + }; + std::map base_cache; + std::string deltaPartPath; + if (outputFormat == "csv_delta") { + std::ostringstream oss; + oss << outputFile << "_delta." << world.rank() << ".part"; + deltaPartPath = oss.str(); + } + std::ofstream deltaPart; + size_t deltaRowCount = 0; + size_t deltaSkipCount = 0; + // Convergence sidecar rows. struct ConvRow { int event_idx; @@ -619,6 +646,176 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } }; + // Populate base_cache from current solved state. Called once after base + // solve on every rank (csv_delta only); world.rank() == 0 is not special + // here -- each rank caches the branches it sees on its task_comm so it + // can join later in captureDeltaRows. + auto populateBaseCache = [&]() { + std::vector v_strs = pf_app.writeBusString("vr_str"); + std::vector b_strs = pf_app.writeBranchString("flow_str"); + if (task_comm.rank() != 0) return; + std::map > vbymag_ang; + for (size_t vi = 0; vi < v_strs.size(); vi++) { + int bus_id = 0, use_vmag = 0, changed = 0; + double angle = 0.0, vmag = 0.0; + if (sscanf(v_strs[vi].c_str(), "%d %lf %lf %d %d", + &bus_id, &angle, &vmag, &use_vmag, &changed) == 5) { + vbymag_ang[bus_id] = std::make_pair(vmag, angle); + } + } + for (size_t bi = 0; bi < b_strs.size(); bi++) { + char ckt_buf[16] = {0}; + int viol = 0; + double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; + int from = 0, to = 0; + if (sscanf(b_strs[bi].c_str(), + "%d %d %15s %lf %lf %lf %lf %d", + &from, &to, ckt_buf, &p, &q, &perf, &ratea, &viol) != 8) { + continue; + } + BranchKey k; + k.from = from; k.to = to; + k.ckt = std::string(ckt_buf); + // Strip trailing spaces from ckt so the key matches what flow_str + // returns later (sscanf %15s already trims leading whitespace). + while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); + BaseFlow bf; + bf.p_mw = p; + bf.q_mvar = q; + bf.mva = std::sqrt(p*p + q*q); + bf.rate_a = ratea; + bf.loading_pct = (ratea > 0.0) ? (bf.mva / ratea) * 100.0 : 0.0; + std::map >::const_iterator vf = + vbymag_ang.find(from); + std::map >::const_iterator vt = + vbymag_ang.find(to); + bf.v_from_pu = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; + bf.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; + bf.v_to_pu = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; + bf.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; + std::map::const_iterator mf = bus_meta.find(from); + std::map::const_iterator mt = bus_meta.find(to); + bf.base_kv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + bf.base_kv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + bf.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; + bf.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; + base_cache[k] = bf; + } + }; + + // Wide-form (base+cont on same row) capture for csv_delta. Mirrors + // captureFlatRows but joins each branch with base_cache. Branches not + // in base_cache are counted in deltaSkipCount and skipped silently. + auto captureDeltaRows = [&](int event_idx, + const gridpack::powerflow::Contingency &evt, + bool emit) { + std::vector v_strs = pf_app.writeBusString("vr_str"); + std::vector b_strs = pf_app.writeBranchString("flow_str"); + if (!emit || task_comm.rank() != 0) return; + if (!deltaPart.is_open()) { + deltaPart.open(deltaPartPath.c_str(), std::ios::out | std::ios::trunc); + deltaPart << std::fixed; + } + // cont_event_facility: built once per contingency. + std::string facility; + if (evt.p_type == Branch && !evt.p_from.empty()) { + int outFrom = evt.p_from[0]; + int area = 0; + std::map::const_iterator mf = bus_meta.find(outFrom); + if (mf != bus_meta.end()) area = mf->second.area; + char buf[64]; + snprintf(buf, sizeof(buf), "[%d] %d %d %s", + area, outFrom, evt.p_to[0], evt.p_ckt[0].c_str()); + facility = buf; + if (evt.p_from.size() > 1) { + char suf[24]; + snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_from.size() - 1); + facility += suf; + } + } else if (evt.p_type == Generator && !evt.p_busid.empty()) { + char buf[48]; + snprintf(buf, sizeof(buf), "gen %d %s", + evt.p_busid[0], evt.p_genid[0].c_str()); + facility = buf; + if (evt.p_busid.size() > 1) { + char suf[24]; + snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_busid.size() - 1); + facility += suf; + } + } + std::string ct_name = evt.p_name; + while (!ct_name.empty() && ct_name[ct_name.size()-1] == ' ') + ct_name.resize(ct_name.size()-1); + const char *type_str = (evt.p_type == Branch) ? "branch" : "generator"; + std::map > vbymag_ang; + for (size_t vi = 0; vi < v_strs.size(); vi++) { + int bus_id = 0, use_vmag = 0, changed = 0; + double angle = 0.0, vmag = 0.0; + if (sscanf(v_strs[vi].c_str(), "%d %lf %lf %d %d", + &bus_id, &angle, &vmag, &use_vmag, &changed) == 5) { + vbymag_ang[bus_id] = std::make_pair(vmag, angle); + } + } + for (size_t bi = 0; bi < b_strs.size(); bi++) { + char ckt_buf[16] = {0}; + int viol = 0; + double p = 0.0, q = 0.0, perf = 0.0, ratea = 0.0; + int from = 0, to = 0; + if (sscanf(b_strs[bi].c_str(), + "%d %d %15s %lf %lf %lf %lf %d", + &from, &to, ckt_buf, &p, &q, &perf, &ratea, &viol) != 8) { + continue; + } + BranchKey k; + k.from = from; k.to = to; + k.ckt = std::string(ckt_buf); + while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') + k.ckt.resize(k.ckt.size()-1); + std::map::const_iterator it = base_cache.find(k); + if (it == base_cache.end()) { deltaSkipCount++; continue; } + const BaseFlow &bf = it->second; + double cont_mva = std::sqrt(p*p + q*q); + double cont_loading = (ratea > 0.0) ? (cont_mva / ratea) * 100.0 : 0.0; + std::map >::const_iterator vf = + vbymag_ang.find(from); + std::map >::const_iterator vt = + vbymag_ang.find(to); + double v_from_c = (vf != vbymag_ang.end()) ? vf->second.first : 0.0; + double a_from_c = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; + double v_to_c = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; + double a_to_c = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; + double d_ang_b = bf.ang_from_deg - bf.ang_to_deg; + double d_ang_c = a_from_c - a_to_c; + deltaPart << event_idx << "," << ct_name << "," << type_str << "," + << from << "," << to << "," << k.ckt << "," + << std::setprecision(2) << bf.base_kv_from << "," + << std::setprecision(2) << bf.base_kv_to << "," + << bf.area_from << "," << bf.area_to << "," + << std::setprecision(4) << bf.rate_a << "," + << std::setprecision(4) << bf.p_mw << "," + << std::setprecision(4) << p << "," + << std::setprecision(4) << bf.q_mvar << "," + << std::setprecision(4) << q << "," + << std::setprecision(4) << bf.mva << "," + << std::setprecision(4) << cont_mva << "," + << std::setprecision(2) << bf.loading_pct << "," + << std::setprecision(2) << cont_loading << "," + << std::setprecision(6) << bf.v_from_pu << "," + << std::setprecision(6) << v_from_c << "," + << std::setprecision(6) << bf.v_to_pu << "," + << std::setprecision(6) << v_to_c << "," + << std::setprecision(4) << bf.ang_from_deg << "," + << std::setprecision(4) << a_from_c << "," + << std::setprecision(4) << bf.ang_to_deg << "," + << std::setprecision(4) << a_to_c << "," + << std::setprecision(4) << d_ang_b << "," + << std::setprecision(4) << d_ang_c << "," + << facility + << "\n"; + deltaRowCount++; + } + }; + // Set minimum and maximum voltage limits on all buses pf_app.setVoltageLimits(Vmin, Vmax); // Solve the base power flow on every task communicator. Abort if it fails. @@ -670,6 +867,10 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // the base case isn't duplicated in the final file. captureFlatRows(0, std::string("base_case"), world.rank() == 0); } + if (outputFormat == "csv_delta") { + // Cache base-case branch state on every rank for the contingency join. + populateBaseCache(); + } // Check if auto-generation of N-1 contingencies is enabled // FullBranchN1: generate N-1 contingencies for all branches @@ -1114,6 +1315,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (outputFormat == "csv_flat") { captureFlatRows(task_id + 1, events[task_id].p_name, true); } + if (outputFormat == "csv_delta") { + captureDeltaRows(task_id + 1, events[task_id], true); + } recordConv(task_id, "OK", std::string()); // Include results of violation checks in output if (ok) { @@ -1349,49 +1553,72 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Close output file for this contingency if (print_calcs) pf_app.close(); } - // csv_flat: each rank streamed its rows to outputFile_flat..part - // during the loop. Close per-rank files, then on world rank 0 write the - // header to the final file and concatenate each rank's part file in - // rank order, unlinking each as it goes. + // csv_flat / csv_delta: each rank streamed rows to its .part file during + // the loop. Close, sync, then world rank 0 writes header + concatenates. if (outputFormat == "csv_flat") { if (flatPart.is_open()) flatPart.close(); + } + if (outputFormat == "csv_delta") { + if (deltaPart.is_open()) deltaPart.close(); + } + if (wantBusSidecar) { world.sync(); if (world.rank() == 0) { - std::string flatFile = outputFile + "_flat.csv"; - std::ofstream fout(flatFile.c_str(), - std::ios::out | std::ios::trunc | std::ios::binary); - fout << "event_idx,contingency,from_bus,to_bus,circuit_id," - "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," - "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg\n"; - size_t total_rows = 0; const size_t BUFSZ = 1 << 20; std::vector buf(BUFSZ); - for (int p = 0; p < world.size(); p++) { - std::ostringstream oss; - oss << outputFile << "_flat." << p << ".part"; - std::string part = oss.str(); - std::ifstream fin(part.c_str(), std::ios::in | std::ios::binary); - if (!fin) continue; - while (fin) { - fin.read(&buf[0], BUFSZ); - std::streamsize got = fin.gcount(); - if (got > 0) { - fout.write(&buf[0], got); - for (std::streamsize k = 0; k < got; k++) { - if (buf[k] == '\n') total_rows++; + + auto concatParts = [&](const char *suffix, const char *header, + const char *tag, const char *outName) { + std::string outFile = outputFile + outName; + std::ofstream fout(outFile.c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); + fout << header; + size_t rows = 0; + for (int p = 0; p < world.size(); p++) { + std::ostringstream oss; + oss << outputFile << suffix << p << ".part"; + std::string part = oss.str(); + std::ifstream fin(part.c_str(), std::ios::in | std::ios::binary); + if (!fin) continue; + while (fin) { + fin.read(&buf[0], BUFSZ); + std::streamsize got = fin.gcount(); + if (got > 0) { + fout.write(&buf[0], got); + for (std::streamsize k = 0; k < got; k++) { + if (buf[k] == '\n') rows++; + } } } + fin.close(); + std::remove(part.c_str()); } - fin.close(); - std::remove(part.c_str()); + fout.close(); + printf("[%s] wrote %zu rows to %s\n", tag, rows, outFile.c_str()); + }; + + if (outputFormat == "csv_flat") { + concatParts("_flat.", + "event_idx,contingency,from_bus,to_bus,circuit_id," + "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," + "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg\n", + "csv_flat", + "_flat.csv"); + } + if (outputFormat == "csv_delta") { + concatParts("_delta.", + "event_idx,contingency,type,from_bus,to_bus,ckt," + "base_kv_from,base_kv_to,area_from,area_to,rate_a," + "base_p_mw,cont_p_mw,base_q_mvar,cont_q_mvar," + "base_mva,cont_mva,base_loading_pct,cont_loading_pct," + "v_from_base,v_from_cont,v_to_base,v_to_cont," + "ang_from_base,ang_from_cont,ang_to_base,ang_to_cont," + "d_angle_base,d_angle_cont,cont_event_facility\n", + "csv_delta", + "_delta.csv"); } - fout.close(); - printf("[csv_flat] wrote %zu rows to %s\n", total_rows, flatFile.c_str()); - // Bus metadata sidecar: each rank wrote its bus_meta to a .part file - // covering its active+ghost buses, so the same bus_id appears on - // multiple ranks. Read each part, dedupe by bus_id (first writer wins), - // then emit one row per unique bus to outputFile_buses.csv. + // Bus metadata sidecar (deduped by bus_id, first writer wins). std::string busFile = outputFile + "_buses.csv"; std::ofstream bout(busFile.c_str(), std::ios::out | std::ios::trunc | std::ios::binary); @@ -1420,7 +1647,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::remove(part.c_str()); } bout.close(); - printf("[csv_flat] wrote %zu rows to %s\n", bus_rows, busFile.c_str()); + printf("[buses] wrote %zu rows to %s\n", bus_rows, busFile.c_str()); + } + } + // Aggregate skip count across ranks for diagnostics. + if (outputFormat == "csv_delta") { + long localSkip = static_cast(deltaSkipCount); + long totalSkip = localSkip; + world.sum(&totalSkip, 1); + if (world.rank() == 0 && totalSkip > 0) { + printf("[csv_delta] %ld branch rows had no base-cache match\n", + totalSkip); } } From 60f83f302d960a6ad027556b06efb9f68796b858 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sun, 21 Jun 2026 11:07:51 -0700 Subject: [PATCH 14/31] add contingencyRating A|B|C option (default C) in xml and output --- .../contingency_analysis/ca_driver.cpp | 191 ++++++++++++++++-- 1 file changed, 176 insertions(+), 15 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 8fb19c92..c463ba0a 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -406,6 +406,22 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) cursor->get("outputFormat", &outputFormat); std::string outputFile = "ca_results"; cursor->get("outputFile", &outputFile); + // Optional CSV allowlist (from_bus,to_bus,ckt). Empty -> emit all. + std::string monitorBranchesFile; + cursor->get("monitorBranchesFile", &monitorBranchesFile); + // Which rating column csv_flat/csv_delta emit. A|B|C, default C. + // Falls back A->B->C order if requested rating is zero/missing. + std::string contingencyRating = "C"; + cursor->get("contingencyRating", &contingencyRating); + util.toUpper(contingencyRating); + if (contingencyRating != "A" && contingencyRating != "B" && + contingencyRating != "C") { + if (world.rank() == 0) { + printf("WARNING: contingencyRating='%s' not A/B/C; defaulting to C\n", + contingencyRating.c_str()); + } + contingencyRating = "C"; + } // Set static flag for PFBus class BEFORE network creation. // This controls how Q values are reported in output functions: // - When check_Qlim = false: output uses calculated Q from p_Qinj @@ -541,9 +557,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ofstream flatPart; size_t flatRowCount = 0; - // csv_delta wide-row state. Per-rank .part file streamed during the - // contingency loop; base_cache holds base-case branch state (populated - // once after base solve) keyed by (from, to, ckt). + // (from, to, ckt) key shared by the monitor allowlist and base_cache. struct BranchKey { int from, to; std::string ckt; @@ -553,8 +567,134 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) return ckt < o.ckt; } }; + + // Per-branch rate-A/B/C from the parsed network data. Keyed by (from,to,ckt) + // so the csv_flat / csv_delta emit paths can pick the configured rating. + // Built once before the contingency loop. Each rank only sees its own + // active+ghost branches; that's fine -- the emit path is also rank-local. + struct BranchRates { + double rate_a, rate_b, rate_c; + }; + std::map branch_rates; + if (outputFormat == "csv_flat" || outputFormat == "csv_delta") { + int nBranch = pf_network->numBranches(); + for (int i = 0; i < nBranch; i++) { + boost::shared_ptr bd = + pf_network->getBranchData(i); + if (!bd) continue; + int from = 0, to = 0, nelems = 0; + bd->getValue(BRANCH_FROMBUS, &from); + bd->getValue(BRANCH_TOBUS, &to); + if (!bd->getValue(BRANCH_NUM_ELEMENTS, &nelems)) continue; + for (int k = 0; k < nelems; k++) { + std::string ckt; + if (!bd->getValue(BRANCH_CKT, &ckt, k)) continue; + // Trim leading/trailing whitespace and PSS/E surrounding quotes. + size_t a = ckt.find_first_not_of(" \t"); + size_t b = ckt.find_last_not_of(" \t"); + ckt = (a == std::string::npos) ? std::string() + : ckt.substr(a, b - a + 1); + if (ckt.size() >= 2 && ckt.front() == '\'' && ckt.back() == '\'') { + ckt = ckt.substr(1, ckt.size() - 2); + a = ckt.find_first_not_of(" \t"); + b = ckt.find_last_not_of(" \t"); + ckt = (a == std::string::npos) ? std::string() + : ckt.substr(a, b - a + 1); + } + BranchRates r; + r.rate_a = 0.0; r.rate_b = 0.0; r.rate_c = 0.0; + bd->getValue(BRANCH_RATING_A, &r.rate_a, k); + bd->getValue(BRANCH_RATING_B, &r.rate_b, k); + bd->getValue(BRANCH_RATING_C, &r.rate_c, k); + BranchKey key; + key.from = from; key.to = to; key.ckt = ckt; + branch_rates[key] = r; + } + } + } + // Base case always uses rate-A (PSS/E "normal" rating). Contingency rows + // use whichever the user picked, with A->B->C fallback if zero/missing. + auto pickContRate = [&](const BranchRates &r) -> double { + if (contingencyRating == "A") { + return r.rate_a; + } + if (contingencyRating == "B") { + return (r.rate_b > 0.0) ? r.rate_b : r.rate_a; + } + if (r.rate_c > 0.0) return r.rate_c; + if (r.rate_b > 0.0) return r.rate_b; + return r.rate_a; + }; + + // Monitor allowlist parsed from monitorBranchesFile. Empty -> emit all. + std::set monitorSet; + if (!monitorBranchesFile.empty() && + (outputFormat == "csv_flat" || outputFormat == "csv_delta")) { + std::ifstream fin(monitorBranchesFile.c_str()); + if (!fin.is_open()) { + if (world.rank() == 0) { + printf("WARNING: monitorBranchesFile '%s' not found; emitting all branches\n", + monitorBranchesFile.c_str()); + } + } else { + std::string line; + size_t lineNo = 0; + while (std::getline(fin, line)) { + lineNo++; + // Strip trailing CR (Windows line endings). + while (!line.empty() && (line[line.size()-1] == '\r' || + line[line.size()-1] == '\n')) { + line.resize(line.size()-1); + } + // Skip blank lines and comments. + size_t firstNon = line.find_first_not_of(" \t"); + if (firstNon == std::string::npos) continue; + if (line[firstNon] == '#') continue; + // Tokenize on commas. + std::vector tok; + size_t pos = 0; + while (pos <= line.size()) { + size_t comma = line.find(',', pos); + std::string t = (comma == std::string::npos) + ? line.substr(pos) + : line.substr(pos, comma - pos); + size_t a = t.find_first_not_of(" \t"); + size_t b = t.find_last_not_of(" \t"); + tok.push_back((a == std::string::npos) ? std::string() + : t.substr(a, b - a + 1)); + if (comma == std::string::npos) break; + pos = comma + 1; + } + if (tok.size() < 3) continue; + // Skip header row: any non-numeric first field. + if (tok[0].empty()) continue; + bool numeric = true; + for (size_t ci = 0; ci < tok[0].size(); ci++) { + char c = tok[0][ci]; + if (!(c >= '0' && c <= '9') && c != '-' && c != '+') { + numeric = false; break; + } + } + if (!numeric) continue; + BranchKey k; + k.from = atoi(tok[0].c_str()); + k.to = atoi(tok[1].c_str()); + k.ckt = tok[2]; + monitorSet.insert(k); + } + if (world.rank() == 0) { + printf("Monitor allowlist: %zu branches loaded from %s\n", + monitorSet.size(), monitorBranchesFile.c_str()); + } + } + } + auto isMonitored = [&](const BranchKey &k) { + return monitorSet.empty() || monitorSet.find(k) != monitorSet.end(); + }; + struct BaseFlow { - double p_mw, q_mvar, mva, loading_pct, rate_a; + double p_mw, q_mvar, mva, loading_pct; + double base_rate, cont_rate; double v_from_pu, v_to_pu, ang_from_deg, ang_to_deg; double base_kv_from, base_kv_to; int area_from, area_to; @@ -587,7 +727,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // per branch into the rank's .part file. Called once per converged case // (base + each contingency) on every task communicator; non-rank-0 // task_comm members short-circuit after the collective. - auto captureFlatRows = [&](int event_idx, const std::string &name, bool emit) { + auto captureFlatRows = [&](int event_idx, const std::string &name, + bool emit, bool is_base) { std::vector v_strs = pf_app.writeBusString("vr_str"); std::vector b_strs = pf_app.writeBranchString("flow_str"); if (!emit || task_comm.rank() != 0) return; @@ -619,8 +760,18 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } char ckt[4]; std::strncpy(ckt, ckt_buf, 3); ckt[3] = '\0'; + BranchKey mk; + mk.from = from; mk.to = to; mk.ckt = ckt; + while (!mk.ckt.empty() && mk.ckt[mk.ckt.size()-1] == ' ') + mk.ckt.resize(mk.ckt.size()-1); + if (!monitorSet.empty() && monitorSet.find(mk) == monitorSet.end()) continue; + std::map::const_iterator rIt = branch_rates.find(mk); + double rate_sel = ratea; + if (rIt != branch_rates.end()) { + rate_sel = is_base ? rIt->second.rate_a : pickContRate(rIt->second); + } double flow_mva = std::sqrt(p*p + q*q); - double loading_pct = (ratea > 0.0) ? (flow_mva / ratea) * 100.0 : 0.0; + double loading_pct = (rate_sel > 0.0) ? (flow_mva / rate_sel) * 100.0 : 0.0; std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = @@ -634,7 +785,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << std::setprecision(4) << p << "," << std::setprecision(4) << q << "," << std::setprecision(4) << flow_mva << "," - << std::setprecision(4) << ratea << "," + << std::setprecision(4) << rate_sel << "," << std::setprecision(2) << loading_pct << "," << viol << "," << std::setprecision(6) << v_from << "," @@ -679,12 +830,20 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Strip trailing spaces from ckt so the key matches what flow_str // returns later (sscanf %15s already trims leading whitespace). while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); + if (!isMonitored(k)) continue; + double base_rate = ratea, cont_rate = ratea; + std::map::const_iterator rIt = branch_rates.find(k); + if (rIt != branch_rates.end()) { + base_rate = rIt->second.rate_a; + cont_rate = pickContRate(rIt->second); + } BaseFlow bf; bf.p_mw = p; bf.q_mvar = q; bf.mva = std::sqrt(p*p + q*q); - bf.rate_a = ratea; - bf.loading_pct = (ratea > 0.0) ? (bf.mva / ratea) * 100.0 : 0.0; + bf.base_rate = base_rate; + bf.cont_rate = cont_rate; + bf.loading_pct = (base_rate > 0.0) ? (bf.mva / base_rate) * 100.0 : 0.0; std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = @@ -771,11 +930,12 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) k.ckt = std::string(ckt_buf); while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); + if (!isMonitored(k)) continue; std::map::const_iterator it = base_cache.find(k); if (it == base_cache.end()) { deltaSkipCount++; continue; } const BaseFlow &bf = it->second; double cont_mva = std::sqrt(p*p + q*q); - double cont_loading = (ratea > 0.0) ? (cont_mva / ratea) * 100.0 : 0.0; + double cont_loading = (bf.cont_rate > 0.0) ? (cont_mva / bf.cont_rate) * 100.0 : 0.0; std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = @@ -791,7 +951,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << std::setprecision(2) << bf.base_kv_from << "," << std::setprecision(2) << bf.base_kv_to << "," << bf.area_from << "," << bf.area_to << "," - << std::setprecision(4) << bf.rate_a << "," + << std::setprecision(4) << bf.base_rate << "," + << std::setprecision(4) << bf.cont_rate << "," << std::setprecision(4) << bf.p_mw << "," << std::setprecision(4) << p << "," << std::setprecision(4) << bf.q_mvar << "," @@ -865,7 +1026,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // calls writeBusString/writeBranchString which are task_comm collectives, // so every task_comm participates -- but only world rank 0 emits rows so // the base case isn't duplicated in the final file. - captureFlatRows(0, std::string("base_case"), world.rank() == 0); + captureFlatRows(0, std::string("base_case"), world.rank() == 0, true); } if (outputFormat == "csv_delta") { // Cache base-case branch state on every rank for the contingency join. @@ -1313,7 +1474,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) localContingencies.push_back(ctResult); } if (outputFormat == "csv_flat") { - captureFlatRows(task_id + 1, events[task_id].p_name, true); + captureFlatRows(task_id + 1, events[task_id].p_name, true, false); } if (outputFormat == "csv_delta") { captureDeltaRows(task_id + 1, events[task_id], true); @@ -1600,7 +1761,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (outputFormat == "csv_flat") { concatParts("_flat.", "event_idx,contingency,from_bus,to_bus,circuit_id," - "p_from_mw,q_from_mvar,mva_from,rate_a_mva,loading_percent," + "p_from_mw,q_from_mvar,mva_from,rate_mva,loading_percent," "viol,v_from_pu,v_to_pu,ang_from_deg,ang_to_deg\n", "csv_flat", "_flat.csv"); @@ -1608,7 +1769,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (outputFormat == "csv_delta") { concatParts("_delta.", "event_idx,contingency,type,from_bus,to_bus,ckt," - "base_kv_from,base_kv_to,area_from,area_to,rate_a," + "base_kv_from,base_kv_to,area_from,area_to,base_rate_mva,cont_rate_mva," "base_p_mw,cont_p_mw,base_q_mvar,cont_q_mvar," "base_mva,cont_mva,base_loading_pct,cont_loading_pct," "v_from_base,v_from_cont,v_to_base,v_to_cont," From 578bd938c5dd7e30c8f61495f6e5670e49c83823 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sun, 21 Jun 2026 16:00:09 -0700 Subject: [PATCH 15/31] add filter feature to monitoring branches --- .../contingency_analysis/ca_driver.cpp | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index c463ba0a..d4dc26f8 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -90,7 +90,9 @@ std::vector std::string buses; contingencies[idx]->get("contingencyLineBuses",&buses); std::string names; - contingencies[idx]->get("contingencyLineNames",&names); + if (!contingencies[idx]->get("CKT",&names)) { + contingencies[idx]->get("contingencyLineNames",&names); + } // Tokenize bus string to get a list of individual buses std::vector string_vec = utils.blankTokenizer(buses); // Convert buses from character strings to ints @@ -126,7 +128,9 @@ std::vector std::string buses; contingencies[idx]->get("contingencyBuses",&buses); std::string gens; - contingencies[idx]->get("contingencyGenerators",&gens); + if (!contingencies[idx]->get("GenID",&gens)) { + contingencies[idx]->get("contingencyGenerators",&gens); + } // Tokenize bus string to get a list of individual buses std::vector string_vec = utils.blankTokenizer(buses); std::vector bus_ids; @@ -409,6 +413,21 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Optional CSV allowlist (from_bus,to_bus,ckt). Empty -> emit all. std::string monitorBranchesFile; cursor->get("monitorBranchesFile", &monitorBranchesFile); + // Optional area/kV gates. Empty/zero/missing -> no restriction on that + // dimension. Filters AND together with monitorBranchesFile. + std::string monitorAreasStr; + cursor->get("monitorAreas", &monitorAreasStr); + double monitorKvMin = 0.0; + cursor->get("monitorKvMin", &monitorKvMin); + double monitorKvMax = 0.0; + cursor->get("monitorKvMax", &monitorKvMax); + std::set monitorAreas; + { + std::vector tok = util.blankTokenizer(monitorAreasStr); + for (size_t i = 0; i < tok.size(); i++) { + if (!tok[i].empty()) monitorAreas.insert(atoi(tok[i].c_str())); + } + } // Which rating column csv_flat/csv_delta emit. A|B|C, default C. // Falls back A->B->C order if requested rating is zero/missing. std::string contingencyRating = "C"; @@ -691,6 +710,45 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) auto isMonitored = [&](const BranchKey &k) { return monitorSet.empty() || monitorSet.find(k) != monitorSet.end(); }; + // Area/kV gate. Either-endpoint match for areas (catches tie-lines). + // kV is gated on max(kv_from, kv_to) so a 138/13.8 stepdown counts as 138. + // Empty area set / zero kV bound = unrestricted on that dimension. + auto passesAreaKv = [&](int area_from, int area_to, + double kv_from, double kv_to) { + if (!monitorAreas.empty()) { + if (monitorAreas.find(area_from) == monitorAreas.end() && + monitorAreas.find(area_to) == monitorAreas.end()) { + return false; + } + } + double kv_max = (kv_from > kv_to) ? kv_from : kv_to; + if (monitorKvMin > 0.0 && kv_max < monitorKvMin) return false; + if (monitorKvMax > 0.0 && kv_max > monitorKvMax) return false; + return true; + }; + // When monitorBranchesFile presents, it overrides area/kV criteria. + bool haveAreaKvFilter = !monitorAreas.empty() || + monitorKvMin > 0.0 || + monitorKvMax > 0.0; + if (!monitorSet.empty() && haveAreaKvFilter) { + if (world.rank() == 0) { + printf("WARNING: monitorBranchesFile is set; ignoring " + "monitorAreas/monitorKvMin/monitorKvMax\n"); + } + monitorAreas.clear(); + monitorKvMin = 0.0; + monitorKvMax = 0.0; + haveAreaKvFilter = false; + } + if (world.rank() == 0) { + if (!monitorAreas.empty()) { + printf("Monitor areas filter: %zu areas\n", monitorAreas.size()); + } + if (monitorKvMin > 0.0 || monitorKvMax > 0.0) { + printf("Monitor kV filter: min=%.2f max=%.2f (0 means unbounded)\n", + monitorKvMin, monitorKvMax); + } + } struct BaseFlow { double p_mw, q_mvar, mva, loading_pct; @@ -765,6 +823,15 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) while (!mk.ckt.empty() && mk.ckt[mk.ckt.size()-1] == ' ') mk.ckt.resize(mk.ckt.size()-1); if (!monitorSet.empty() && monitorSet.find(mk) == monitorSet.end()) continue; + if (haveAreaKvFilter) { + std::map::const_iterator mf = bus_meta.find(from); + std::map::const_iterator mt = bus_meta.find(to); + int af = (mf != bus_meta.end()) ? mf->second.area : 0; + int at = (mt != bus_meta.end()) ? mt->second.area : 0; + double kf = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + double kt = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + if (!passesAreaKv(af, at, kf, kt)) continue; + } std::map::const_iterator rIt = branch_rates.find(mk); double rate_sel = ratea; if (rIt != branch_rates.end()) { @@ -831,6 +898,15 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // returns later (sscanf %15s already trims leading whitespace). while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); if (!isMonitored(k)) continue; + if (haveAreaKvFilter) { + std::map::const_iterator mf = bus_meta.find(from); + std::map::const_iterator mt = bus_meta.find(to); + int af = (mf != bus_meta.end()) ? mf->second.area : 0; + int at = (mt != bus_meta.end()) ? mt->second.area : 0; + double kf = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; + double kt = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; + if (!passesAreaKv(af, at, kf, kt)) continue; + } double base_rate = ratea, cont_rate = ratea; std::map::const_iterator rIt = branch_rates.find(k); if (rIt != branch_rates.end()) { From cff541f4a0fff89ddd71f254833efad50419cd68 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sun, 21 Jun 2026 17:09:56 -0700 Subject: [PATCH 16/31] Add filters feature, csv_delta outputs, with input examples for IEEE 14-bus and guide --- .../contingency_analysis/README.md | 169 +++++++++++++++--- .../contingency_analysis/ca_driver.cpp | 10 +- .../input/ca/input_14_filters_example.xml | 72 ++++++++ .../input/ca/monitor_branches_14.csv | 10 ++ 4 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 src/applications/data_sets/input/ca/input_14_filters_example.xml create mode 100644 src/applications/data_sets/input/ca/monitor_branches_14.csv diff --git a/src/applications/contingency_analysis/README.md b/src/applications/contingency_analysis/README.md index f95934f2..55b260ff 100644 --- a/src/applications/contingency_analysis/README.md +++ b/src/applications/contingency_analysis/README.md @@ -40,11 +40,77 @@ When combined, duplicates from the file are automatically skipped. | `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 | +| `outputFormat` | `text` / `json` / `csv` / `csv_flat` / `csv_delta` | `text` | +| `outputFile` | Base name for output files | `ca_results` | +| `writeStats` | Emit StatBlock summary files (vmag.txt etc.). Set false to skip and avoid the per-case StatBlock work | true | +| `contingencyRating` | Which PSS/E rating drives `cont_rate_mva` / `cont_loading_pct`: `A`, `B`, or `C` (with A→B→C fallback if missing). `base_rate_mva` always uses rate-A | `C` | +| `monitorBranchesFile` | Path to a CSV allowlist (`from_bus,to_bus,ckt`). When set, overrides the area/kV gates (TARA / PSS/E convention) | (unset) | +| `monitorAreas` | Space-separated list of PSS/E area numbers. Branch is emitted if **either endpoint** is in the set | (unset) | +| `monitorKvMin` | Lower kV threshold; branch passes if `max(kv_from, kv_to) >= monitorKvMin` | 0 (unbounded) | +| `monitorKvMax` | Upper kV threshold; branch passes if `max(kv_from, kv_to) <= monitorKvMax` | 0 (unbounded) | + +### Filtering csv_flat / csv_delta output + +`csv_flat` and `csv_delta` emit per-(contingency, branch) rows. All filters +are optional — unset means "monitor everything". `monitorBranchesFile` is +authoritative when set; otherwise `monitorAreas` and the kV bounds AND +together. + +```xml + + + +monitor_branches.csv + + +11 12 19 +100.0 +500.0 +``` + +`monitorBranchesFile` is a CSV of `from_bus,to_bus,ckt` rows (header +optional; `#` is a line comment). When set, area/kV options are ignored +and a warning is logged. + +`monitorAreas` matches branches with either endpoint in the set (catches +tie-lines). `monitorKvMin/Max` gate on `max(kv_from, kv_to)` so a 138/13.8 +step-down counts as 138. + +`contingencyRating` (`A` | `B` | `C`, default `C`) selects the rating +behind `cont_rate_mva` / `cont_loading_pct`. `base_rate_mva` always uses +rate-A. Falls back A→B→C if the requested rating is zero/missing. + +A complete annotated example is in +`src/applications/data_sets/input/ca/input_14_filters_example.xml` with a +sample monitor file `monitor_branches_14.csv` in the same directory. + +### Output ordering + +Rows are not sorted by contingency. The driver distributes contingencies +across MPI ranks and streams each rank's results to its own `.part` file; +rank 0 concatenates in rank order, so the final file is grouped by rank +and ordered by completion within each rank. Column 1 (`event_idx`) +preserves input-deck order — sort downstream if needed: + +```bash +( head -1 my_run_delta.csv && tail -n +2 my_run_delta.csv | sort -t, -k1,1n ) > my_run_delta.sorted.csv +``` + ### Contingency File Format See `contingencies_nk_example.xml` for examples of N-1, N-2, and N-3 contingency definitions. +The line-tag and generator-id elements accept PSS/E-aligned aliases for clarity: + +| Element | Alias | Holds | +|---|---|---| +| `` | `` | Branch circuit ID (PSS/E `CKT` field) | +| `` | `` | Generator ID (PSS/E `ID` field) | + +Either name works; mix-and-match within the same file is fine. Both legacy +files and new files using the PSS/E names continue to parse without changes. + --- ## Advanced Features @@ -64,13 +130,6 @@ After the contingency analysis completes, the slack bus is restored to its origi 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: @@ -90,23 +149,89 @@ 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 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 +### CSV outputs (`outputFormat=csv_flat` / `csv_delta`) + +When `outputFormat` is set to `csv_flat` or `csv_delta`, the application writes +per-(contingency, branch) rows for downstream statistical analysis instead of +the aggregated `.txt` files described later in this section. All file names +below use the value of `outputFile` as a prefix; the default prefix is +`ca_results`. + +**`_delta.csv`** *(`outputFormat=csv_delta` only)* — wide-form, +one row per (contingency, monitored branch) joining base + contingency state +on the same row. This is the format most downstream consumers prefer because +each row is self-contained (no separate base-case join needed). + +| # | Column | Notes | +|---|---|---| +| 1 | `event_idx` | 0 = base case (only if a base row is emitted), 1..N = contingencies in input-deck order | +| 2 | `contingency` | contingency name from the input XML (`base_case` for the base) | +| 3 | `type` | `branch` or `generator` — what kind of contingency was tripped | +| 4–6 | `from_bus`, `to_bus`, `ckt` | Identity of the **monitored branch** in the row (not the tripped element) | +| 7–8 | `base_kv_from`, `base_kv_to` | Endpoint base kV | +| 9–10 | `area_from`, `area_to` | PSS/E area numbers | +| 11 | `base_rate_mva` | Always rate-A (PSS/E "normal" rating) | +| 12 | `cont_rate_mva` | Rating selected by `contingencyRating` (default C, with A→B→C fallback if zero/missing) | +| 13–14 | `base_p_mw`, `cont_p_mw` | Real-power flow before / after contingency | +| 15–16 | `base_q_mvar`, `cont_q_mvar` | Reactive-power flow before / after | +| 17–18 | `base_mva`, `cont_mva` | `sqrt(P² + Q²)` before / after | +| 19 | `base_loading_pct` | `base_mva / base_rate_mva × 100` | +| 20 | `cont_loading_pct` | `cont_mva / cont_rate_mva × 100` | +| 21–22 | `v_from_base`, `v_from_cont` | From-bus voltage magnitude (pu) before / after | +| 23–24 | `v_to_base`, `v_to_cont` | To-bus voltage magnitude (pu) before / after | +| 25–28 | `ang_from_base`, `ang_from_cont`, `ang_to_base`, `ang_to_cont` | Bus angles (deg) | +| 29–30 | `d_angle_base`, `d_angle_cont` | `ang_from − ang_to` before / after | +| 31 | `cont_event_facility` | Identifier of the **tripped element** in this contingency (e.g. `[area] from to ckt` for branch trips, `gen ` for gen trips) | + +**`_flat.csv`** *(`outputFormat=csv_flat` only)* — long-form, +one row per (case, branch). Columns: `event_idx, contingency, from_bus, +to_bus, ckt, p_from_mw, q_from_mvar, mva_from, rate_mva, loading_percent, +viol, v_from_pu, v_to_pu, ang_from_deg, ang_to_deg`. `rate_mva` is rate-A +on `event_idx=0` rows, the configured `contingencyRating` on contingency +rows. + +**`_buses.csv`** *(both `csv_flat` and `csv_delta`)* — bus +metadata sidecar so the per-branch files can stay narrow. Columns: +`bus_id, bus_name, base_kv, area, zone, owner, area_name, zone_name, +owner_name`. + +**`_convergence.csv`** *(every `outputFormat`)* — one row per +contingency. Columns: `event_idx, contingency, type, status, iterations, +final_tolerance, max_p_bus, max_p_mismatch, max_q_bus, max_q_mismatch`. +Failed/divergent contingencies appear here even though they're omitted +from `_delta.csv` / `_flat.csv`. + +When monitor filters are active, the data-row count of `_delta.csv` / +`_flat.csv` equals `|monitored branches| × |converged contingencies|`. + +#### Pandas quickstart + +```python +import pandas as pd +df = pd.read_csv("my_run_delta.csv") +df[df.cont_loading_pct >= 90.0] # overloaded branches +df.assign(dv=df.v_from_cont - df.v_from_base) \ + .nsmallest(20, "dv")[["contingency","from_bus","dv"]] ``` -- `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) +--- + +### Aggregated `.txt` outputs (`writeStats=true`, default) + +The remaining files in this section are produced by the StatBlock summary +pipeline, controlled by the `writeStats` option (default `true`). They +contain per-element statistics (mean / RMS / min / max) aggregated **across +all contingencies**, not per-contingency rows. Set `writeStats=false` to +skip them when csv_flat / csv_delta output is sufficient. + +The set of files emitted is fixed; their names are not configurable. With +`writeStats=true` you get: `vmag.txt`, `vmag_mm.txt`, `vang.txt`, +`vang_mm.txt`, `pgen.txt`, `pgen_mm.txt`, `qgen.txt`, `qgen_mm.txt`, +`pflow.txt`, `pflow_mm.txt`, `qflow.txt`, `qflow_mm.txt`, `perf_mm.txt`, +`perf_sum.txt`, `line_flt_cnt.txt`. The file `pq_change_cnt.txt` is also +written when `qlim=true`. Per-contingency convergence/status is reported +via the `_convergence.csv` sidecar described above, which is written for +every `outputFormat`. **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/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index d4dc26f8..95df9fd3 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -15,6 +15,11 @@ * - Q-limit support integration * @date 2026-01-31 * + * @updated Yousu Chen + * - csv_flat / csv_delta per-(contingency,branch) outputs + * - monitorBranchesFile / monitorAreas / monitorKvMin/Max filters + * @date 2026-06-21 + * * @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 @@ -777,9 +782,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::string status; }; std::vector localConvRows; - bool emitConv = (outputFormat == "csv" || - outputFormat == "csv_flat" || - outputFormat == "csv_delta"); + // _convergence.csv: written for every outputFormat. + bool emitConv = true; // Lambda: parse current solved flow_str/vr_str and stream one CSV row // per branch into the rank's .part file. Called once per converged case diff --git a/src/applications/data_sets/input/ca/input_14_filters_example.xml b/src/applications/data_sets/input/ca/input_14_filters_example.xml new file mode 100644 index 00000000..47def1a1 --- /dev/null +++ b/src/applications/data_sets/input/ca/input_14_filters_example.xml @@ -0,0 +1,72 @@ + + + + + false + true + true + 1 + 1.1 + 0.9 + true + false + + + csv_delta + ca_IEEE14 + + + + + + + + + + + + + C + + + + IEEE14_ca.raw + 50 + 1.0e-6 + true + + 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/monitor_branches_14.csv b/src/applications/data_sets/input/ca/monitor_branches_14.csv new file mode 100644 index 00000000..2d156828 --- /dev/null +++ b/src/applications/data_sets/input/ca/monitor_branches_14.csv @@ -0,0 +1,10 @@ +from_bus,to_bus,ckt +# Sample monitor allowlist for the IEEE 14-bus case. +# Format: from_bus,to_bus,ckt (one branch per row) +# Header row is tolerated; '#' starts a line comment; blank lines are skipped. +# Whitespace and Windows line endings are tolerated. +1,2,1 +1,5,1 +2,3,1 +4,7,1 +4,9,1 From f814caf5dc57a455717c0f64f87f2d6af902aea2 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 09:34:50 -0700 Subject: [PATCH 17/31] Fix phantom flow on out-of-service branches in getComplexPower --- .../components/pf_matrix/pf_components.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 0aecbd33..55538764 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -3571,7 +3571,16 @@ gridpack::ComplexType gridpack::powerflow::PFBranch::getComplexPower( { gridpack::ComplexType vi, vj, Yii, Yij, s; s = ComplexType(0.0,0.0); - gridpack::powerflow::PFBus *bus1 = + // Out-of-service line: getLineElements does not gate on status, so + // return zero to avoid a phantom flow from stale admittance. + int bsize = p_branch_status.size(); + for (int i=0; i(getBus1().get()); vi = bus1->getComplexVoltage(); gridpack::powerflow::PFBus *bus2 = @@ -3592,6 +3601,14 @@ gridpack::ComplexType gridpack::powerflow::PFBranch::getReversePower( { gridpack::ComplexType vi, vj, Yjj, Yji, s; s = ComplexType(0.0,0.0); + // Out-of-service line: see getComplexPower. + int bsize = p_branch_status.size(); + for (int i=0; i(getBus1().get()); vi = bus1->getComplexVoltage(); From 38484d5ae0d30da324f2b1fe60cca58d83a1283e Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 10:27:25 -0700 Subject: [PATCH 18/31] Populate BRANCH_RATING_A/B/C from RATE1-3 for 2W xfmrs in PTI34/PTI35 --- .../block_parsers/transformer_parser34.cpp | 20 ++++++++++++------- .../block_parsers/transformer_parser35.cpp | 20 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/parser/block_parsers/transformer_parser34.cpp b/src/parser/block_parsers/transformer_parser34.cpp index 32d08a33..af42576e 100644 --- a/src/parser/block_parsers/transformer_parser34.cpp +++ b/src/parser/block_parsers/transformer_parser34.cpp @@ -633,14 +633,20 @@ void gridpack::parser::TransformerParser34::parse( /* * type: float - * BRANCH_RATE1-12 + * BRANCH_RATE1-12; also mirror the first three into + * BRANCH_RATING_A/B/C so downstream consumers (PFBranch::load, + * overload checks, JSON export) see the ratings that other PTI + * dialects populate. */ - p_branchData[l_idx]->addValue(BRANCH_RATE1, - atof(split_line3[3].c_str()),nelems); - p_branchData[l_idx]->addValue(BRANCH_RATE2, - atof(split_line3[4].c_str()),nelems); - p_branchData[l_idx]->addValue(BRANCH_RATE3, - atof(split_line3[5].c_str()),nelems); + double rate1 = atof(split_line3[3].c_str()); + double rate2 = atof(split_line3[4].c_str()); + double rate3 = atof(split_line3[5].c_str()); + p_branchData[l_idx]->addValue(BRANCH_RATE1, rate1, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATE2, rate2, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATE3, rate3, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_A, rate1, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_B, rate2, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_C, rate3, nelems); p_branchData[l_idx]->addValue(BRANCH_RATE4, atof(split_line3[6].c_str()),nelems); p_branchData[l_idx]->addValue(BRANCH_RATE5, diff --git a/src/parser/block_parsers/transformer_parser35.cpp b/src/parser/block_parsers/transformer_parser35.cpp index 514d69f8..2bdbc440 100644 --- a/src/parser/block_parsers/transformer_parser35.cpp +++ b/src/parser/block_parsers/transformer_parser35.cpp @@ -634,14 +634,18 @@ void gridpack::parser::TransformerParser35::parse( /* * type: float - * BRANCH_RATE1-12 - */ - p_branchData[l_idx]->addValue(BRANCH_RATE1, - atof(split_line3[3].c_str()),nelems); - p_branchData[l_idx]->addValue(BRANCH_RATE2, - atof(split_line3[4].c_str()),nelems); - p_branchData[l_idx]->addValue(BRANCH_RATE3, - atof(split_line3[5].c_str()),nelems); + * BRANCH_RATE1-12; also mirror the first three into + * BRANCH_RATING_A/B/C for downstream consumers. + */ + double rate1 = atof(split_line3[3].c_str()); + double rate2 = atof(split_line3[4].c_str()); + double rate3 = atof(split_line3[5].c_str()); + p_branchData[l_idx]->addValue(BRANCH_RATE1, rate1, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATE2, rate2, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATE3, rate3, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_A, rate1, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_B, rate2, nelems); + p_branchData[l_idx]->addValue(BRANCH_RATING_C, rate3, nelems); p_branchData[l_idx]->addValue(BRANCH_RATE4, atof(split_line3[6].c_str()),nelems); p_branchData[l_idx]->addValue(BRANCH_RATE5, From 458feddb040c649e276937cc8a5a6e9c62e83121 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 10:28:41 -0700 Subject: [PATCH 19/31] Unify contingencyRating across CA violation logic and JSON loading_percent --- .../modules/powerflow/pf_app_module.cpp | 14 +- .../modules/powerflow/pf_app_module.hpp | 6 + .../modules/powerflow/pf_factory_module.cpp | 132 +++++++++--------- .../modules/powerflow/pf_factory_module.hpp | 13 ++ src/utilities/results_exporter.cpp | 70 +++++++--- src/utilities/results_exporter.hpp | 33 ++++- 6 files changed, 180 insertions(+), 88 deletions(-) diff --git a/src/applications/modules/powerflow/pf_app_module.cpp b/src/applications/modules/powerflow/pf_app_module.cpp index bd33584d..d5fcb0a3 100644 --- a/src/applications/modules/powerflow/pf_app_module.cpp +++ b/src/applications/modules/powerflow/pf_app_module.cpp @@ -1156,9 +1156,13 @@ gridpack::powerflow::PFAppModule::collectResults() bres.qLoss = bres.qFrom + bres.qTo; bres.rateA = branch->getBranchRatingA(cktIds[k]); - if (bres.rateA > 0.0) { + // Under the current contingency rating tier (A/B/C with fallback). + double ratePicked = p_factory->pickBranchRating(i, static_cast(k)); + if (ratePicked <= 0.0) ratePicked = bres.rateA; + bres.rateSelected = ratePicked; + if (ratePicked > 0.0) { double maxMVA = (bres.mvaFrom > bres.mvaTo) ? bres.mvaFrom : bres.mvaTo; - bres.loadingPercent = maxMVA / bres.rateA * 100.0; + bres.loadingPercent = maxMVA / ratePicked * 100.0; } else { bres.loadingPercent = 0.0; } @@ -1872,6 +1876,12 @@ void gridpack::powerflow::PFAppModule::useRateB(bool flag) p_factory->useRateB(flag); } +void gridpack::powerflow::PFAppModule::setContingencyRating( + const std::string& rating) +{ + p_factory->setContingencyRating(rating); +} + /** * Suppress all output from power flow module * @param flag if true, suppress printing diff --git a/src/applications/modules/powerflow/pf_app_module.hpp b/src/applications/modules/powerflow/pf_app_module.hpp index 1582dbd9..5d7ab249 100644 --- a/src/applications/modules/powerflow/pf_app_module.hpp +++ b/src/applications/modules/powerflow/pf_app_module.hpp @@ -436,6 +436,12 @@ class PFAppModule */ void useRateB(bool flag); + /** + * Select rating tier ("A" | "B" | "C") for CA violation checks and + * loadingPercent. A->B->C fallback when the picked tier is zero. + */ + void setContingencyRating(const std::string& rating); + /** * Suppress all output from power flow module * @param flag if true, suppress printing diff --git a/src/applications/modules/powerflow/pf_factory_module.cpp b/src/applications/modules/powerflow/pf_factory_module.cpp index a18d8624..7ed8d030 100644 --- a/src/applications/modules/powerflow/pf_factory_module.cpp +++ b/src/applications/modules/powerflow/pf_factory_module.cpp @@ -58,6 +58,7 @@ PFFactoryModule::PFFactoryModule(PFFactoryModule::NetworkPtr network) { p_network = network; p_rateB = false; + p_contingencyRating = "A"; p_islandCount = 0; p_hasLoneBus = false; p_qlim_deadband = 0.1; @@ -701,53 +702,38 @@ bool gridpack::powerflow::PFFactoryModule::checkLineOverloadViolations() int numBranch = p_network->numBranches(); int i; bool branch_ok = true; + // p_rateB is legacy rtpr; treat it as "rating tier B" for one call. + std::string savedRating = p_contingencyRating; + if (p_rateB) p_contingencyRating = "B"; for (i=0; igetActiveBranch(i)) { gridpack::powerflow::PFBranch *branch = dynamic_cast (p_network->getBranch(i).get()); - // Loop over all lines in the branch and choose the smallest rating value int nlines; p_network->getBranchData(i)->getValue(BRANCH_NUM_ELEMENTS,&nlines); std::vector tags = branch->getLineTags(); - double rate; for (int k = 0; kgetIgnore(tags[k])) { - bool foundRating=false; - if (p_rateB) { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_B,&rate,k)) { - foundRating = true; - } else { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_A,&rate,k)) { - foundRating = true; - } - } - } else { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_A,&rate,k)) { - foundRating = true; - } - } - if (foundRating) { - if (rate > 0.0) { - gridpack::ComplexType s = branch->getComplexPower(tags[k]); - double pq = abs(s); - if (pq > rate) { - branch_ok = false; - gridpack::powerflow::PFFactoryModule::Violation violation; - violation.bus_violation = false; - violation.line_violation = true; - violation.bus1 = branch->getBus1OriginalIndex(); - violation.bus2 = branch->getBus2OriginalIndex(); - strncpy(violation.tag,tags[k].c_str(),2); - violation.tag[2] = '\0'; - p_violations.push_back(violation); - } - } - } + if (branch->getIgnore(tags[k])) continue; + double rate = pickBranchRating(i, k); + if (rate <= 0.0) continue; + gridpack::ComplexType s = branch->getComplexPower(tags[k]); + double pq = abs(s); + if (pq > rate) { + branch_ok = false; + gridpack::powerflow::PFFactoryModule::Violation violation; + violation.bus_violation = false; + violation.line_violation = true; + violation.bus1 = branch->getBus1OriginalIndex(); + violation.bus2 = branch->getBus2OriginalIndex(); + strncpy(violation.tag,tags[k].c_str(),2); + violation.tag[2] = '\0'; + p_violations.push_back(violation); } } } } + p_contingencyRating = savedRating; return checkTrue(branch_ok); } @@ -762,52 +748,34 @@ bool gridpack::powerflow::PFFactoryModule::checkLineOverloadViolations(int area) int numBranch = p_network->numBranches(); int i; bool branch_ok = true; + std::string savedRating = p_contingencyRating; + if (p_rateB) p_contingencyRating = "B"; for (i=0; igetActiveBranch(i)) { gridpack::powerflow::PFBranch *branch = dynamic_cast (p_network->getBranch(i).get()); - // get buses at either end gridpack::powerflow::PFBus *bus1 = dynamic_cast (branch->getBus1().get()); gridpack::powerflow::PFBus *bus2 = dynamic_cast (branch->getBus2().get()); - // Loop over all lines in the branch and choose the smallest rating value - if (bus1->getArea() == area || bus2->getArea() == area) { - int nlines; - p_network->getBranchData(i)->getValue(BRANCH_NUM_ELEMENTS,&nlines); - std::vector tags = branch->getLineTags(); - double rate; - for (int k = 0; kgetIgnore(tags[k])) { - bool foundRating=false; - if (p_rateB) { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_B,&rate,k)) { - foundRating = true; - } else { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_A,&rate,k)) { - foundRating = true; - } - } - } else { - if (p_network->getBranchData(i)->getValue(BRANCH_RATING_A,&rate,k)) { - foundRating = true; - } - } - if (foundRating) { - if (rate > 0.0) { - gridpack::ComplexType s = branch->getComplexPower(tags[k]); - double pq = abs(s); - if (pq > rate) branch_ok = false; - } - } - } - } + if (bus1->getArea() != area && bus2->getArea() != area) continue; + int nlines; + p_network->getBranchData(i)->getValue(BRANCH_NUM_ELEMENTS,&nlines); + std::vector tags = branch->getLineTags(); + for (int k = 0; kgetIgnore(tags[k])) continue; + double rate = pickBranchRating(i, k); + if (rate <= 0.0) continue; + gridpack::ComplexType s = branch->getComplexPower(tags[k]); + double pq = abs(s); + if (pq > rate) branch_ok = false; } } } + p_contingencyRating = savedRating; return checkTrue(branch_ok); } @@ -1484,6 +1452,38 @@ void gridpack::powerflow::PFFactoryModule::useRateB(bool flag) } } +/** + * Select rating tier for overload checks. + */ +void gridpack::powerflow::PFFactoryModule::setContingencyRating( + const std::string& rating) +{ + if (rating == "A" || rating == "B" || rating == "C") { + p_contingencyRating = rating; + } else { + p_contingencyRating = "A"; + } +} + +/** + * Rating for one line element under the current contingency tier + * with A->B->C fallback when the picked tier is zero/missing. + */ +double gridpack::powerflow::PFFactoryModule::pickBranchRating( + int branchLocalIdx, int elemIdx) const +{ + double a = 0.0, b = 0.0, c = 0.0; + p_network->getBranchData(branchLocalIdx)->getValue(BRANCH_RATING_A, &a, elemIdx); + p_network->getBranchData(branchLocalIdx)->getValue(BRANCH_RATING_B, &b, elemIdx); + p_network->getBranchData(branchLocalIdx)->getValue(BRANCH_RATING_C, &c, elemIdx); + if (p_contingencyRating == "A") return a; + if (p_contingencyRating == "B") return (b > 0.0) ? b : a; + // "C" + if (c > 0.0) return c; + if (b > 0.0) return b; + return a; +} + /** * Check switched shunt violations and adjust shunt B values. * For buses with SWREM != 0, resolves remote bus voltage via getLocalBusIndices. diff --git a/src/applications/modules/powerflow/pf_factory_module.hpp b/src/applications/modules/powerflow/pf_factory_module.hpp index a1b783fd..20fddae6 100644 --- a/src/applications/modules/powerflow/pf_factory_module.hpp +++ b/src/applications/modules/powerflow/pf_factory_module.hpp @@ -365,6 +365,18 @@ class PFFactoryModule */ void useRateB(bool flag); + /** + * Select which rating tier drives overload checks. 'A', 'B', 'C'; + * A->B->C fallback when the picked tier is zero/missing. + */ + void setContingencyRating(const std::string& rating); + std::string getContingencyRating() const { return p_contingencyRating; } + + /** + * Rating for one branch element under the current tier + fallback. + */ + double pickBranchRating(int branchLocalIdx, int elemIdx) const; + private: NetworkPtr p_network; @@ -381,6 +393,7 @@ class PFFactoryModule std::vector p_violations; bool p_rateB; + std::string p_contingencyRating; // "A" | "B" | "C" (default "A") double p_qlim_deadband; // Q deadband (Mvar) for PV->PQ switch }; diff --git a/src/utilities/results_exporter.cpp b/src/utilities/results_exporter.cpp index b55a0f79..b8e24182 100644 --- a/src/utilities/results_exporter.cpp +++ b/src/utilities/results_exporter.cpp @@ -144,6 +144,7 @@ void ResultsExporter::writeBranchesJSON(std::ostream& out, out << indent << " \"mva_from\": " << std::setprecision(4) << br.mvaFrom << ",\n"; out << indent << " \"mva_to\": " << std::setprecision(4) << br.mvaTo << ",\n"; out << indent << " \"rate_a_mva\": " << std::setprecision(4) << br.rateA << ",\n"; + out << indent << " \"rate_selected_mva\": " << std::setprecision(4) << br.rateSelected << ",\n"; out << indent << " \"loading_percent\": " << std::setprecision(2) << br.loadingPercent << "\n"; out << indent << " }"; if (i + 1 < branches.size()) { @@ -183,6 +184,52 @@ void ResultsExporter::writeGeneratorsJSON(std::ostream& out, out << indent << "]"; } +// ------------------------------------------------------------- +// writeViolationsJSON +// ------------------------------------------------------------- +void ResultsExporter::writeViolationsJSON(std::ostream& out, + const std::vector& brViols, + const std::vector& vViols, + const std::string& indent) +{ + out << std::fixed; + out << indent << "\"violations\": {\n"; + out << indent << " \"branches\": ["; + for (size_t i = 0; i < brViols.size(); ++i) { + const BranchViolation& v = brViols[i]; + out << (i == 0 ? "\n" : ",\n"); + out << indent << " {" + << "\"from_bus\": " << v.fromBus + << ", \"to_bus\": " << v.toBus + << ", \"circuit_id\": \"" << escapeJSON(v.circuitId) << "\"" + << ", \"mva\": " << std::setprecision(4) << v.mva + << ", \"rate_mva\": " << std::setprecision(4) << v.rate + << ", \"loading_percent\": " << std::setprecision(2) << v.loadingPercent + << ", \"base_mva\": " << std::setprecision(4) << v.baseMva + << ", \"delta_mva\": " << std::setprecision(4) << v.deltaMva + << ", \"severity\": \"" << escapeJSON(v.severity) << "\"" + << "}"; + } + if (!brViols.empty()) out << "\n" << indent << " "; + out << "],\n"; + out << indent << " \"voltages\": ["; + for (size_t i = 0; i < vViols.size(); ++i) { + const VoltageViolation& v = vViols[i]; + out << (i == 0 ? "\n" : ",\n"); + out << indent << " {" + << "\"bus_id\": " << v.busId + << ", \"v_pu\": " << std::setprecision(6) << v.vPu + << ", \"limit_low\": " << std::setprecision(4) << v.limitLow + << ", \"limit_high\": " << std::setprecision(4) << v.limitHigh + << ", \"deviation_pu\": " << std::setprecision(6) << v.deviationPu + << ", \"severity\": \"" << escapeJSON(v.severity) << "\"" + << "}"; + } + if (!vViols.empty()) out << "\n" << indent << " "; + out << "]\n"; + out << indent << "}"; +} + // ------------------------------------------------------------- // writePFJSON // ------------------------------------------------------------- @@ -221,23 +268,7 @@ void ResultsExporter::writeCAJSON(std::ofstream& out, out << " \"contingencies\": [\n"; for (size_t i = 0; i < r.contingencies.size(); ++i) { - const ContingencyResult& ct = r.contingencies[i]; - out << " {\n"; - out << " \"name\": \"" << escapeJSON(ct.name) << "\",\n"; - out << " \"type\": \"" << escapeJSON(ct.type) << "\",\n"; - out << " \"has_voltage_violation\": " << (ct.hasVoltageViolation ? "true" : "false") << ",\n"; - out << " \"has_branch_violation\": " << (ct.hasBranchViolation ? "true" : "false") << ",\n"; - out << " \"solution\": {\n"; - writeConvergenceJSON(out, ct.solution.convergence, " "); - out << ",\n"; - writeBusesJSON(out, ct.solution.buses, " "); - out << ",\n"; - writeBranchesJSON(out, ct.solution.branches, " "); - out << ",\n"; - writeGeneratorsJSON(out, ct.solution.generators, " "); - out << "\n"; - out << " }\n"; - out << " }"; + writeContingencyResultJSON(out, r.contingencies[i]); if (i + 1 < r.contingencies.size()) { out << ","; } @@ -320,7 +351,7 @@ void ResultsExporter::writePFCSV(const std::string& basename, out << "from_bus,to_bus,circuit_id," << "p_from_mw,q_from_mvar,p_to_mw,q_to_mvar," << "p_loss_mw,q_loss_mvar," - << "mva_from,mva_to,rate_a_mva,loading_percent\n"; + << "mva_from,mva_to,rate_a_mva,rate_selected_mva,loading_percent\n"; } for (size_t i = 0; i < r.branches.size(); ++i) { @@ -340,6 +371,7 @@ void ResultsExporter::writePFCSV(const std::string& basename, << std::setprecision(4) << br.mvaFrom << "," << std::setprecision(4) << br.mvaTo << "," << std::setprecision(4) << br.rateA << "," + << std::setprecision(4) << br.rateSelected << "," << std::setprecision(2) << br.loadingPercent << "\n"; } out.close(); @@ -485,6 +517,8 @@ void ResultsExporter::writeContingencyResultJSON(std::ostream& out, << (ct.hasVoltageViolation ? "true" : "false") << ",\n"; out << " \"has_branch_violation\": " << (ct.hasBranchViolation ? "true" : "false") << ",\n"; + writeViolationsJSON(out, ct.branchViolations, ct.voltageViolations, " "); + out << ",\n"; out << " \"solution\": {\n"; writeConvergenceJSON(out, ct.solution.convergence, " "); out << ",\n"; diff --git a/src/utilities/results_exporter.hpp b/src/utilities/results_exporter.hpp index 6ad70bb9..49a940fd 100644 --- a/src/utilities/results_exporter.hpp +++ b/src/utilities/results_exporter.hpp @@ -58,8 +58,9 @@ struct BranchResult { double qLoss; // MVAr double mvaFrom; // MVA double mvaTo; // MVA - double rateA; // MVA - double loadingPercent; // max(|S_from|,|S_to|)/rateA * 100 + double rateA; // MVA (always rate-A) + double rateSelected; // MVA under the picked contingency rating tier + double loadingPercent; // max(|S_from|,|S_to|)/rateSelected * 100 }; struct GeneratorResult { @@ -95,12 +96,35 @@ struct PowerFlowResults { std::vector generators; }; +struct BranchViolation { + int fromBus; + int toBus; + std::string circuitId; + double mva; // max(|S_from|,|S_to|) + double rate; // MVA under selected tier + double loadingPercent; // mva / rate * 100 + double baseMva; // base-case max MVA on same element (0 if unknown) + double deltaMva; // mva - baseMva + std::string severity; // "warning" (<105%), "critical" (>=105%) +}; + +struct VoltageViolation { + int busId; + double vPu; + double limitLow; + double limitHigh; + double deviationPu; // signed: v - nearest limit + std::string severity; +}; + struct ContingencyResult { std::string name; std::string type; // "branch" or "generator" PowerFlowResults solution; bool hasVoltageViolation; bool hasBranchViolation; + std::vector branchViolations; + std::vector voltageViolations; }; struct ContingencyAnalysisResults { @@ -192,6 +216,11 @@ class ResultsExporter { const std::vector& gens, const std::string& indent); + static void writeViolationsJSON(std::ostream& out, + const std::vector& brViols, + const std::vector& vViols, + const std::string& indent); + static void writePFJSON(std::ofstream& out, const PowerFlowResults& r); static void writeCAJSON(std::ofstream& out, From 0ad54411de44447426ff214b9ed7b2bab163b477 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 10:29:25 -0700 Subject: [PATCH 20/31] Add violation reporting --- .../contingency_analysis/ca_driver.cpp | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 95df9fd3..3ab272cd 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -446,6 +446,12 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } contingencyRating = "C"; } + // Severity threshold for violation reporting; loading% > threshold*100 + // is flagged. Default 1.0 (100% of rate). + double violationSeverityThreshold = 1.0; + cursor->get("violationSeverityThreshold", &violationSeverityThreshold); + if (violationSeverityThreshold <= 0.0) violationSeverityThreshold = 1.0; + // Set static flag for PFBus class BEFORE network creation. // This controls how Q values are reported in output functions: // - When check_Qlim = false: output uses calculated Q from p_Qinj @@ -581,6 +587,111 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::ofstream flatPart; size_t flatRowCount = 0; + // Per-rank _violations.csv stream. Populated by every output format that + // knows about violations (json/csv via populateViolations; csv_flat/csv_delta + // inline where loading_pct is already computed). Emit-branch and emit-voltage + // helpers below open the file lazily. + std::string violPartPath; + { + std::ostringstream oss; + oss << outputFile << "_violations." << world.rank() << ".part"; + violPartPath = oss.str(); + } + std::ofstream violPart; + size_t violRowCount = 0; + // Running summary state; captureFlatRows / captureDeltaRows / populateViolations + // all funnel through the emit helpers, so this stays consistent regardless of + // outputFormat. + struct WorstBranchState { + double loading_pct = 0.0; + int from = 0, to = 0; + std::string ckt; + std::string ct_name; + }; + struct WorstVoltageState { + double v_pu = 1.0; + double dev_pu = 0.0; // signed + int bus_id = 0; + std::string ct_name; + }; + WorstBranchState worstBr; + WorstVoltageState worstVLo; + worstVLo.v_pu = 1e9; // start high so any real low value beats it + WorstVoltageState worstVHi; + worstVHi.v_pu = -1e9; + std::set ctsWithBranchViol; + std::set ctsWithVoltageViol; + auto openViolPart = [&]() { + if (violPart.is_open()) return; + violPart.open(violPartPath.c_str(), std::ios::out | std::ios::trunc); + violPart << std::fixed; + }; + // Row schema (same 11 columns for branch and voltage rows; unused fields blank): + // event_idx,contingency,type,element,mva_or_vpu,rate_or_limit,loading_percent, + // base_mva,delta,severity + // For branch: element = "from-to-ckt", mva_or_vpu = MVA, rate_or_limit = rate, + // loading_percent = %, base_mva/delta populated, severity. + // For voltage: element = "bus_id", mva_or_vpu = v_pu, rate_or_limit = "low/high", + // loading_percent = "", base_mva = "", delta = signed dev pu. + auto emitBranchViolation = [&](int event_idx, const std::string &ct_name, + int from, int to, const std::string &ckt, + double mva, double rate, + double loading_pct, double base_mva) { + openViolPart(); + const char *sev = (loading_pct >= 105.0) ? "critical" : "warning"; + violPart << event_idx << "," << ct_name << ",branch," + << from << "-" << to << "-" << ckt << "," + << std::setprecision(4) << mva << "," + << std::setprecision(4) << rate << "," + << std::setprecision(2) << loading_pct << "," + << std::setprecision(4) << base_mva << "," + << std::setprecision(4) << (mva - base_mva) << "," + << sev << "\n"; + violRowCount++; + if (loading_pct > worstBr.loading_pct) { + worstBr.loading_pct = loading_pct; + worstBr.from = from; worstBr.to = to; + worstBr.ckt = ckt; + worstBr.ct_name = ct_name; + } + ctsWithBranchViol.insert(ct_name); + }; + auto emitVoltageViolation = [&](int event_idx, const std::string &ct_name, + int bus_id, double v_pu, + double lo, double hi) { + openViolPart(); + double dev = (v_pu < lo) ? (v_pu - lo) + : (v_pu > hi) ? (v_pu - hi) + : 0.0; + if (dev == 0.0) return; + const char *sev = (std::abs(dev) >= 0.05) ? "critical" : "warning"; + // rate_or_limit column holds "low_pu:high_pu" for voltage rows. + std::ostringstream limits; + limits << std::setprecision(4) << std::fixed << lo << ":" << hi; + violPart << event_idx << "," << ct_name << ",voltage," + << bus_id << "," + << std::setprecision(6) << v_pu << "," + << limits.str() << "," + << "," // loading_percent blank + << "," // base_mva blank + << std::setprecision(6) << dev << "," + << sev << "\n"; + violRowCount++; + if (dev < 0.0 && v_pu < worstVLo.v_pu) { + worstVLo.v_pu = v_pu; + worstVLo.dev_pu = dev; + worstVLo.bus_id = bus_id; + worstVLo.ct_name = ct_name; + } + if (dev > 0.0 && v_pu > worstVHi.v_pu) { + worstVHi.v_pu = v_pu; + worstVHi.dev_pu = dev; + worstVHi.bus_id = bus_id; + worstVHi.ct_name = ct_name; + } + ctsWithVoltageViol.insert(ct_name); + }; + // (from, to, ckt) key shared by the monitor allowlist and base_cache. struct BranchKey { int from, to; @@ -843,6 +954,15 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } double flow_mva = std::sqrt(p*p + q*q); double loading_pct = (rate_sel > 0.0) ? (flow_mva / rate_sel) * 100.0 : 0.0; + // Stream to _violations.csv for csv_flat runs (contingency rows only). + if (!is_base && loading_pct > violationSeverityThreshold * 100.0) { + double base_mva = 0.0; + // No base_cache in csv_flat mode; look up in the persistent map built + // by populateBaseCache-style capture below? We don't have one for + // csv_flat, so base_mva stays 0 -- delta will just equal mva. + emitBranchViolation(event_idx, ct_name, from, to, ckt, + flow_mva, rate_sel, loading_pct, base_mva); + } std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = @@ -1016,6 +1136,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) const BaseFlow &bf = it->second; double cont_mva = std::sqrt(p*p + q*q); double cont_loading = (bf.cont_rate > 0.0) ? (cont_mva / bf.cont_rate) * 100.0 : 0.0; + // Stream to _violations.csv (delta path knows base_mva already). + if (cont_loading > violationSeverityThreshold * 100.0) { + emitBranchViolation(event_idx, ct_name, from, to, k.ckt, + cont_mva, bf.cont_rate, cont_loading, bf.mva); + } std::map >::const_iterator vf = vbymag_ang.find(from); std::map >::const_iterator vt = @@ -1059,6 +1184,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Set minimum and maximum voltage limits on all buses pf_app.setVoltageLimits(Vmin, Vmax); + // Route CA violation checks and loadingPercent through the same rating tier. + pf_app.setContingencyRating(contingencyRating); // Solve the base power flow on every task communicator. Abort if it fails. bool baseSolveOk = false; try { @@ -1417,6 +1544,66 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Local contingency results storage for JSON/CSV export std::vector localContingencies; + // Base-case per-element MVA lookup, keyed on (from,to,ckt). + // Populated on rank 0 of each task_comm (only place collectResults ran). + // Used to fill BranchViolation.baseMva/deltaMva during contingency reporting. + std::map baseMvaByKey; + if (outputFormat == "json" || outputFormat == "csv") { + for (size_t bi = 0; bi < baseCaseResults.branches.size(); bi++) { + const gridpack::utility::BranchResult &br = baseCaseResults.branches[bi]; + BranchKey k; k.from = br.fromBus; k.to = br.toBus; k.ckt = br.circuitId; + while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); + double mva = (br.mvaFrom > br.mvaTo) ? br.mvaFrom : br.mvaTo; + baseMvaByKey[k] = mva; + } + } + + // Fill BranchViolation/VoltageViolation arrays from a solved ct result, + // and stream the same rows to _violations..part. + // event_idx of 0 is reserved for base case; contingencies get task_id+1. + auto populateViolations = [&](gridpack::utility::ContingencyResult &ct, + int event_idx) { + const double threshPct = violationSeverityThreshold * 100.0; + for (size_t bi = 0; bi < ct.solution.branches.size(); bi++) { + const gridpack::utility::BranchResult &br = ct.solution.branches[bi]; + if (br.loadingPercent <= threshPct) continue; + gridpack::utility::BranchViolation v; + v.fromBus = br.fromBus; + v.toBus = br.toBus; + v.circuitId = br.circuitId; + v.mva = (br.mvaFrom > br.mvaTo) ? br.mvaFrom : br.mvaTo; + v.rate = br.rateSelected; + v.loadingPercent = br.loadingPercent; + BranchKey k; k.from = br.fromBus; k.to = br.toBus; k.ckt = br.circuitId; + while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); + std::map::const_iterator it = baseMvaByKey.find(k); + v.baseMva = (it != baseMvaByKey.end()) ? it->second : 0.0; + v.deltaMva = v.mva - v.baseMva; + v.severity = (br.loadingPercent >= 105.0) ? "critical" : "warning"; + ct.branchViolations.push_back(v); + emitBranchViolation(event_idx, ct.name, v.fromBus, v.toBus, v.circuitId, + v.mva, v.rate, v.loadingPercent, v.baseMva); + } + for (size_t bi = 0; bi < ct.solution.buses.size(); bi++) { + const gridpack::utility::BusResult &b = ct.solution.buses[bi]; + double v_pu = b.voltage; + if (v_pu <= 0.0) continue; // Skip isolated / not-solved buses. + bool lo = v_pu < Vmin, hi = v_pu > Vmax; + if (!lo && !hi) continue; + gridpack::utility::VoltageViolation vv; + vv.busId = b.busId; + vv.vPu = v_pu; + vv.limitLow = Vmin; + vv.limitHigh = Vmax; + vv.deviationPu = lo ? (v_pu - Vmin) : (v_pu - Vmax); + double dev = std::abs(vv.deviationPu); + vv.severity = (dev >= 0.05) ? "critical" : "warning"; + ct.voltageViolations.push_back(vv); + emitVoltageViolation(event_idx, ct.name, vv.busId, vv.vPu, + vv.limitLow, vv.limitHigh); + } + }; + // Convergence row recorder; indexes events[task_id]. auto recordConv = [&](int task_id, const char *status, const std::string &) { @@ -1551,6 +1738,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) ctResult.hasVoltageViolation = !ok1; ctResult.hasBranchViolation = !ok2; ctResult.solution = pf_app.collectResults(); + populateViolations(ctResult, static_cast(task_id) + 1); localContingencies.push_back(ctResult); } if (outputFormat == "csv_flat") { @@ -1891,6 +2079,43 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) printf("[buses] wrote %zu rows to %s\n", bus_rows, busFile.c_str()); } } + // Concat per-rank _violations..part into _violations.csv. + // Runs for every outputFormat; ranks that emitted zero rows simply have no + // .part file to include. + if (violPart.is_open()) violPart.close(); + world.sync(); + if (world.rank() == 0) { + const size_t BUFSZ = 1 << 20; + std::vector buf(BUFSZ); + std::string outFile = outputFile + "_violations.csv"; + std::ofstream fout(outFile.c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); + fout << "event_idx,contingency,type,element,mva_or_vpu,rate_or_limit," + "loading_percent,base_mva,delta,severity\n"; + size_t rows = 0; + for (int p = 0; p < world.size(); p++) { + std::ostringstream oss; + oss << outputFile << "_violations." << p << ".part"; + std::string part = oss.str(); + std::ifstream fin(part.c_str(), std::ios::in | std::ios::binary); + if (!fin) continue; + while (fin) { + fin.read(&buf[0], BUFSZ); + std::streamsize got = fin.gcount(); + if (got > 0) { + fout.write(&buf[0], got); + for (std::streamsize k = 0; k < got; k++) { + if (buf[k] == '\n') rows++; + } + } + } + fin.close(); + std::remove(part.c_str()); + } + fout.close(); + printf("[violations] wrote %zu rows to %s\n", rows, outFile.c_str()); + } + // Aggregate skip count across ranks for diagnostics. if (outputFormat == "csv_delta") { long localSkip = static_cast(deltaSkipCount); @@ -1902,6 +2127,137 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } } + // Aggregate per-run summary across all ranks. Emit _summary.json + // on rank 0. Counters and worst-of values follow the same shape commercial + // tools use in their contingency reports. + { + long localCounters[4] = { 0 }; + // 0: total_ct 1: converged 2: cts_with_branch_viol 3: cts_with_voltage_viol + // (violation_rows is summed separately below) + if (!localContingencies.empty()) { + // json/csv paths: authoritative per-ct list. + for (size_t ci = 0; ci < localContingencies.size(); ci++) { + const gridpack::utility::ContingencyResult &ct = localContingencies[ci]; + localCounters[0] += 1; + if (ct.solution.convergence.converged) localCounters[1] += 1; + } + } else { + // csv_flat / csv_delta: no ct list, count from convergence rows. + localCounters[0] = static_cast(localConvRows.size()); + long conv = 0; + for (size_t i = 0; i < localConvRows.size(); i++) { + if (localConvRows[i].cs.converged) conv++; + } + localCounters[1] = conv; + } + localCounters[2] = static_cast(ctsWithBranchViol.size()); + localCounters[3] = static_cast(ctsWithVoltageViol.size()); + long totalCounters[4] = { 0 }; + for (int i = 0; i < 4; i++) totalCounters[i] = localCounters[i]; + world.sum(&totalCounters[0], 4); + long localViolRows = static_cast(violRowCount); + long totalViolRows = localViolRows; + world.sum(&totalViolRows, 1); + + struct WorstBranchWire { + double loading_pct; + int from, to; + char ckt[4]; + char ct_name[32]; + }; + struct WorstVoltageWire { + double v_pu; + double dev_pu; + int bus_id; + char ct_name[32]; + }; + WorstBranchWire wbLo; + wbLo.loading_pct = worstBr.loading_pct; + wbLo.from = worstBr.from; wbLo.to = worstBr.to; + std::strncpy(wbLo.ckt, worstBr.ckt.c_str(), 3); wbLo.ckt[3] = '\0'; + std::strncpy(wbLo.ct_name, worstBr.ct_name.c_str(), 31); wbLo.ct_name[31] = '\0'; + WorstVoltageWire wvLo; + wvLo.v_pu = worstVLo.v_pu; wvLo.dev_pu = worstVLo.dev_pu; + wvLo.bus_id = worstVLo.bus_id; + std::strncpy(wvLo.ct_name, worstVLo.ct_name.c_str(), 31); wvLo.ct_name[31] = '\0'; + WorstVoltageWire wvHi; + wvHi.v_pu = worstVHi.v_pu; wvHi.dev_pu = worstVHi.dev_pu; + wvHi.bus_id = worstVHi.bus_id; + std::strncpy(wvHi.ct_name, worstVHi.ct_name.c_str(), 31); wvHi.ct_name[31] = '\0'; + MPI_Comm mpi_comm = static_cast(world); + if (world.rank() == 0) { + for (int p = 1; p < world.size(); p++) { + WorstBranchWire otherB; + WorstVoltageWire otherLo, otherHi; + MPI_Recv(&otherB, sizeof(otherB), MPI_BYTE, p, 30, mpi_comm, MPI_STATUS_IGNORE); + MPI_Recv(&otherLo, sizeof(otherLo), MPI_BYTE, p, 31, mpi_comm, MPI_STATUS_IGNORE); + MPI_Recv(&otherHi, sizeof(otherHi), MPI_BYTE, p, 32, mpi_comm, MPI_STATUS_IGNORE); + if (otherB.loading_pct > wbLo.loading_pct) wbLo = otherB; + if (otherLo.dev_pu < wvLo.dev_pu) wvLo = otherLo; + if (otherHi.dev_pu > wvHi.dev_pu) wvHi = otherHi; + } + } else { + MPI_Send(&wbLo, sizeof(wbLo), MPI_BYTE, 0, 30, mpi_comm); + MPI_Send(&wvLo, sizeof(wvLo), MPI_BYTE, 0, 31, mpi_comm); + MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); + } + if (world.rank() == 0) { + std::string sumFile = outputFile + "_summary.json"; + std::ofstream sout(sumFile.c_str()); + sout << std::fixed; + sout << "{\n"; + sout << " \"total_contingencies\": " << totalCounters[0] << ",\n"; + sout << " \"converged\": " << totalCounters[1] << ",\n"; + sout << " \"diverged\": " << (totalCounters[0] - totalCounters[1]) << ",\n"; + sout << " \"with_branch_violation\": " << totalCounters[2] << ",\n"; + sout << " \"with_voltage_violation\": " << totalCounters[3] << ",\n"; + sout << " \"violation_rows\": " << totalViolRows << ",\n"; + sout << " \"contingency_rating\": \"" << contingencyRating << "\",\n"; + sout << " \"voltage_limit_low\": " << std::setprecision(4) << Vmin << ",\n"; + sout << " \"voltage_limit_high\": " << std::setprecision(4) << Vmax << ",\n"; + sout << " \"severity_threshold\": " << std::setprecision(4) + << violationSeverityThreshold << ",\n"; + sout << " \"worst_loading\": "; + if (wbLo.loading_pct > 0.0) { + sout << "{\"contingency\": \"" << wbLo.ct_name << "\"" + << ", \"from_bus\": " << wbLo.from + << ", \"to_bus\": " << wbLo.to + << ", \"circuit_id\": \"" << wbLo.ckt << "\"" + << ", \"loading_percent\": " << std::setprecision(2) << wbLo.loading_pct + << "},\n"; + } else { + sout << "null,\n"; + } + sout << " \"worst_voltage_low\": "; + if (wvLo.dev_pu < 0.0) { + sout << "{\"contingency\": \"" << wvLo.ct_name << "\"" + << ", \"bus_id\": " << wvLo.bus_id + << ", \"v_pu\": " << std::setprecision(6) << wvLo.v_pu + << ", \"deviation_pu\": " << std::setprecision(6) << wvLo.dev_pu + << "},\n"; + } else { + sout << "null,\n"; + } + sout << " \"worst_voltage_high\": "; + if (wvHi.dev_pu > 0.0) { + sout << "{\"contingency\": \"" << wvHi.ct_name << "\"" + << ", \"bus_id\": " << wvHi.bus_id + << ", \"v_pu\": " << std::setprecision(6) << wvHi.v_pu + << ", \"deviation_pu\": " << std::setprecision(6) << wvHi.dev_pu + << "}\n"; + } else { + sout << "null\n"; + } + sout << "}\n"; + sout.close(); + printf("[summary] wrote %s (%ld contingencies, %ld converged, " + "%ld with branch violations, %ld with voltage violations)\n", + sumFile.c_str(), + totalCounters[0], totalCounters[1], + totalCounters[2], totalCounters[3]); + } + } + // Print statistics from task manager describing the number of tasks performed // per processor taskmgr.printStats(); From 71bb31efecb777122c0ca066f9697344aeb1d399 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 10:48:30 -0700 Subject: [PATCH 21/31] Add PI ranking and contingency rosters to CA summary --- .../contingency_analysis/ca_driver.cpp | 187 +++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 3ab272cd..5d813a3d 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -451,6 +451,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double violationSeverityThreshold = 1.0; cursor->get("violationSeverityThreshold", &violationSeverityThreshold); if (violationSeverityThreshold <= 0.0) violationSeverityThreshold = 1.0; + // Cap on top_pi / top_voltage_severity and roster arrays in _summary.json. + int topN = 20; + cursor->get("topN", &topN); + if (topN < 1) topN = 1; + if (topN > 10000) topN = 10000; // Set static flag for PFBus class BEFORE network creation. // This controls how Q values are reported in output functions: @@ -621,6 +626,20 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) worstVHi.v_pu = -1e9; std::set ctsWithBranchViol; std::set ctsWithVoltageViol; + // Per-contingency composite indices. ctPi = sum(mva/rate)^2 across + // branches; ctVpi = sum(dev_pu)^2 across violated buses. Rank-local, + // reduced to world rank 0 in the summary block. + std::map ctPi; + std::map ctVpi; + auto accumBranchPi = [&](const std::string &ct_name, + double mva, double rate) { + if (rate <= 0.0) return; + double r = mva / rate; + ctPi[ct_name] += r * r; + }; + auto accumVoltagePi = [&](const std::string &ct_name, double dev_pu) { + ctVpi[ct_name] += dev_pu * dev_pu; + }; auto openViolPart = [&]() { if (violPart.is_open()) return; violPart.open(violPartPath.c_str(), std::ios::out | std::ios::trunc); @@ -690,6 +709,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) worstVHi.ct_name = ct_name; } ctsWithVoltageViol.insert(ct_name); + accumVoltagePi(ct_name, dev); }; // (from, to, ckt) key shared by the monitor allowlist and base_cache. @@ -954,6 +974,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } double flow_mva = std::sqrt(p*p + q*q); double loading_pct = (rate_sel > 0.0) ? (flow_mva / rate_sel) * 100.0 : 0.0; + if (!is_base) accumBranchPi(ct_name, flow_mva, rate_sel); // Stream to _violations.csv for csv_flat runs (contingency rows only). if (!is_base && loading_pct > violationSeverityThreshold * 100.0) { double base_mva = 0.0; @@ -1136,6 +1157,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) const BaseFlow &bf = it->second; double cont_mva = std::sqrt(p*p + q*q); double cont_loading = (bf.cont_rate > 0.0) ? (cont_mva / bf.cont_rate) * 100.0 : 0.0; + accumBranchPi(ct_name, cont_mva, bf.cont_rate); // Stream to _violations.csv (delta path knows base_mva already). if (cont_loading > violationSeverityThreshold * 100.0) { emitBranchViolation(event_idx, ct_name, from, to, k.ckt, @@ -1564,8 +1586,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) auto populateViolations = [&](gridpack::utility::ContingencyResult &ct, int event_idx) { const double threshPct = violationSeverityThreshold * 100.0; + // Gate PI on task_comm rank 0 so groupSize>1 doesn't double-count. + bool accumPi = (task_comm.rank() == 0); for (size_t bi = 0; bi < ct.solution.branches.size(); bi++) { const gridpack::utility::BranchResult &br = ct.solution.branches[bi]; + if (accumPi) { + double mva_br = (br.mvaFrom > br.mvaTo) ? br.mvaFrom : br.mvaTo; + accumBranchPi(ct.name, mva_br, br.rateSelected); + } if (br.loadingPercent <= threshPct) continue; gridpack::utility::BranchViolation v; v.fromBus = br.fromBus; @@ -2201,6 +2229,91 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) MPI_Send(&wvLo, sizeof(wvLo), MPI_BYTE, 0, 31, mpi_comm); MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); } + + // Gather per-contingency PI/VPI/rosters to rank 0. One line per name: + // \t\t\t\t\n + // Union locally over the four rank-local maps/sets first. + std::map aggPi; + std::map aggVpi; + std::set aggBranchViol; + std::set aggVoltageViol; + { + std::ostringstream localOut; + localOut << std::setprecision(10); + std::set names; + for (std::map::const_iterator it = ctPi.begin(); + it != ctPi.end(); ++it) names.insert(it->first); + for (std::map::const_iterator it = ctVpi.begin(); + it != ctVpi.end(); ++it) names.insert(it->first); + for (std::set::const_iterator it = ctsWithBranchViol.begin(); + it != ctsWithBranchViol.end(); ++it) names.insert(*it); + for (std::set::const_iterator it = ctsWithVoltageViol.begin(); + it != ctsWithVoltageViol.end(); ++it) names.insert(*it); + for (std::set::const_iterator it = names.begin(); + it != names.end(); ++it) { + const std::string &nm = *it; + double pi = 0.0, vpi = 0.0; + std::map::const_iterator itP = ctPi.find(nm); + if (itP != ctPi.end()) pi = itP->second; + std::map::const_iterator itV = ctVpi.find(nm); + if (itV != ctVpi.end()) vpi = itV->second; + int brFlag = ctsWithBranchViol.count(nm) ? 1 : 0; + int vFlag = ctsWithVoltageViol.count(nm) ? 1 : 0; + localOut << nm << '\t' << pi << '\t' << vpi << '\t' + << brFlag << '\t' << vFlag << '\n'; + } + std::string localBlob = localOut.str(); + // Rank 0 seeds aggregates from its own local blob, then absorbs + // one blob per remote rank. + auto absorb = [&](const std::string &blob) { + size_t pos = 0; + while (pos < blob.size()) { + size_t eol = blob.find('\n', pos); + if (eol == std::string::npos) break; + std::string line = blob.substr(pos, eol - pos); + pos = eol + 1; + size_t t1 = line.find('\t'); + size_t t2 = (t1 == std::string::npos) ? std::string::npos + : line.find('\t', t1 + 1); + size_t t3 = (t2 == std::string::npos) ? std::string::npos + : line.find('\t', t2 + 1); + size_t t4 = (t3 == std::string::npos) ? std::string::npos + : line.find('\t', t3 + 1); + if (t4 == std::string::npos) continue; + std::string nm = line.substr(0, t1); + double pi = std::atof(line.substr(t1 + 1, t2 - t1 - 1).c_str()); + double vpi = std::atof(line.substr(t2 + 1, t3 - t2 - 1).c_str()); + int brF = std::atoi(line.substr(t3 + 1, t4 - t3 - 1).c_str()); + int vF = std::atoi(line.substr(t4 + 1).c_str()); + if (pi != 0.0) aggPi[nm] += pi; + if (vpi != 0.0) aggVpi[nm] += vpi; + if (brF) aggBranchViol.insert(nm); + if (vF) aggVoltageViol.insert(nm); + } + }; + if (world.rank() == 0) { + absorb(localBlob); + for (int p = 1; p < world.size(); p++) { + int len = 0; + MPI_Recv(&len, 1, MPI_INT, p, 33, mpi_comm, MPI_STATUS_IGNORE); + std::string remote; + remote.resize(len); + if (len > 0) { + MPI_Recv(&remote[0], len, MPI_CHAR, p, 34, mpi_comm, + MPI_STATUS_IGNORE); + } + absorb(remote); + } + } else { + int len = static_cast(localBlob.size()); + MPI_Send(&len, 1, MPI_INT, 0, 33, mpi_comm); + if (len > 0) { + MPI_Send(const_cast(localBlob.c_str()), len, MPI_CHAR, 0, 34, + mpi_comm); + } + } + } + if (world.rank() == 0) { std::string sumFile = outputFile + "_summary.json"; std::ofstream sout(sumFile.c_str()); @@ -2244,9 +2357,79 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << ", \"bus_id\": " << wvHi.bus_id << ", \"v_pu\": " << std::setprecision(6) << wvHi.v_pu << ", \"deviation_pu\": " << std::setprecision(6) << wvHi.dev_pu - << "}\n"; + << "},\n"; } else { - sout << "null\n"; + sout << "null,\n"; + } + sout << " \"top_n\": " << topN << ",\n"; + auto emitNameArray = [&](const char *field, + const std::set &names) { + sout << " \"" << field << "\": ["; + int emitted = 0; + for (std::set::const_iterator it = names.begin(); + it != names.end() && emitted < topN; ++it, ++emitted) { + if (emitted) sout << ", "; + sout << "\"" << *it << "\""; + } + sout << "],\n"; + }; + emitNameArray("contingencies_with_branch_violation", aggBranchViol); + emitNameArray("contingencies_with_voltage_violation", aggVoltageViol); + // top_pi: contingencies sorted by composite PI (desc). + { + std::vector > byPi; + byPi.reserve(aggPi.size()); + for (std::map::const_iterator it = aggPi.begin(); + it != aggPi.end(); ++it) { + byPi.push_back(std::make_pair(it->second, it->first)); + } + std::sort(byPi.begin(), byPi.end(), + [](const std::pair &a, + const std::pair &b) { + if (a.first != b.first) return a.first > b.first; + return a.second < b.second; + }); + sout << " \"top_pi\": ["; + int emitted = 0; + for (size_t i = 0; i < byPi.size() && emitted < topN; ++i, ++emitted) { + if (emitted) sout << ",\n "; + else sout << "\n "; + sout << "{\"contingency\": \"" << byPi[i].second << "\"" + << ", \"pi\": " << std::setprecision(6) << byPi[i].first + << ", \"has_branch_violation\": " + << (aggBranchViol.count(byPi[i].second) ? "true" : "false") + << ", \"has_voltage_violation\": " + << (aggVoltageViol.count(byPi[i].second) ? "true" : "false") + << "}"; + } + sout << (emitted ? "\n ],\n" : "],\n"); + } + // top_voltage_severity: sorted by sum(dev_pu^2) desc. + { + std::vector > byV; + byV.reserve(aggVpi.size()); + for (std::map::const_iterator it = aggVpi.begin(); + it != aggVpi.end(); ++it) { + if (it->second > 0.0) + byV.push_back(std::make_pair(it->second, it->first)); + } + std::sort(byV.begin(), byV.end(), + [](const std::pair &a, + const std::pair &b) { + if (a.first != b.first) return a.first > b.first; + return a.second < b.second; + }); + sout << " \"top_voltage_severity\": ["; + int emitted = 0; + for (size_t i = 0; i < byV.size() && emitted < topN; ++i, ++emitted) { + if (emitted) sout << ",\n "; + else sout << "\n "; + sout << "{\"contingency\": \"" << byV[i].second << "\"" + << ", \"voltage_severity_index\": " + << std::setprecision(6) << byV[i].first + << "}"; + } + sout << (emitted ? "\n ]\n" : "]\n"); } sout << "}\n"; sout.close(); From ae4a3f466b3e6ad277cbee1785f0d1995d42ae95 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 11:21:02 -0700 Subject: [PATCH 22/31] Populate _violations.csv and _summary.json in text output mode --- .../contingency_analysis/ca_driver.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 5d813a3d..984f1fd5 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -1247,7 +1247,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Collect base case results for export. csv_flat captures rows directly // in the hot loop and skips the heavyweight collectResults() path. gridpack::utility::PowerFlowResults baseCaseResults; - if (outputFormat == "json" || outputFormat == "csv") { + if (outputFormat == "json" || outputFormat == "csv" || + outputFormat == "text") { baseCaseResults = pf_app.collectResults(); } if (outputFormat == "csv_flat") { @@ -1570,7 +1571,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Populated on rank 0 of each task_comm (only place collectResults ran). // Used to fill BranchViolation.baseMva/deltaMva during contingency reporting. std::map baseMvaByKey; - if (outputFormat == "json" || outputFormat == "csv") { + if (outputFormat == "json" || outputFormat == "csv" || + outputFormat == "text") { for (size_t bi = 0; bi < baseCaseResults.branches.size(); bi++) { const gridpack::utility::BranchResult &br = baseCaseResults.branches[bi]; BranchKey k; k.from = br.fromBus; k.to = br.toBus; k.ckt = br.circuitId; @@ -1758,8 +1760,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) bool ok1 = pf_app.checkVoltageViolations(); bool ok2 = pf_app.checkLineOverloadViolations(); bool ok = ok1 && ok2; - // Collect results for JSON/CSV export - if (outputFormat == "json" || outputFormat == "csv") { + // text mode runs the summary path but discards the per-ct struct. + if (outputFormat == "json" || outputFormat == "csv" || + outputFormat == "text") { gridpack::utility::ContingencyResult ctResult; ctResult.name = events[task_id].p_name; ctResult.type = (events[task_id].p_type == Branch) ? "branch" : "generator"; @@ -1767,7 +1770,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) ctResult.hasBranchViolation = !ok2; ctResult.solution = pf_app.collectResults(); populateViolations(ctResult, static_cast(task_id) + 1); - localContingencies.push_back(ctResult); + if (outputFormat != "text") localContingencies.push_back(ctResult); } if (outputFormat == "csv_flat") { captureFlatRows(task_id + 1, events[task_id].p_name, true, false); @@ -2170,11 +2173,13 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (ct.solution.convergence.converged) localCounters[1] += 1; } } else { - // csv_flat / csv_delta: no ct list, count from convergence rows. + // text / csv_flat / csv_delta: count from convergence rows. + // status=="OK" is the authoritative converged flag; cs.converged can be + // stale from a prior solve on ISLANDED/NO_SLACK/DIVERGED paths. localCounters[0] = static_cast(localConvRows.size()); long conv = 0; for (size_t i = 0; i < localConvRows.size(); i++) { - if (localConvRows[i].cs.converged) conv++; + if (localConvRows[i].status == "OK") conv++; } localCounters[1] = conv; } From 2785ee819d9b06f7f60e1cc755cde0b0b1d15a5d Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 11:29:41 -0700 Subject: [PATCH 23/31] Reject unknown outputFormat values instead of silently producing empty output --- .../contingency_analysis/ca_driver.cpp | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 984f1fd5..ce9f0df3 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -410,9 +410,20 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Check for Q limit violations (qlim: true=enabled, false=disabled) bool check_Qlim = cursor->get("qlim", true); double qlim_deadband = cursor->get("qlimDeadband", 0.1); - // Output format: "json", "csv", or "text" (default) + // Output format: "text" (default), "json", "csv", "csv_flat", "csv_delta". std::string outputFormat = "text"; cursor->get("outputFormat", &outputFormat); + if (outputFormat != "text" && outputFormat != "json" && + outputFormat != "csv" && outputFormat != "csv_flat" && + outputFormat != "csv_delta") { + if (world.rank() == 0) { + printf("ERROR: unrecognized outputFormat='%s'. " + "Must be one of: text, json, csv, csv_flat, csv_delta. Aborting.\n", + outputFormat.c_str()); + } + world.barrier(); + MPI_Abort(static_cast(world), 1); + } std::string outputFile = "ca_results"; cursor->get("outputFile", &outputFile); // Optional CSV allowlist (from_bus,to_bus,ckt). Empty -> emit all. @@ -626,11 +637,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) worstVHi.v_pu = -1e9; std::set ctsWithBranchViol; std::set ctsWithVoltageViol; - // Per-contingency composite indices. ctPi = sum(mva/rate)^2 across - // branches; ctVpi = sum(dev_pu)^2 across violated buses. Rank-local, - // reduced to world rank 0 in the summary block. - std::map ctPi; - std::map ctVpi; + // Rank-local per-ct composite indices; reduced to world 0 in summary. + std::map ctPi; // sum(mva/rate)^2 over branches + std::map ctVpi; // sum(dev_pu)^2 over violated buses auto accumBranchPi = [&](const std::string &ct_name, double mva, double rate) { if (rate <= 0.0) return; @@ -2173,9 +2182,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (ct.solution.convergence.converged) localCounters[1] += 1; } } else { - // text / csv_flat / csv_delta: count from convergence rows. - // status=="OK" is the authoritative converged flag; cs.converged can be - // stale from a prior solve on ISLANDED/NO_SLACK/DIVERGED paths. + // text/csv_flat/csv_delta: count from convergence rows. Use status + // rather than cs.converged, which can be stale on ISLANDED/DIVERGED. localCounters[0] = static_cast(localConvRows.size()); long conv = 0; for (size_t i = 0; i < localConvRows.size(); i++) { @@ -2235,9 +2243,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); } - // Gather per-contingency PI/VPI/rosters to rank 0. One line per name: - // \t\t\t\t\n - // Union locally over the four rank-local maps/sets first. + // Gather per-ct PI/VPI/rosters to rank 0 as one text blob per rank. + // Line: name\tpi\tvpi\tbr_flag\tv_flag\n std::map aggPi; std::map aggVpi; std::set aggBranchViol; @@ -2268,8 +2275,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << brFlag << '\t' << vFlag << '\n'; } std::string localBlob = localOut.str(); - // Rank 0 seeds aggregates from its own local blob, then absorbs - // one blob per remote rank. + // Rank 0 absorbs its own blob then each remote rank's. auto absorb = [&](const std::string &blob) { size_t pos = 0; while (pos < blob.size()) { From 93613075707b182df770c58f38a7985671eff6f6 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 14:43:18 -0700 Subject: [PATCH 24/31] Add composite PI ranking; textbook voltage PI over all buses --- .../contingency_analysis/ca_driver.cpp | 170 ++++++++++++++---- 1 file changed, 135 insertions(+), 35 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index ce9f0df3..a0b98de6 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -462,11 +462,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double violationSeverityThreshold = 1.0; cursor->get("violationSeverityThreshold", &violationSeverityThreshold); if (violationSeverityThreshold <= 0.0) violationSeverityThreshold = 1.0; - // Cap on top_pi / top_voltage_severity and roster arrays in _summary.json. + // Cap on top_*_pi and roster arrays in _summary.json. int topN = 20; cursor->get("topN", &topN); if (topN < 1) topN = 1; if (topN > 10000) topN = 10000; + // Weights for composite_pi = piBranchWeight*branch_pi + piVoltageWeight*voltage_pi. + double piBranchWeight = 1.0, piVoltageWeight = 1.0; + cursor->get("piBranchWeight", &piBranchWeight); + cursor->get("piVoltageWeight", &piVoltageWeight); + if (piBranchWeight < 0.0) piBranchWeight = 0.0; + if (piVoltageWeight < 0.0) piVoltageWeight = 0.0; // Set static flag for PFBus class BEFORE network creation. // This controls how Q values are reported in output functions: @@ -638,16 +644,34 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::set ctsWithBranchViol; std::set ctsWithVoltageViol; // Rank-local per-ct composite indices; reduced to world 0 in summary. - std::map ctPi; // sum(mva/rate)^2 over branches - std::map ctVpi; // sum(dev_pu)^2 over violated buses + // ctPi = sum(mva/rate)^2 over all monitored branches + // ctVpi = sum((v-1)/dv)^2 over all energized buses (textbook voltage PI, + // direction-aware denominator so v=Vmin and v=Vmax each score 1) + // ctVdev = sum(v-limit)^2 over violated buses only (legacy "depth" metric, + // kept for backward compatibility as voltage_deviation_index) + std::map ctPi; + std::map ctVpi; + std::map ctVdev; auto accumBranchPi = [&](const std::string &ct_name, double mva, double rate) { if (rate <= 0.0) return; double r = mva / rate; ctPi[ct_name] += r * r; }; - auto accumVoltagePi = [&](const std::string &ct_name, double dev_pu) { - ctVpi[ct_name] += dev_pu * dev_pu; + // Textbook voltage PI: normalize by the direction-aware half-band so a bus + // at Vmin or Vmax contributes 1.0 even when limits are asymmetric. + // Skip isolated (v<=0) and NaN/inf buses so numerical failures on a single + // bus don't poison the accumulator. + auto accumVoltagePi = [&](const std::string &ct_name, double v_pu) { + if (v_pu <= 0.0 || !std::isfinite(v_pu)) return; + double denom = (v_pu < 1.0) ? (1.0 - Vmin) : (Vmax - 1.0); + if (denom <= 0.0) return; + double r = (v_pu - 1.0) / denom; + ctVpi[ct_name] += r * r; + }; + // Legacy deviation metric: (v-limit)^2 accrued only for violated buses. + auto accumVoltageDev = [&](const std::string &ct_name, double dev_pu) { + ctVdev[ct_name] += dev_pu * dev_pu; }; auto openViolPart = [&]() { if (violPart.is_open()) return; @@ -718,7 +742,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) worstVHi.ct_name = ct_name; } ctsWithVoltageViol.insert(ct_name); - accumVoltagePi(ct_name, dev); + accumVoltageDev(ct_name, dev); }; // (from, to, ckt) key shared by the monitor allowlist and base_cache. @@ -950,6 +974,13 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) char ct_name[24]; std::strncpy(ct_name, name.c_str(), sizeof(ct_name) - 1); ct_name[sizeof(ct_name) - 1] = '\0'; + // Accrue voltage PI on every energized bus in contingency rows only. + if (!is_base) { + for (std::map >::const_iterator vit = + vbymag_ang.begin(); vit != vbymag_ang.end(); ++vit) { + accumVoltagePi(ct_name, vit->second.first); + } + } for (size_t bi = 0; bi < b_strs.size(); bi++) { char ckt_buf[16] = {0}; int viol = 0; @@ -1145,6 +1176,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) vbymag_ang[bus_id] = std::make_pair(vmag, angle); } } + // Accrue voltage PI on every energized bus in this contingency. + for (std::map >::const_iterator vit = + vbymag_ang.begin(); vit != vbymag_ang.end(); ++vit) { + accumVoltagePi(ct_name, vit->second.first); + } for (size_t bi = 0; bi < b_strs.size(); bi++) { char ckt_buf[16] = {0}; int viol = 0; @@ -1627,6 +1663,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) const gridpack::utility::BusResult &b = ct.solution.buses[bi]; double v_pu = b.voltage; if (v_pu <= 0.0) continue; // Skip isolated / not-solved buses. + // Voltage PI accrues on every energized bus (textbook form), + // gated on task_comm rank 0 to avoid double-count under groupSize>1. + if (accumPi) accumVoltagePi(ct.name, v_pu); bool lo = v_pu < Vmin, hi = v_pu > Vmax; if (!lo && !hi) continue; gridpack::utility::VoltageViolation vv; @@ -2243,10 +2282,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); } - // Gather per-ct PI/VPI/rosters to rank 0 as one text blob per rank. - // Line: name\tpi\tvpi\tbr_flag\tv_flag\n + // Gather per-ct PI/VPI/Vdev/rosters to rank 0 as one text blob per rank. + // Line: name\tbranch_pi\tvoltage_pi\tvoltage_dev\tbr_flag\tv_flag\n std::map aggPi; std::map aggVpi; + std::map aggVdev; std::set aggBranchViol; std::set aggVoltageViol; { @@ -2257,6 +2297,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) it != ctPi.end(); ++it) names.insert(it->first); for (std::map::const_iterator it = ctVpi.begin(); it != ctVpi.end(); ++it) names.insert(it->first); + for (std::map::const_iterator it = ctVdev.begin(); + it != ctVdev.end(); ++it) names.insert(it->first); for (std::set::const_iterator it = ctsWithBranchViol.begin(); it != ctsWithBranchViol.end(); ++it) names.insert(*it); for (std::set::const_iterator it = ctsWithVoltageViol.begin(); @@ -2264,14 +2306,16 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) for (std::set::const_iterator it = names.begin(); it != names.end(); ++it) { const std::string &nm = *it; - double pi = 0.0, vpi = 0.0; + double pi = 0.0, vpi = 0.0, vdev = 0.0; std::map::const_iterator itP = ctPi.find(nm); if (itP != ctPi.end()) pi = itP->second; std::map::const_iterator itV = ctVpi.find(nm); if (itV != ctVpi.end()) vpi = itV->second; + std::map::const_iterator itD = ctVdev.find(nm); + if (itD != ctVdev.end()) vdev = itD->second; int brFlag = ctsWithBranchViol.count(nm) ? 1 : 0; int vFlag = ctsWithVoltageViol.count(nm) ? 1 : 0; - localOut << nm << '\t' << pi << '\t' << vpi << '\t' + localOut << nm << '\t' << pi << '\t' << vpi << '\t' << vdev << '\t' << brFlag << '\t' << vFlag << '\n'; } std::string localBlob = localOut.str(); @@ -2290,14 +2334,18 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) : line.find('\t', t2 + 1); size_t t4 = (t3 == std::string::npos) ? std::string::npos : line.find('\t', t3 + 1); - if (t4 == std::string::npos) continue; + size_t t5 = (t4 == std::string::npos) ? std::string::npos + : line.find('\t', t4 + 1); + if (t5 == std::string::npos) continue; std::string nm = line.substr(0, t1); - double pi = std::atof(line.substr(t1 + 1, t2 - t1 - 1).c_str()); - double vpi = std::atof(line.substr(t2 + 1, t3 - t2 - 1).c_str()); - int brF = std::atoi(line.substr(t3 + 1, t4 - t3 - 1).c_str()); - int vF = std::atoi(line.substr(t4 + 1).c_str()); - if (pi != 0.0) aggPi[nm] += pi; - if (vpi != 0.0) aggVpi[nm] += vpi; + double pi = std::atof(line.substr(t1 + 1, t2 - t1 - 1).c_str()); + double vpi = std::atof(line.substr(t2 + 1, t3 - t2 - 1).c_str()); + double vdev = std::atof(line.substr(t3 + 1, t4 - t3 - 1).c_str()); + int brF = std::atoi(line.substr(t4 + 1, t5 - t4 - 1).c_str()); + int vF = std::atoi(line.substr(t5 + 1).c_str()); + if (pi != 0.0) aggPi[nm] += pi; + if (vpi != 0.0) aggVpi[nm] += vpi; + if (vdev != 0.0) aggVdev[nm] += vdev; if (brF) aggBranchViol.insert(nm); if (vF) aggVoltageViol.insert(nm); } @@ -2386,7 +2434,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) }; emitNameArray("contingencies_with_branch_violation", aggBranchViol); emitNameArray("contingencies_with_voltage_violation", aggVoltageViol); - // top_pi: contingencies sorted by composite PI (desc). + sout << " \"pi_branch_weight\": " << std::setprecision(4) << piBranchWeight << ",\n"; + sout << " \"pi_voltage_weight\": " << std::setprecision(4) << piVoltageWeight << ",\n"; + auto piCmp = [](const std::pair &a, + const std::pair &b) { + if (a.first != b.first) return a.first > b.first; + return a.second < b.second; + }; + // top_branch_pi: sum((mva/rate)^2) over monitored branches. { std::vector > byPi; byPi.reserve(aggPi.size()); @@ -2394,19 +2449,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) it != aggPi.end(); ++it) { byPi.push_back(std::make_pair(it->second, it->first)); } - std::sort(byPi.begin(), byPi.end(), - [](const std::pair &a, - const std::pair &b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; - }); - sout << " \"top_pi\": ["; + std::sort(byPi.begin(), byPi.end(), piCmp); + sout << " \"top_branch_pi\": ["; int emitted = 0; for (size_t i = 0; i < byPi.size() && emitted < topN; ++i, ++emitted) { if (emitted) sout << ",\n "; else sout << "\n "; sout << "{\"contingency\": \"" << byPi[i].second << "\"" - << ", \"pi\": " << std::setprecision(6) << byPi[i].first + << ", \"branch_pi\": " << std::setprecision(6) << byPi[i].first << ", \"has_branch_violation\": " << (aggBranchViol.count(byPi[i].second) ? "true" : "false") << ", \"has_voltage_violation\": " @@ -2415,7 +2465,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } sout << (emitted ? "\n ],\n" : "],\n"); } - // top_voltage_severity: sorted by sum(dev_pu^2) desc. + // top_voltage_pi: textbook voltage PI over all energized buses. + // Each entry also carries voltage_deviation_index = sum((v-limit)^2) + // over violated buses, the legacy metric. { std::vector > byV; byV.reserve(aggVpi.size()); @@ -2424,20 +2476,68 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (it->second > 0.0) byV.push_back(std::make_pair(it->second, it->first)); } - std::sort(byV.begin(), byV.end(), - [](const std::pair &a, - const std::pair &b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; - }); - sout << " \"top_voltage_severity\": ["; + std::sort(byV.begin(), byV.end(), piCmp); + sout << " \"top_voltage_pi\": ["; int emitted = 0; for (size_t i = 0; i < byV.size() && emitted < topN; ++i, ++emitted) { if (emitted) sout << ",\n "; else sout << "\n "; + double vdev = 0.0; + std::map::const_iterator itD = + aggVdev.find(byV[i].second); + if (itD != aggVdev.end()) vdev = itD->second; sout << "{\"contingency\": \"" << byV[i].second << "\"" - << ", \"voltage_severity_index\": " + << ", \"voltage_pi\": " << std::setprecision(6) << byV[i].first + << ", \"voltage_deviation_index\": " + << std::setprecision(6) << vdev + << ", \"has_voltage_violation\": " + << (aggVoltageViol.count(byV[i].second) ? "true" : "false") + << "}"; + } + sout << (emitted ? "\n ],\n" : "],\n"); + } + // top_composite_pi: piBranchWeight*branch_pi + piVoltageWeight*voltage_pi. + { + std::set ctSet; + for (std::map::const_iterator it = aggPi.begin(); + it != aggPi.end(); ++it) ctSet.insert(it->first); + for (std::map::const_iterator it = aggVpi.begin(); + it != aggVpi.end(); ++it) ctSet.insert(it->first); + std::vector > byC; + byC.reserve(ctSet.size()); + for (std::set::const_iterator it = ctSet.begin(); + it != ctSet.end(); ++it) { + double bp = 0.0, vp = 0.0; + std::map::const_iterator itP = aggPi.find(*it); + if (itP != aggPi.end()) bp = itP->second; + std::map::const_iterator itV = aggVpi.find(*it); + if (itV != aggVpi.end()) vp = itV->second; + double cpi = piBranchWeight * bp + piVoltageWeight * vp; + if (cpi > 0.0) byC.push_back(std::make_pair(cpi, *it)); + } + std::sort(byC.begin(), byC.end(), piCmp); + sout << " \"top_composite_pi\": ["; + int emitted = 0; + for (size_t i = 0; i < byC.size() && emitted < topN; ++i, ++emitted) { + if (emitted) sout << ",\n "; + else sout << "\n "; + double bp = 0.0, vp = 0.0; + std::map::const_iterator itP = aggPi.find(byC[i].second); + if (itP != aggPi.end()) bp = itP->second; + std::map::const_iterator itV = aggVpi.find(byC[i].second); + if (itV != aggVpi.end()) vp = itV->second; + sout << "{\"contingency\": \"" << byC[i].second << "\"" + << ", \"composite_pi\": " + << std::setprecision(6) << byC[i].first + << ", \"branch_pi\": " + << std::setprecision(6) << bp + << ", \"voltage_pi\": " + << std::setprecision(6) << vp + << ", \"has_branch_violation\": " + << (aggBranchViol.count(byC[i].second) ? "true" : "false") + << ", \"has_voltage_violation\": " + << (aggVoltageViol.count(byC[i].second) ? "true" : "false") << "}"; } sout << (emitted ? "\n ]\n" : "]\n"); From b92affa1628da35db7fae980fd200b1f28ca3e42 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 15:44:28 -0700 Subject: [PATCH 25/31] Add top_severe_contingencies with violated-first hierarchy --- .../contingency_analysis/ca_driver.cpp | 200 +++++++++++++++--- 1 file changed, 166 insertions(+), 34 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index a0b98de6..085b417f 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -644,14 +644,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::set ctsWithBranchViol; std::set ctsWithVoltageViol; // Rank-local per-ct composite indices; reduced to world 0 in summary. - // ctPi = sum(mva/rate)^2 over all monitored branches - // ctVpi = sum((v-1)/dv)^2 over all energized buses (textbook voltage PI, - // direction-aware denominator so v=Vmin and v=Vmax each score 1) - // ctVdev = sum(v-limit)^2 over violated buses only (legacy "depth" metric, - // kept for backward compatibility as voltage_deviation_index) - std::map ctPi; - std::map ctVpi; - std::map ctVdev; + std::map ctPi; // sum(mva/rate)^2, all monitored branches + std::map ctVpi; // sum((v-1)/dv)^2, all energized buses + std::map ctVdev; // sum(v-limit)^2, violated buses only + std::map ctWorstLoading; // max loading_pct among violated branches + std::map ctWorstVdevLo; // most negative (v-Vmin), 0 if none + std::map ctWorstVdevHi; // most positive (v-Vmax), 0 if none + std::map ctWorstVpuLo; // v_pu that produced ctWorstVdevLo + std::map ctWorstVpuHi; // v_pu that produced ctWorstVdevHi auto accumBranchPi = [&](const std::string &ct_name, double mva, double rate) { if (rate <= 0.0) return; @@ -707,6 +707,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) worstBr.ct_name = ct_name; } ctsWithBranchViol.insert(ct_name); + double &pc = ctWorstLoading[ct_name]; + if (loading_pct > pc) pc = loading_pct; }; auto emitVoltageViolation = [&](int event_idx, const std::string &ct_name, int bus_id, double v_pu, @@ -743,6 +745,13 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } ctsWithVoltageViol.insert(ct_name); accumVoltageDev(ct_name, dev); + if (dev < 0.0) { + double &d = ctWorstVdevLo[ct_name]; + if (dev < d) { d = dev; ctWorstVpuLo[ct_name] = v_pu; } + } else { + double &d = ctWorstVdevHi[ct_name]; + if (dev > d) { d = dev; ctWorstVpuHi[ct_name] = v_pu; } + } }; // (from, to, ckt) key shared by the monitor allowlist and base_cache. @@ -2282,11 +2291,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); } - // Gather per-ct PI/VPI/Vdev/rosters to rank 0 as one text blob per rank. - // Line: name\tbranch_pi\tvoltage_pi\tvoltage_dev\tbr_flag\tv_flag\n + // Gather per-ct PI/VPI/Vdev/rosters + per-ct worst-single-element data to + // rank 0 as one text blob per rank. Line has 10 tab-separated fields: + // name pi vpi vdev worstLoad worstVdevLo worstVpuLo worstVdevHi worstVpuHi br_flag v_flag std::map aggPi; std::map aggVpi; std::map aggVdev; + std::map aggWorstLoading; + std::map aggWorstVdevLo; + std::map aggWorstVpuLo; + std::map aggWorstVdevHi; + std::map aggWorstVpuHi; std::set aggBranchViol; std::set aggVoltageViol; { @@ -2303,49 +2318,71 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) it != ctsWithBranchViol.end(); ++it) names.insert(*it); for (std::set::const_iterator it = ctsWithVoltageViol.begin(); it != ctsWithVoltageViol.end(); ++it) names.insert(*it); + auto lookup = [](const std::map &m, + const std::string &k) -> double { + std::map::const_iterator it = m.find(k); + return (it == m.end()) ? 0.0 : it->second; + }; for (std::set::const_iterator it = names.begin(); it != names.end(); ++it) { const std::string &nm = *it; - double pi = 0.0, vpi = 0.0, vdev = 0.0; - std::map::const_iterator itP = ctPi.find(nm); - if (itP != ctPi.end()) pi = itP->second; - std::map::const_iterator itV = ctVpi.find(nm); - if (itV != ctVpi.end()) vpi = itV->second; - std::map::const_iterator itD = ctVdev.find(nm); - if (itD != ctVdev.end()) vdev = itD->second; + double pi = lookup(ctPi, nm); + double vpi = lookup(ctVpi, nm); + double vdev = lookup(ctVdev, nm); + double wL = lookup(ctWorstLoading, nm); + double wDLo = lookup(ctWorstVdevLo, nm); + double wVLo = lookup(ctWorstVpuLo, nm); + double wDHi = lookup(ctWorstVdevHi, nm); + double wVHi = lookup(ctWorstVpuHi, nm); int brFlag = ctsWithBranchViol.count(nm) ? 1 : 0; int vFlag = ctsWithVoltageViol.count(nm) ? 1 : 0; localOut << nm << '\t' << pi << '\t' << vpi << '\t' << vdev << '\t' + << wL << '\t' << wDLo << '\t' << wVLo << '\t' + << wDHi << '\t' << wVHi << '\t' << brFlag << '\t' << vFlag << '\n'; } std::string localBlob = localOut.str(); - // Rank 0 absorbs its own blob then each remote rank's. + auto splitTabs = [](const std::string &line, + std::vector &out) { + out.clear(); + size_t pos = 0; + while (pos <= line.size()) { + size_t t = line.find('\t', pos); + if (t == std::string::npos) { + out.push_back(line.substr(pos)); + break; + } + out.push_back(line.substr(pos, t - pos)); + pos = t + 1; + } + }; auto absorb = [&](const std::string &blob) { size_t pos = 0; + std::vector fields; while (pos < blob.size()) { size_t eol = blob.find('\n', pos); if (eol == std::string::npos) break; std::string line = blob.substr(pos, eol - pos); pos = eol + 1; - size_t t1 = line.find('\t'); - size_t t2 = (t1 == std::string::npos) ? std::string::npos - : line.find('\t', t1 + 1); - size_t t3 = (t2 == std::string::npos) ? std::string::npos - : line.find('\t', t2 + 1); - size_t t4 = (t3 == std::string::npos) ? std::string::npos - : line.find('\t', t3 + 1); - size_t t5 = (t4 == std::string::npos) ? std::string::npos - : line.find('\t', t4 + 1); - if (t5 == std::string::npos) continue; - std::string nm = line.substr(0, t1); - double pi = std::atof(line.substr(t1 + 1, t2 - t1 - 1).c_str()); - double vpi = std::atof(line.substr(t2 + 1, t3 - t2 - 1).c_str()); - double vdev = std::atof(line.substr(t3 + 1, t4 - t3 - 1).c_str()); - int brF = std::atoi(line.substr(t4 + 1, t5 - t4 - 1).c_str()); - int vF = std::atoi(line.substr(t5 + 1).c_str()); + splitTabs(line, fields); + if (fields.size() < 11) continue; + const std::string &nm = fields[0]; + double pi = std::atof(fields[1].c_str()); + double vpi = std::atof(fields[2].c_str()); + double vdev = std::atof(fields[3].c_str()); + double wL = std::atof(fields[4].c_str()); + double wDLo = std::atof(fields[5].c_str()); + double wVLo = std::atof(fields[6].c_str()); + double wDHi = std::atof(fields[7].c_str()); + double wVHi = std::atof(fields[8].c_str()); + int brF = std::atoi(fields[9].c_str()); + int vF = std::atoi(fields[10].c_str()); if (pi != 0.0) aggPi[nm] += pi; if (vpi != 0.0) aggVpi[nm] += vpi; if (vdev != 0.0) aggVdev[nm] += vdev; + if (wL > aggWorstLoading[nm]) aggWorstLoading[nm] = wL; + if (wDLo < aggWorstVdevLo[nm]) { aggWorstVdevLo[nm] = wDLo; aggWorstVpuLo[nm] = wVLo; } + if (wDHi > aggWorstVdevHi[nm]) { aggWorstVdevHi[nm] = wDHi; aggWorstVpuHi[nm] = wVHi; } if (brF) aggBranchViol.insert(nm); if (vF) aggVoltageViol.insert(nm); } @@ -2540,6 +2577,101 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << (aggVoltageViol.count(byC[i].second) ? "true" : "false") << "}"; } + sout << (emitted ? "\n ],\n" : "],\n"); + } + // top_severe_contingencies: Group A (any violation) first, sorted by + // worst-single-element severity; Group B (no violations) after, + // sorted by composite_pi. Combined list capped at topN. + { + std::set ctSet; + for (std::map::const_iterator it = aggPi.begin(); + it != aggPi.end(); ++it) ctSet.insert(it->first); + for (std::map::const_iterator it = aggVpi.begin(); + it != aggVpi.end(); ++it) ctSet.insert(it->first); + for (std::set::const_iterator it = aggBranchViol.begin(); + it != aggBranchViol.end(); ++it) ctSet.insert(*it); + for (std::set::const_iterator it = aggVoltageViol.begin(); + it != aggVoltageViol.end(); ++it) ctSet.insert(*it); + auto agg_get = [](const std::map &m, + const std::string &k) -> double { + std::map::const_iterator it = m.find(k); + return (it == m.end()) ? 0.0 : it->second; + }; + struct SevRow { + std::string name; + bool violated; + double sortKey; // Group A: worst-single severity; Group B: composite_pi + double composite, branchPi, voltagePi; + double worstLoading; + double worstVpuLo, worstVdevLo; + double worstVpuHi, worstVdevHi; + }; + std::vector groupA, groupB; + for (std::set::const_iterator it = ctSet.begin(); + it != ctSet.end(); ++it) { + SevRow r; + r.name = *it; + r.branchPi = agg_get(aggPi, *it); + r.voltagePi = agg_get(aggVpi, *it); + r.composite = piBranchWeight * r.branchPi + piVoltageWeight * r.voltagePi; + r.worstLoading = agg_get(aggWorstLoading, *it); + r.worstVdevLo = agg_get(aggWorstVdevLo, *it); + r.worstVpuLo = agg_get(aggWorstVpuLo, *it); + r.worstVdevHi = agg_get(aggWorstVdevHi, *it); + r.worstVpuHi = agg_get(aggWorstVpuHi, *it); + bool hasBr = aggBranchViol.count(*it) > 0; + bool hasV = aggVoltageViol.count(*it) > 0; + r.violated = hasBr || hasV; + if (r.violated) { + double s = 0.0; + if (r.worstLoading > 100.0) s = std::max(s, r.worstLoading - 100.0); + if (r.worstVdevLo < 0.0) s = std::max(s, std::abs(r.worstVdevLo) * 1000.0); + if (r.worstVdevHi > 0.0) s = std::max(s, r.worstVdevHi * 1000.0); + r.sortKey = s; + groupA.push_back(r); + } else { + r.sortKey = r.composite; + if (r.sortKey > 0.0) groupB.push_back(r); + } + } + auto sevCmp = [](const SevRow &a, const SevRow &b) { + if (a.sortKey != b.sortKey) return a.sortKey > b.sortKey; + return a.name < b.name; + }; + std::sort(groupA.begin(), groupA.end(), sevCmp); + std::sort(groupB.begin(), groupB.end(), sevCmp); + auto emitRow = [&](const SevRow &r, bool first) { + if (!first) sout << ",\n "; + else sout << "\n "; + sout << "{\"contingency\": \"" << r.name << "\"" + << ", \"group\": \"" << (r.violated ? "violated" : "stressed") << "\"" + << ", \"composite_pi\": " << std::setprecision(6) << r.composite + << ", \"branch_pi\": " << std::setprecision(6) << r.branchPi + << ", \"voltage_pi\": " << std::setprecision(6) << r.voltagePi + << ", \"worst_branch_loading_percent\": " + << std::setprecision(2) << r.worstLoading + << ", \"worst_voltage_pu_low\": " + << std::setprecision(6) << r.worstVpuLo + << ", \"worst_voltage_deviation_pu_low\": " + << std::setprecision(6) << r.worstVdevLo + << ", \"worst_voltage_pu_high\": " + << std::setprecision(6) << r.worstVpuHi + << ", \"worst_voltage_deviation_pu_high\": " + << std::setprecision(6) << r.worstVdevHi + << ", \"has_branch_violation\": " + << (aggBranchViol.count(r.name) ? "true" : "false") + << ", \"has_voltage_violation\": " + << (aggVoltageViol.count(r.name) ? "true" : "false") + << "}"; + }; + sout << " \"top_severe_contingencies\": ["; + int emitted = 0; + for (size_t i = 0; i < groupA.size() && emitted < topN; ++i, ++emitted) { + emitRow(groupA[i], emitted == 0); + } + for (size_t i = 0; i < groupB.size() && emitted < topN; ++i, ++emitted) { + emitRow(groupB[i], emitted == 0); + } sout << (emitted ? "\n ]\n" : "]\n"); } sout << "}\n"; From e4d643e1e268b4b5f7a6ca3591f1e6e3328ec1a1 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 16:07:52 -0700 Subject: [PATCH 26/31] Slim _summary.json: drop derived fields, use null for unset worst_* --- .../contingency_analysis/ca_driver.cpp | 156 +++--------------- 1 file changed, 23 insertions(+), 133 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 085b417f..b8cfd691 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -462,8 +462,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double violationSeverityThreshold = 1.0; cursor->get("violationSeverityThreshold", &violationSeverityThreshold); if (violationSeverityThreshold <= 0.0) violationSeverityThreshold = 1.0; - // Cap on top_*_pi and roster arrays in _summary.json. - int topN = 20; + // Cap on top_severe_contingencies and roster arrays in _summary.json. + int topN = 10; cursor->get("topN", &topN); if (topN < 1) topN = 1; if (topN > 10000) topN = 10000; @@ -2420,12 +2420,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) sout << " \"diverged\": " << (totalCounters[0] - totalCounters[1]) << ",\n"; sout << " \"with_branch_violation\": " << totalCounters[2] << ",\n"; sout << " \"with_voltage_violation\": " << totalCounters[3] << ",\n"; - sout << " \"violation_rows\": " << totalViolRows << ",\n"; - sout << " \"contingency_rating\": \"" << contingencyRating << "\",\n"; - sout << " \"voltage_limit_low\": " << std::setprecision(4) << Vmin << ",\n"; - sout << " \"voltage_limit_high\": " << std::setprecision(4) << Vmax << ",\n"; - sout << " \"severity_threshold\": " << std::setprecision(4) - << violationSeverityThreshold << ",\n"; sout << " \"worst_loading\": "; if (wbLo.loading_pct > 0.0) { sout << "{\"contingency\": \"" << wbLo.ct_name << "\"" @@ -2457,13 +2451,12 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } else { sout << "null,\n"; } - sout << " \"top_n\": " << topN << ",\n"; auto emitNameArray = [&](const char *field, const std::set &names) { sout << " \"" << field << "\": ["; int emitted = 0; for (std::set::const_iterator it = names.begin(); - it != names.end() && emitted < topN; ++it, ++emitted) { + it != names.end(); ++it, ++emitted) { if (emitted) sout << ", "; sout << "\"" << *it << "\""; } @@ -2471,114 +2464,6 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) }; emitNameArray("contingencies_with_branch_violation", aggBranchViol); emitNameArray("contingencies_with_voltage_violation", aggVoltageViol); - sout << " \"pi_branch_weight\": " << std::setprecision(4) << piBranchWeight << ",\n"; - sout << " \"pi_voltage_weight\": " << std::setprecision(4) << piVoltageWeight << ",\n"; - auto piCmp = [](const std::pair &a, - const std::pair &b) { - if (a.first != b.first) return a.first > b.first; - return a.second < b.second; - }; - // top_branch_pi: sum((mva/rate)^2) over monitored branches. - { - std::vector > byPi; - byPi.reserve(aggPi.size()); - for (std::map::const_iterator it = aggPi.begin(); - it != aggPi.end(); ++it) { - byPi.push_back(std::make_pair(it->second, it->first)); - } - std::sort(byPi.begin(), byPi.end(), piCmp); - sout << " \"top_branch_pi\": ["; - int emitted = 0; - for (size_t i = 0; i < byPi.size() && emitted < topN; ++i, ++emitted) { - if (emitted) sout << ",\n "; - else sout << "\n "; - sout << "{\"contingency\": \"" << byPi[i].second << "\"" - << ", \"branch_pi\": " << std::setprecision(6) << byPi[i].first - << ", \"has_branch_violation\": " - << (aggBranchViol.count(byPi[i].second) ? "true" : "false") - << ", \"has_voltage_violation\": " - << (aggVoltageViol.count(byPi[i].second) ? "true" : "false") - << "}"; - } - sout << (emitted ? "\n ],\n" : "],\n"); - } - // top_voltage_pi: textbook voltage PI over all energized buses. - // Each entry also carries voltage_deviation_index = sum((v-limit)^2) - // over violated buses, the legacy metric. - { - std::vector > byV; - byV.reserve(aggVpi.size()); - for (std::map::const_iterator it = aggVpi.begin(); - it != aggVpi.end(); ++it) { - if (it->second > 0.0) - byV.push_back(std::make_pair(it->second, it->first)); - } - std::sort(byV.begin(), byV.end(), piCmp); - sout << " \"top_voltage_pi\": ["; - int emitted = 0; - for (size_t i = 0; i < byV.size() && emitted < topN; ++i, ++emitted) { - if (emitted) sout << ",\n "; - else sout << "\n "; - double vdev = 0.0; - std::map::const_iterator itD = - aggVdev.find(byV[i].second); - if (itD != aggVdev.end()) vdev = itD->second; - sout << "{\"contingency\": \"" << byV[i].second << "\"" - << ", \"voltage_pi\": " - << std::setprecision(6) << byV[i].first - << ", \"voltage_deviation_index\": " - << std::setprecision(6) << vdev - << ", \"has_voltage_violation\": " - << (aggVoltageViol.count(byV[i].second) ? "true" : "false") - << "}"; - } - sout << (emitted ? "\n ],\n" : "],\n"); - } - // top_composite_pi: piBranchWeight*branch_pi + piVoltageWeight*voltage_pi. - { - std::set ctSet; - for (std::map::const_iterator it = aggPi.begin(); - it != aggPi.end(); ++it) ctSet.insert(it->first); - for (std::map::const_iterator it = aggVpi.begin(); - it != aggVpi.end(); ++it) ctSet.insert(it->first); - std::vector > byC; - byC.reserve(ctSet.size()); - for (std::set::const_iterator it = ctSet.begin(); - it != ctSet.end(); ++it) { - double bp = 0.0, vp = 0.0; - std::map::const_iterator itP = aggPi.find(*it); - if (itP != aggPi.end()) bp = itP->second; - std::map::const_iterator itV = aggVpi.find(*it); - if (itV != aggVpi.end()) vp = itV->second; - double cpi = piBranchWeight * bp + piVoltageWeight * vp; - if (cpi > 0.0) byC.push_back(std::make_pair(cpi, *it)); - } - std::sort(byC.begin(), byC.end(), piCmp); - sout << " \"top_composite_pi\": ["; - int emitted = 0; - for (size_t i = 0; i < byC.size() && emitted < topN; ++i, ++emitted) { - if (emitted) sout << ",\n "; - else sout << "\n "; - double bp = 0.0, vp = 0.0; - std::map::const_iterator itP = aggPi.find(byC[i].second); - if (itP != aggPi.end()) bp = itP->second; - std::map::const_iterator itV = aggVpi.find(byC[i].second); - if (itV != aggVpi.end()) vp = itV->second; - sout << "{\"contingency\": \"" << byC[i].second << "\"" - << ", \"composite_pi\": " - << std::setprecision(6) << byC[i].first - << ", \"branch_pi\": " - << std::setprecision(6) << bp - << ", \"voltage_pi\": " - << std::setprecision(6) << vp - << ", \"has_branch_violation\": " - << (aggBranchViol.count(byC[i].second) ? "true" : "false") - << ", \"has_voltage_violation\": " - << (aggVoltageViol.count(byC[i].second) ? "true" : "false") - << "}"; - } - sout << (emitted ? "\n ],\n" : "],\n"); - } // top_severe_contingencies: Group A (any violation) first, sorted by // worst-single-element severity; Group B (no violations) after, // sorted by composite_pi. Combined list capped at topN. @@ -2643,23 +2528,28 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) auto emitRow = [&](const SevRow &r, bool first) { if (!first) sout << ",\n "; else sout << "\n "; + bool hasBr = aggBranchViol.count(r.name) > 0; + bool hasVLo = r.worstVdevLo < 0.0; + bool hasVHi = r.worstVdevHi > 0.0; sout << "{\"contingency\": \"" << r.name << "\"" - << ", \"group\": \"" << (r.violated ? "violated" : "stressed") << "\"" << ", \"composite_pi\": " << std::setprecision(6) << r.composite - << ", \"branch_pi\": " << std::setprecision(6) << r.branchPi - << ", \"voltage_pi\": " << std::setprecision(6) << r.voltagePi - << ", \"worst_branch_loading_percent\": " - << std::setprecision(2) << r.worstLoading - << ", \"worst_voltage_pu_low\": " - << std::setprecision(6) << r.worstVpuLo - << ", \"worst_voltage_deviation_pu_low\": " - << std::setprecision(6) << r.worstVdevLo - << ", \"worst_voltage_pu_high\": " - << std::setprecision(6) << r.worstVpuHi - << ", \"worst_voltage_deviation_pu_high\": " - << std::setprecision(6) << r.worstVdevHi - << ", \"has_branch_violation\": " - << (aggBranchViol.count(r.name) ? "true" : "false") + << ", \"worst_branch_loading_percent\": "; + if (hasBr) sout << std::setprecision(2) << r.worstLoading; + else sout << "null"; + sout << ", \"worst_voltage_pu_low\": "; + if (hasVLo) sout << std::setprecision(6) << r.worstVpuLo; + else sout << "null"; + sout << ", \"worst_voltage_deviation_pu_low\": "; + if (hasVLo) sout << std::setprecision(6) << r.worstVdevLo; + else sout << "null"; + sout << ", \"worst_voltage_pu_high\": "; + if (hasVHi) sout << std::setprecision(6) << r.worstVpuHi; + else sout << "null"; + sout << ", \"worst_voltage_deviation_pu_high\": "; + if (hasVHi) sout << std::setprecision(6) << r.worstVdevHi; + else sout << "null"; + sout << ", \"has_branch_violation\": " + << (hasBr ? "true" : "false") << ", \"has_voltage_violation\": " << (aggVoltageViol.count(r.name) ? "true" : "false") << "}"; From f3f8d18de700f8d33d969416efddfe58159c54b7 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 18 Jul 2026 16:18:32 -0700 Subject: [PATCH 27/31] Merge worst voltage low/high into single unsigned-deviation field --- .../contingency_analysis/ca_driver.cpp | 102 +++++++----------- 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index b8cfd691..860f618d 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -648,10 +648,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::map ctVpi; // sum((v-1)/dv)^2, all energized buses std::map ctVdev; // sum(v-limit)^2, violated buses only std::map ctWorstLoading; // max loading_pct among violated branches - std::map ctWorstVdevLo; // most negative (v-Vmin), 0 if none - std::map ctWorstVdevHi; // most positive (v-Vmax), 0 if none - std::map ctWorstVpuLo; // v_pu that produced ctWorstVdevLo - std::map ctWorstVpuHi; // v_pu that produced ctWorstVdevHi + std::map ctWorstVdev; // max |v - limit| among violated buses + std::map ctWorstVpu; // v_pu that produced ctWorstVdev auto accumBranchPi = [&](const std::string &ct_name, double mva, double rate) { if (rate <= 0.0) return; @@ -745,13 +743,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } ctsWithVoltageViol.insert(ct_name); accumVoltageDev(ct_name, dev); - if (dev < 0.0) { - double &d = ctWorstVdevLo[ct_name]; - if (dev < d) { d = dev; ctWorstVpuLo[ct_name] = v_pu; } - } else { - double &d = ctWorstVdevHi[ct_name]; - if (dev > d) { d = dev; ctWorstVpuHi[ct_name] = v_pu; } - } + double absDev = std::abs(dev); + double &d = ctWorstVdev[ct_name]; + if (absDev > d) { d = absDev; ctWorstVpu[ct_name] = v_pu; } }; // (from, to, ckt) key shared by the monitor allowlist and base_cache. @@ -2291,17 +2285,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) MPI_Send(&wvHi, sizeof(wvHi), MPI_BYTE, 0, 32, mpi_comm); } - // Gather per-ct PI/VPI/Vdev/rosters + per-ct worst-single-element data to - // rank 0 as one text blob per rank. Line has 10 tab-separated fields: - // name pi vpi vdev worstLoad worstVdevLo worstVpuLo worstVdevHi worstVpuHi br_flag v_flag + // Gather per-ct PI/VPI/Vdev/rosters + per-ct worst-single-element data. + // Blob line: name pi vpi vdev worstLoad worstVdevAbs worstVpu br_flag v_flag std::map aggPi; std::map aggVpi; std::map aggVdev; std::map aggWorstLoading; - std::map aggWorstVdevLo; - std::map aggWorstVpuLo; - std::map aggWorstVdevHi; - std::map aggWorstVpuHi; + std::map aggWorstVdev; // unsigned max |v-limit| + std::map aggWorstVpu; // v_pu that produced aggWorstVdev std::set aggBranchViol; std::set aggVoltageViol; { @@ -2326,19 +2317,16 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) for (std::set::const_iterator it = names.begin(); it != names.end(); ++it) { const std::string &nm = *it; - double pi = lookup(ctPi, nm); - double vpi = lookup(ctVpi, nm); - double vdev = lookup(ctVdev, nm); - double wL = lookup(ctWorstLoading, nm); - double wDLo = lookup(ctWorstVdevLo, nm); - double wVLo = lookup(ctWorstVpuLo, nm); - double wDHi = lookup(ctWorstVdevHi, nm); - double wVHi = lookup(ctWorstVpuHi, nm); + double pi = lookup(ctPi, nm); + double vpi = lookup(ctVpi, nm); + double vdev = lookup(ctVdev, nm); + double wL = lookup(ctWorstLoading, nm); + double wDabs = lookup(ctWorstVdev, nm); + double wV = lookup(ctWorstVpu, nm); int brFlag = ctsWithBranchViol.count(nm) ? 1 : 0; int vFlag = ctsWithVoltageViol.count(nm) ? 1 : 0; localOut << nm << '\t' << pi << '\t' << vpi << '\t' << vdev << '\t' - << wL << '\t' << wDLo << '\t' << wVLo << '\t' - << wDHi << '\t' << wVHi << '\t' + << wL << '\t' << wDabs << '\t' << wV << '\t' << brFlag << '\t' << vFlag << '\n'; } std::string localBlob = localOut.str(); @@ -2365,24 +2353,21 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) std::string line = blob.substr(pos, eol - pos); pos = eol + 1; splitTabs(line, fields); - if (fields.size() < 11) continue; + if (fields.size() < 9) continue; const std::string &nm = fields[0]; - double pi = std::atof(fields[1].c_str()); - double vpi = std::atof(fields[2].c_str()); - double vdev = std::atof(fields[3].c_str()); - double wL = std::atof(fields[4].c_str()); - double wDLo = std::atof(fields[5].c_str()); - double wVLo = std::atof(fields[6].c_str()); - double wDHi = std::atof(fields[7].c_str()); - double wVHi = std::atof(fields[8].c_str()); - int brF = std::atoi(fields[9].c_str()); - int vF = std::atoi(fields[10].c_str()); + double pi = std::atof(fields[1].c_str()); + double vpi = std::atof(fields[2].c_str()); + double vdev = std::atof(fields[3].c_str()); + double wL = std::atof(fields[4].c_str()); + double wDabs = std::atof(fields[5].c_str()); + double wV = std::atof(fields[6].c_str()); + int brF = std::atoi(fields[7].c_str()); + int vF = std::atoi(fields[8].c_str()); if (pi != 0.0) aggPi[nm] += pi; if (vpi != 0.0) aggVpi[nm] += vpi; if (vdev != 0.0) aggVdev[nm] += vdev; - if (wL > aggWorstLoading[nm]) aggWorstLoading[nm] = wL; - if (wDLo < aggWorstVdevLo[nm]) { aggWorstVdevLo[nm] = wDLo; aggWorstVpuLo[nm] = wVLo; } - if (wDHi > aggWorstVdevHi[nm]) { aggWorstVdevHi[nm] = wDHi; aggWorstVpuHi[nm] = wVHi; } + if (wL > aggWorstLoading[nm]) aggWorstLoading[nm] = wL; + if (wDabs > aggWorstVdev[nm]) { aggWorstVdev[nm] = wDabs; aggWorstVpu[nm] = wV; } if (brF) aggBranchViol.insert(nm); if (vF) aggVoltageViol.insert(nm); } @@ -2488,8 +2473,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double sortKey; // Group A: worst-single severity; Group B: composite_pi double composite, branchPi, voltagePi; double worstLoading; - double worstVpuLo, worstVdevLo; - double worstVpuHi, worstVdevHi; + double worstVpu, worstVdev; // worstVdev is unsigned |v - limit| }; std::vector groupA, groupB; for (std::set::const_iterator it = ctSet.begin(); @@ -2500,18 +2484,15 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) r.voltagePi = agg_get(aggVpi, *it); r.composite = piBranchWeight * r.branchPi + piVoltageWeight * r.voltagePi; r.worstLoading = agg_get(aggWorstLoading, *it); - r.worstVdevLo = agg_get(aggWorstVdevLo, *it); - r.worstVpuLo = agg_get(aggWorstVpuLo, *it); - r.worstVdevHi = agg_get(aggWorstVdevHi, *it); - r.worstVpuHi = agg_get(aggWorstVpuHi, *it); + r.worstVdev = agg_get(aggWorstVdev, *it); + r.worstVpu = agg_get(aggWorstVpu, *it); bool hasBr = aggBranchViol.count(*it) > 0; bool hasV = aggVoltageViol.count(*it) > 0; r.violated = hasBr || hasV; if (r.violated) { double s = 0.0; if (r.worstLoading > 100.0) s = std::max(s, r.worstLoading - 100.0); - if (r.worstVdevLo < 0.0) s = std::max(s, std::abs(r.worstVdevLo) * 1000.0); - if (r.worstVdevHi > 0.0) s = std::max(s, r.worstVdevHi * 1000.0); + if (r.worstVdev > 0.0) s = std::max(s, r.worstVdev * 1000.0); r.sortKey = s; groupA.push_back(r); } else { @@ -2529,25 +2510,18 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (!first) sout << ",\n "; else sout << "\n "; bool hasBr = aggBranchViol.count(r.name) > 0; - bool hasVLo = r.worstVdevLo < 0.0; - bool hasVHi = r.worstVdevHi > 0.0; + bool hasV = r.worstVdev > 0.0; sout << "{\"contingency\": \"" << r.name << "\"" << ", \"composite_pi\": " << std::setprecision(6) << r.composite << ", \"worst_branch_loading_percent\": "; if (hasBr) sout << std::setprecision(2) << r.worstLoading; else sout << "null"; - sout << ", \"worst_voltage_pu_low\": "; - if (hasVLo) sout << std::setprecision(6) << r.worstVpuLo; - else sout << "null"; - sout << ", \"worst_voltage_deviation_pu_low\": "; - if (hasVLo) sout << std::setprecision(6) << r.worstVdevLo; - else sout << "null"; - sout << ", \"worst_voltage_pu_high\": "; - if (hasVHi) sout << std::setprecision(6) << r.worstVpuHi; - else sout << "null"; - sout << ", \"worst_voltage_deviation_pu_high\": "; - if (hasVHi) sout << std::setprecision(6) << r.worstVdevHi; - else sout << "null"; + sout << ", \"worst_voltage_pu\": "; + if (hasV) sout << std::setprecision(6) << r.worstVpu; + else sout << "null"; + sout << ", \"worst_voltage_deviation_pu\": "; + if (hasV) sout << std::setprecision(6) << r.worstVdev; + else sout << "null"; sout << ", \"has_branch_violation\": " << (hasBr ? "true" : "false") << ", \"has_voltage_violation\": " From 42943889a73a12c191b3351846af3f59659008ae Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Wed, 26 Aug 2026 08:11:10 -0700 Subject: [PATCH 28/31] Unify CA loading% denominator across all outputs; default rating A --- .../components/pf_matrix/pf_components.cpp | 39 ++++++++++++++++- .../components/pf_matrix/pf_components.hpp | 8 ++++ .../contingency_analysis/ca_driver.cpp | 43 +++++++++++++------ .../modules/powerflow/pf_factory_module.cpp | 3 ++ 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 55538764..6334fdd8 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -3628,6 +3628,38 @@ gridpack::ComplexType gridpack::powerflow::PFBranch::getReversePower( * routine what about kind of information to write * @return true if branch is contributing string to output, false otherwise */ +std::string gridpack::powerflow::PFBranch::s_contingencyRating = "A"; + +void gridpack::powerflow::PFBranch::setContingencyRating( + const std::string& rating) +{ + if (rating == "A" || rating == "B" || rating == "C") { + s_contingencyRating = rating; + } else { + s_contingencyRating = "A"; + } +} + +std::string gridpack::powerflow::PFBranch::getContingencyRating() +{ + return s_contingencyRating; +} + +// A->B->C fallback when the picked tier is zero. +double gridpack::powerflow::PFBranch::pickBranchRating(int elemIdx) const +{ + if (elemIdx < 0 || elemIdx >= static_cast(p_rateA.size())) return 0.0; + double a = p_rateA[elemIdx]; + double b = (elemIdx < static_cast(p_rateB.size())) ? p_rateB[elemIdx] : 0.0; + double c = (elemIdx < static_cast(p_rateC.size())) ? p_rateC[elemIdx] : 0.0; + if (s_contingencyRating == "A") return a; + if (s_contingencyRating == "B") return (b > 0.0) ? b : a; + // "C" + if (c > 0.0) return c; + if (b > 0.0) return b; + return a; +} + bool gridpack::powerflow::PFBranch::serialWrite(char *string, const int bufsize, const char *signal) { @@ -3710,10 +3742,13 @@ bool gridpack::powerflow::PFBranch::serialWrite(char *string, const int bufsize, if (bus1->isIsolated() || bus2->isIsolated()) p=0.0; if (bus1->isIsolated() || bus2->isIsolated()) q=0.0; double S = sqrt(p*p+q*q); - if (S > p_rateA[i] && p_rateA[i] != 0.0){ + // Use the picked contingency rating tier (A/B/C w/ fallback) as the + // loading% denominator so this .out file agrees with _violations.csv. + double rate = pickBranchRating(i); + if (S > rate && rate != 0.0){ sprintf(buf, " %6d %6d %s %12.6f %12.6f %8.2f %8.2f%s\n", getBus1OriginalIndex(),getBus2OriginalIndex(),tags[i].c_str(), - p,q,p_rateA[i],S/p_rateA[i]*100,"%"); + p,q,rate,S/rate*100,"%"); int len = strlen(buf); if (ilen + len < bufsize) { sprintf(string,"%s",buf); diff --git a/src/applications/components/pf_matrix/pf_components.hpp b/src/applications/components/pf_matrix/pf_components.hpp index 56e7b69d..08a41cf3 100644 --- a/src/applications/components/pf_matrix/pf_components.hpp +++ b/src/applications/components/pf_matrix/pf_components.hpp @@ -990,6 +990,12 @@ class PFBranch */ double getBranchRatingC(std::string tag); + // Contingency rating tier ("A"|"B"|"C") used by serialWrite("flow",...). + // Default "A" preserves non-CA behavior; A->B->C fallback if picked=0. + static void setContingencyRating(const std::string& rating); + static std::string getContingencyRating(); + double pickBranchRating(int elemIdx) const; + /** * Get list of line IDs * @return list of line identifiers @@ -1077,6 +1083,8 @@ class PFBranch // LTC (Load Tap Changer) control variables bool p_hasLTC; // true if this branch has an LTC-controlled transformer int p_ltc_elem; // index of the LTC element within this branch + static std::string s_contingencyRating; + int p_ltc_code; // control mode (1=voltage, 0=off) int p_ltc_cont; // controlled bus number bool p_ltc_cont_is_to; // true if controlled bus is the to-bus (tap direction reversal) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 860f618d..1d4dc062 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -444,18 +444,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (!tok[i].empty()) monitorAreas.insert(atoi(tok[i].c_str())); } } - // Which rating column csv_flat/csv_delta emit. A|B|C, default C. - // Falls back A->B->C order if requested rating is zero/missing. - std::string contingencyRating = "C"; + // Loading% denominator for all CA outputs (.out, _violations.csv, JSON, + // csv_flat, csv_delta). A|B|C, default A to match PW/PSSE convention. + // A->B->C fallback if the requested tier is zero/missing. + std::string contingencyRating = "A"; cursor->get("contingencyRating", &contingencyRating); util.toUpper(contingencyRating); if (contingencyRating != "A" && contingencyRating != "B" && contingencyRating != "C") { if (world.rank() == 0) { - printf("WARNING: contingencyRating='%s' not A/B/C; defaulting to C\n", + printf("WARNING: contingencyRating='%s' not A/B/C; defaulting to A\n", contingencyRating.c_str()); } - contingencyRating = "C"; + contingencyRating = "A"; } // Severity threshold for violation reporting; loading% > threshold*100 // is flagged. Default 1.0 (100% of rate). @@ -2213,9 +2214,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // on rank 0. Counters and worst-of values follow the same shape commercial // tools use in their contingency reports. { - long localCounters[4] = { 0 }; // 0: total_ct 1: converged 2: cts_with_branch_viol 3: cts_with_voltage_viol - // (violation_rows is summed separately below) + // 4: islanded 5: no_slack 6: diverged 7: slack_overload + long localCounters[8] = { 0 }; if (!localContingencies.empty()) { // json/csv paths: authoritative per-ct list. for (size_t ci = 0; ci < localContingencies.size(); ci++) { @@ -2224,8 +2225,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (ct.solution.convergence.converged) localCounters[1] += 1; } } else { - // text/csv_flat/csv_delta: count from convergence rows. Use status - // rather than cs.converged, which can be stale on ISLANDED/DIVERGED. + // text/csv_flat/csv_delta: count from convergence rows. localCounters[0] = static_cast(localConvRows.size()); long conv = 0; for (size_t i = 0; i < localConvRows.size(); i++) { @@ -2235,9 +2235,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } localCounters[2] = static_cast(ctsWithBranchViol.size()); localCounters[3] = static_cast(ctsWithVoltageViol.size()); - long totalCounters[4] = { 0 }; - for (int i = 0; i < 4; i++) totalCounters[i] = localCounters[i]; - world.sum(&totalCounters[0], 4); + // Per-status breakdown from convergence rows (populated in every mode). + for (size_t i = 0; i < localConvRows.size(); i++) { + const std::string &st = localConvRows[i].status; + if (st == "ISLANDED") localCounters[4] += 1; + else if (st == "NO_SLACK") localCounters[5] += 1; + else if (st == "DIVERGED") localCounters[6] += 1; + else if (st == "SLACK_OVERLOAD") localCounters[7] += 1; + } + long totalCounters[8] = { 0 }; + for (int i = 0; i < 8; i++) totalCounters[i] = localCounters[i]; + world.sum(&totalCounters[0], 8); long localViolRows = static_cast(violRowCount); long totalViolRows = localViolRows; world.sum(&totalViolRows, 1); @@ -2403,6 +2411,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) sout << " \"total_contingencies\": " << totalCounters[0] << ",\n"; sout << " \"converged\": " << totalCounters[1] << ",\n"; sout << " \"diverged\": " << (totalCounters[0] - totalCounters[1]) << ",\n"; + // Per-status breakdown of the diverged bucket. Sum equals `diverged`. + sout << " \"islanded\": " << totalCounters[4] << ",\n"; + sout << " \"no_slack\": " << totalCounters[5] << ",\n"; + sout << " \"solver_diverged\": " << totalCounters[6] << ",\n"; + sout << " \"slack_overload\": " << totalCounters[7] << ",\n"; sout << " \"with_branch_violation\": " << totalCounters[2] << ",\n"; sout << " \"with_voltage_violation\": " << totalCounters[3] << ",\n"; sout << " \"worst_loading\": "; @@ -2731,10 +2744,14 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Universal convergence sidecar: gather, sort by event_idx, write. if (emitConv) { auto formatRow = [](std::ostringstream &os, const ConvRow &r) { + // Derive converged from status so ISLANDED/NO_SLACK cases -- where solve() + // was never entered and pf_app.getConvergence() returns the previous + // case's stale value -- read as false, matching _summary.json's + // diverged=total-converged accounting. os << r.event_idx << "," << r.name << "," << r.type << "," - << (r.cs.converged ? "true" : "false") << "," + << ((r.status == "OK") ? "true" : "false") << "," << r.cs.iterations << "," << std::scientific << r.cs.finalTolerance << "," << std::fixed diff --git a/src/applications/modules/powerflow/pf_factory_module.cpp b/src/applications/modules/powerflow/pf_factory_module.cpp index 7ed8d030..858e33c8 100644 --- a/src/applications/modules/powerflow/pf_factory_module.cpp +++ b/src/applications/modules/powerflow/pf_factory_module.cpp @@ -1463,6 +1463,9 @@ void gridpack::powerflow::PFFactoryModule::setContingencyRating( } else { p_contingencyRating = "A"; } + // Keep PFBranch's serialWrite("flow",...) denominator in sync so the .out + // file's loading% matches _violations.csv / JSON loading_percent. + gridpack::powerflow::PFBranch::setContingencyRating(p_contingencyRating); } /** From 5a6ed902df8933223a011235c40878e010b39bef Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Wed, 26 Aug 2026 08:51:27 -0700 Subject: [PATCH 29/31] minor update on default rating and manual --- .../user_manual/sphinx/Section10-Examples.rst | 5 +- .../sphinx/Section9-ApplicationModules.rst | 259 ++++++++++++++++++ .../contingency_analysis/README.md | 31 +-- .../input/ca/input_14_filters_example.xml | 8 +- 4 files changed, 279 insertions(+), 24 deletions(-) diff --git a/docs/user_manual/sphinx/Section10-Examples.rst b/docs/user_manual/sphinx/Section10-Examples.rst index 348d3e81..b51e41a9 100644 --- a/docs/user_manual/sphinx/Section10-Examples.rst +++ b/docs/user_manual/sphinx/Section10-Examples.rst @@ -1305,7 +1305,10 @@ An example contingency application has been included in the contingency analysis directory. This contingency analysis is simpler than the one available under the ``applications`` directory and provides a relatively compact demonstration of some of the advanced features of -GridPACK. This application is built entirely around the power flow +GridPACK. The production driver under ``src/applications/contingency_analysis`` +adds CSV outputs, monitor filters, and PSS/E-aligned options; its +runtime configuration is documented in the *Contingency Analysis Module* +section of Section 9. This application is built entirely around the power flow module, so it has no network component classes of its own. The main functionality is located in the ``CADriver`` class that consists of two methods (other than the constructor and destructor). One function is diff --git a/docs/user_manual/sphinx/Section9-ApplicationModules.rst b/docs/user_manual/sphinx/Section9-ApplicationModules.rst index d7fbe364..fef346f5 100644 --- a/docs/user_manual/sphinx/Section9-ApplicationModules.rst +++ b/docs/user_manual/sphinx/Section9-ApplicationModules.rst @@ -135,6 +135,16 @@ adjusted by one discrete step per controller iteration, bounded by number of tap positions (NTP), or read directly from the STEP field when available. Cycle detection prevents tap hunting. +Remote voltage regulation (IREG) is enabled automatically for any +generator whose PSS/E ``IREG`` field points to a bus other than its +own. The controlled remote bus is treated as PV (held at the +generator's scheduled voltage ``VS``) and the local bus is solved +normally; the source bus voltage is adjusted across the controller loop +until the remote bus tracks setpoint. No XML option is required. When +the remote bus is the swing bus the regulation is dropped with a +warning, and when multiple generators on the same source bus include a +locally-regulating unit (``IREG=0``) local control wins. + The ``AreaInterchange`` parameter (default ``false``) enables area interchange control. When enabled, after the inner controller loop converges, the solver computes actual MW exports for each area by @@ -537,6 +547,255 @@ Again, this may be useful in contingency calculations where multiple calculations are run on the same network and it is desirable that they all start with the same initial condition. +Contingency Analysis Module +--------------------------- + +The production contingency analysis driver lives in +``src/applications/contingency_analysis``. It is built on +``PFAppModule`` and adds an MPI task manager, a contingency parser, +monitor filtering, and per-(contingency, branch) CSV output for +downstream statistical analysis. Configuration goes under a +``Contingency_analysis`` block in the input deck alongside the standard +``Powerflow`` block. The XML reader for the contingency list itself is +covered in Section 10; this section documents the runtime options. + +Input options +~~~~~~~~~~~~~ + ++----------------------------+--------+--------------------------------------------------------------+ +| Option | Default| Description | ++============================+========+==============================================================+ +| ``contingencyList`` | (none) | Path to the contingency XML. May be combined with | +| | | ``FullBranchN1`` / ``FullGeneratorN1`` for N-1 + custom N-K. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``FullBranchN1`` | false | Auto-generate N-1 over every in-service branch. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``FullGeneratorN1`` | false | Auto-generate N-1 over every in-service generator. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``groupSize`` | 1 | MPI processes per contingency. Power flow scales poorly so | +| | | leave at 1 and add ranks to widen task parallelism. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``minVoltage`` | 0.9 | Lower voltage limit (pu) for violation checks. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``maxVoltage`` | 1.1 | Upper voltage limit (pu) for violation checks. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``qlim`` | false | Enable PV→PQ Q-limit enforcement during the contingency | +| | | solve. Honors ``qlimDeadband`` from the ``Powerflow`` block. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``printCalcFiles`` | true | Write per-contingency text output (``.out``). | ++----------------------------+--------+--------------------------------------------------------------+ +| ``outputFormat`` | text | ``text`` | ``json`` | ``csv`` | ``csv_flat`` | ``csv_delta``.| ++----------------------------+--------+--------------------------------------------------------------+ +| ``outputFile`` | | Base name (prefix) for the structured output files. | +| | results| | ++----------------------------+--------+--------------------------------------------------------------+ +| ``writeStats`` | true | Emit the StatBlock summary ``.txt`` files. Set ``false`` to | +| | | skip the per-case StatBlock work when CSV is sufficient. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``contingencyRating`` | A | Loading% denominator across every CA output. ``A``, ``B``, | +| | | or ``C`` with A→B→C fallback. Default ``A`` matches PW / | +| | | PSS/E ACCC. ``base_rate_mva`` always uses rate-A. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``monitorBranchesFile`` | (none) | Path to a CSV allowlist (``from_bus,to_bus,ckt`` rows). | +| | | When set, area/kV gates are ignored. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``monitorAreas`` | (none) | Space-separated PSS/E area numbers. Branch passes if | +| | | **either endpoint** is in the set (catches tie-lines). | ++----------------------------+--------+--------------------------------------------------------------+ +| ``monitorKvMin`` | 0 | Lower kV threshold; branch passes if | +| | | ``max(kv_from, kv_to) >= monitorKvMin``. 0 disables. | ++----------------------------+--------+--------------------------------------------------------------+ +| ``monitorKvMax`` | 0 | Upper kV threshold; branch passes if | +| | | ``max(kv_from, kv_to) <= monitorKvMax``. 0 disables. | ++----------------------------+--------+--------------------------------------------------------------+ + +Violation / ranking options driving ``_violations.csv`` and +``_summary.json``: + +* ``violationSeverityThreshold`` (``1.0``) — loading% > threshold × 100 + flags a branch violation. +* ``topN`` (``20``, clamped ``[1,10000]``) — cap on ranked arrays. +* ``piBranchWeight`` / ``piVoltageWeight`` (both ``1.0``) — weights on + the branch-PI and voltage-PI terms in the composite PI ranking. + +Monitor filter precedence: When ``monitorBranchesFile`` is set, area/kV +options are ignored and a warning is logged. Otherwise ``monitorAreas`` +AND the kV bounds combine. With no filter set every branch is monitored. +Circuit IDs are matched with whitespace trimmed on both sides so PSS/E ckt +strings with leading or trailing spaces compare equal to the unpadded form. + +A complete annotated example is shipped at +``src/applications/data_sets/input/ca/input_14_filters_example.xml`` +with a sample monitor list ``monitor_branches_14.csv``. + +CSV outputs (``outputFormat=csv_flat`` / ``csv_delta``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``outputFormat`` is ``csv_flat`` or ``csv_delta`` the driver writes +per-(contingency, monitored branch) rows for downstream analysis instead +of the aggregated ``.txt`` files. All file names use the value of +``outputFile`` as a prefix; the default prefix is ``ca_results``. Both +formats also work for generator contingencies — column 3 (``type``) +labels what was tripped and column 31 (``cont_event_facility``) +identifies the tripped element. + +``_delta.csv`` (``csv_delta`` only) — wide form, one row +per (contingency, monitored branch) joining base and contingency state +on the same row. Most downstream consumers prefer this because each row +is self-contained. + ++-----+----------------------------+----------------------------------------------------------+ +| # | Column | Notes | ++=====+============================+==========================================================+ +| 1 | ``event_idx`` | 0 = base case, 1..N = contingencies in input-deck order. | ++-----+----------------------------+----------------------------------------------------------+ +| 2 | ``contingency`` | Contingency name (``base_case`` for the base row). | ++-----+----------------------------+----------------------------------------------------------+ +| 3 | ``type`` | ``branch`` or ``generator`` — what was tripped. | ++-----+----------------------------+----------------------------------------------------------+ +| 4–6 | ``from_bus``, ``to_bus``, | Identity of the **monitored branch** (not the tripped | +| | ``ckt`` | element). | ++-----+----------------------------+----------------------------------------------------------+ +| 7–8 | ``base_kv_from``, | Endpoint base kV. | +| | ``base_kv_to`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 9–10| ``area_from``, | PSS/E area numbers. | +| | ``area_to`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 11 | ``base_rate_mva`` | Always rate-A (PSS/E "normal" rating). | ++-----+----------------------------+----------------------------------------------------------+ +| 12 | ``cont_rate_mva`` | Rating selected by ``contingencyRating``. | ++-----+----------------------------+----------------------------------------------------------+ +| 13–14| ``base_p_mw``, | Real-power flow before / after. | +| | ``cont_p_mw`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 15–16| ``base_q_mvar``, | Reactive-power flow before / after. | +| | ``cont_q_mvar`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 17–18| ``base_mva``, | ``sqrt(P² + Q²)`` before / after. | +| | ``cont_mva`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 19 | ``base_loading_pct`` | ``base_mva / base_rate_mva × 100``. | ++-----+----------------------------+----------------------------------------------------------+ +| 20 | ``cont_loading_pct`` | ``cont_mva / cont_rate_mva × 100``. | ++-----+----------------------------+----------------------------------------------------------+ +| 21–22| ``v_from_base``, | From-bus voltage magnitude (pu). | +| | ``v_from_cont`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 23–24| ``v_to_base``, | To-bus voltage magnitude (pu). | +| | ``v_to_cont`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 25–28| ``ang_from_base``, | Endpoint bus angles (deg). | +| | ``ang_from_cont``, | | +| | ``ang_to_base``, | | +| | ``ang_to_cont`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 29–30| ``d_angle_base``, | ``ang_from − ang_to`` before / after. | +| | ``d_angle_cont`` | | ++-----+----------------------------+----------------------------------------------------------+ +| 31 | ``cont_event_facility`` | The tripped element (e.g. ``[area] from to ckt`` for a | +| | | branch trip, ``gen `` for a gen trip). | ++-----+----------------------------+----------------------------------------------------------+ + +``_flat.csv`` (``csv_flat`` only) — long form, one row per +(case, branch). Columns: ``event_idx, contingency, from_bus, to_bus, +ckt, p_from_mw, q_from_mvar, mva_from, rate_mva, loading_percent, viol, +v_from_pu, v_to_pu, ang_from_deg, ang_to_deg``. ``rate_mva`` is rate-A +on the base row and the configured ``contingencyRating`` on contingency +rows. + +``_buses.csv`` (both CSV formats) — bus metadata sidecar so +the per-branch files can stay narrow. Columns: ``bus_id, bus_name, +base_kv, area, zone, owner, area_name, zone_name, owner_name``. + +``_convergence.csv`` (every ``outputFormat``) — one row per +contingency. Columns: ``event_idx, contingency, type, converged, +iterations, final_tolerance, max_p_bus, max_p_mismatch, max_q_bus, +max_q_mismatch, status_code``. ``converged`` is ``true`` iff +``status_code == "OK"``; ``status_code`` is one of ``OK`` / ``ISLANDED`` +/ ``NO_SLACK`` / ``DIVERGED`` / ``SLACK_OVERLOAD``. Failed rows appear +here even when omitted from ``_delta.csv`` / ``_flat.csv``. + +``_violations.csv`` (every ``outputFormat``) — streamed row +per branch/voltage violation. Columns: ``event_idx, contingency, type, +element, mva_or_vpu, rate_or_limit, loading_percent, base_mva, delta, +severity``. Emitted when ``loading_percent > violationSeverityThreshold +× 100`` (branch) or ``v_pu`` outside ``[minVoltage, maxVoltage]`` +(voltage). ``loading_percent`` divides by the rate picked under +``contingencyRating``. ``severity = "critical"`` when branch loading +≥ 105 % or |Δv| ≥ 0.05 pu, else ``"warning"``. + +``_summary.json`` (every ``outputFormat``) — end-of-run +aggregate: ``total_contingencies``, ``converged``, ``diverged = +total − converged``, plus the per-status split ``islanded``, +``no_slack``, ``solver_diverged``, ``slack_overload`` (sum to +``diverged``). Also ``with_branch_violation`` / ``with_voltage_violation`` +/ ``violation_rows``; single-element extremes ``worst_loading`` / +``worst_voltage_low`` / ``worst_voltage_high`` (``null`` if none); +name arrays ``contingencies_with_branch_violation`` and +``contingencies_with_voltage_violation``; a length-``topN`` ranked +``top_severe_contingencies`` list (violated cases first by severity, +non-violated after by composite PI); and an echo of the run config +(``contingency_rating``, ``voltage_limit_low``/``high``, +``severity_threshold``). + +When monitor filters are active the data-row count of ``_delta.csv`` / +``_flat.csv`` equals ``|monitored branches| × |converged +contingencies|``. + +Output ordering +~~~~~~~~~~~~~~~ + +Rows are not sorted by contingency. The driver streams each MPI rank's +results to a per-rank ``.part`` file and rank 0 concatenates them in +rank order, so the final file is grouped by rank and ordered by +completion within each rank. Column 1 (``event_idx``) preserves +input-deck order — sort downstream if needed:: + + ( head -1 my_run_delta.csv && \ + tail -n +2 my_run_delta.csv | sort -t, -k1,1n ) > my_run_delta.sorted.csv + +Aggregated ``.txt`` outputs (``writeStats=true``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``writeStats=true`` the driver also writes the StatBlock summary +files: ``vmag.txt``, ``vmag_mm.txt``, ``vang.txt``, ``vang_mm.txt``, +``pgen.txt``, ``pgen_mm.txt``, ``qgen.txt``, ``qgen_mm.txt``, +``pflow.txt``, ``pflow_mm.txt``, ``qflow.txt``, ``qflow_mm.txt``, +``perf_mm.txt``, ``perf_sum.txt``, ``line_flt_cnt.txt``. The file +``pq_change_cnt.txt`` is added when ``qlim=true``. These hold per-element +statistics (mean / RMS / min / max) **across all contingencies**, not +per-contingency rows; their layout is described in the application +``README.md``. Set ``writeStats=false`` to skip them when CSV output is +sufficient. + +Contingency XML aliases +~~~~~~~~~~~~~~~~~~~~~~~ + +The element names in a contingency XML accept PSS/E-aligned aliases: + ++----------------------------+------------+-------------------------------+ +| Element | Alias | Holds | ++============================+============+===============================+ +| ``contingencyLineNames`` | ``CKT`` | Branch circuit ID (PSS/E | +| | | ``CKT`` field). | ++----------------------------+------------+-------------------------------+ +| ``contingencyGenerators`` | ``GenID`` | Generator ID (PSS/E ``ID`` | +| | | field). | ++----------------------------+------------+-------------------------------+ + +Either name parses; mix-and-match within a file is fine. + +Advanced behavior +~~~~~~~~~~~~~~~~~ + +The driver also includes automatic slack-bus transfer when the slack +generator is the tripped element, a slack-capacity check after each +solve, and island / lone-bus detection when a branch trip splits the +network. The underlying mechanisms (``checkAndTransferSlack``, +``restoreSlack``, ``checkSlackCapacity``, ``getIslandCount``, +``hasLoneBus``) are exposed by ``PFAppModule`` and described above. + State Estimation Module ----------------------- diff --git a/src/applications/contingency_analysis/README.md b/src/applications/contingency_analysis/README.md index 55b260ff..cdfe7edd 100644 --- a/src/applications/contingency_analysis/README.md +++ b/src/applications/contingency_analysis/README.md @@ -43,7 +43,7 @@ When combined, duplicates from the file are automatically skipped. | `outputFormat` | `text` / `json` / `csv` / `csv_flat` / `csv_delta` | `text` | | `outputFile` | Base name for output files | `ca_results` | | `writeStats` | Emit StatBlock summary files (vmag.txt etc.). Set false to skip and avoid the per-case StatBlock work | true | -| `contingencyRating` | Which PSS/E rating drives `cont_rate_mva` / `cont_loading_pct`: `A`, `B`, or `C` (with A→B→C fallback if missing). `base_rate_mva` always uses rate-A | `C` | +| `contingencyRating` | Loading% denominator across every CA output: `A`, `B`, or `C` with A→B→C fallback. Default `A` matches PW / PSS/E ACCC. `base_rate_mva` always uses rate-A | `A` | | `monitorBranchesFile` | Path to a CSV allowlist (`from_bus,to_bus,ckt`). When set, overrides the area/kV gates (TARA / PSS/E convention) | (unset) | | `monitorAreas` | Space-separated list of PSS/E area numbers. Branch is emitted if **either endpoint** is in the set | (unset) | | `monitorKvMin` | Lower kV threshold; branch passes if `max(kv_from, kv_to) >= monitorKvMin` | 0 (unbounded) | @@ -76,9 +76,10 @@ and a warning is logged. tie-lines). `monitorKvMin/Max` gate on `max(kv_from, kv_to)` so a 138/13.8 step-down counts as 138. -`contingencyRating` (`A` | `B` | `C`, default `C`) selects the rating -behind `cont_rate_mva` / `cont_loading_pct`. `base_rate_mva` always uses -rate-A. Falls back A→B→C if the requested rating is zero/missing. +`contingencyRating` (`A` | `B` | `C`, default `A`) sets the loading% +denominator for every CA output. Default `A` matches PowerWorld / PSS/E +ACCC. `base_rate_mva` always uses rate-A. A→B→C fallback if the picked +tier is zero/missing. A complete annotated example is in `src/applications/data_sets/input/ca/input_14_filters_example.xml` with a @@ -171,7 +172,7 @@ each row is self-contained (no separate base-case join needed). | 7–8 | `base_kv_from`, `base_kv_to` | Endpoint base kV | | 9–10 | `area_from`, `area_to` | PSS/E area numbers | | 11 | `base_rate_mva` | Always rate-A (PSS/E "normal" rating) | -| 12 | `cont_rate_mva` | Rating selected by `contingencyRating` (default C, with A→B→C fallback if zero/missing) | +| 12 | `cont_rate_mva` | Rating selected by `contingencyRating` (default A, with A→B→C fallback if zero/missing) | | 13–14 | `base_p_mw`, `cont_p_mw` | Real-power flow before / after contingency | | 15–16 | `base_q_mvar`, `cont_q_mvar` | Reactive-power flow before / after | | 17–18 | `base_mva`, `cont_mva` | `sqrt(P² + Q²)` before / after | @@ -196,24 +197,16 @@ metadata sidecar so the per-branch files can stay narrow. Columns: owner_name`. **`_convergence.csv`** *(every `outputFormat`)* — one row per -contingency. Columns: `event_idx, contingency, type, status, iterations, -final_tolerance, max_p_bus, max_p_mismatch, max_q_bus, max_q_mismatch`. -Failed/divergent contingencies appear here even though they're omitted -from `_delta.csv` / `_flat.csv`. +contingency. Columns: `event_idx, contingency, type, converged, +iterations, final_tolerance, max_p_bus, max_p_mismatch, max_q_bus, +max_q_mismatch, status_code`. `converged` is `true` iff `status_code == +"OK"`; `status_code` is one of `OK` / `ISLANDED` / `NO_SLACK` / +`DIVERGED` / `SLACK_OVERLOAD`. Failed rows appear here even though +they're omitted from `_delta.csv` / `_flat.csv`. When monitor filters are active, the data-row count of `_delta.csv` / `_flat.csv` equals `|monitored branches| × |converged contingencies|`. -#### Pandas quickstart - -```python -import pandas as pd -df = pd.read_csv("my_run_delta.csv") -df[df.cont_loading_pct >= 90.0] # overloaded branches -df.assign(dv=df.v_from_cont - df.v_from_base) \ - .nsmallest(20, "dv")[["contingency","from_bus","dv"]] -``` - --- ### Aggregated `.txt` outputs (`writeStats=true`, default) diff --git a/src/applications/data_sets/input/ca/input_14_filters_example.xml b/src/applications/data_sets/input/ca/input_14_filters_example.xml index 47def1a1..70a34ec6 100644 --- a/src/applications/data_sets/input/ca/input_14_filters_example.xml +++ b/src/applications/data_sets/input/ca/input_14_filters_example.xml @@ -47,10 +47,10 @@ - + C From 349f4b6b8f3e6c82d447105d3e9656ec819455d4 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Thu, 3 Sep 2026 11:21:01 -0700 Subject: [PATCH 30/31] updated contingency output formats for better usability --- .../components/pf_matrix/pf_components.cpp | 22 ++- .../contingency_analysis/ca_driver.cpp | 140 ++++++++++++++---- 2 files changed, 124 insertions(+), 38 deletions(-) diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 6334fdd8..3ad04f35 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -3663,7 +3663,7 @@ double gridpack::powerflow::PFBranch::pickBranchRating(int elemIdx) const bool gridpack::powerflow::PFBranch::serialWrite(char *string, const int bufsize, const char *signal) { - char buf[128]; + char buf[256]; gridpack::powerflow::PFBus *bus1 = dynamic_cast(getBus1().get()); gridpack::powerflow::PFBus *bus2 @@ -3737,18 +3737,28 @@ bool gridpack::powerflow::PFBranch::serialWrite(char *string, const int bufsize, s = getComplexPower(tags[i]); double p = real(s); double q = imag(s); + gridpack::ComplexType s2 = getReversePower(tags[i]); + double p2 = real(s2); + double q2 = imag(s2); if (!p_branch_status[i]) p = 0.0; if (!p_branch_status[i]) q = 0.0; + if (!p_branch_status[i]) p2 = 0.0; + if (!p_branch_status[i]) q2 = 0.0; if (bus1->isIsolated() || bus2->isIsolated()) p=0.0; if (bus1->isIsolated() || bus2->isIsolated()) q=0.0; - double S = sqrt(p*p+q*q); - // Use the picked contingency rating tier (A/B/C w/ fallback) as the - // loading% denominator so this .out file agrees with _violations.csv. + if (bus1->isIsolated() || bus2->isIsolated()) p2=0.0; + if (bus1->isIsolated() || bus2->isIsolated()) q2=0.0; + double Sfrom = sqrt(p*p+q*q); + double Sto = sqrt(p2*p2+q2*q2); + // One loading value, measured at the more heavily loaded end, as + // PowerWorld reports it and as _violations.csv/_branches.csv compute it. + double S = (Sfrom > Sto) ? Sfrom : Sto; double rate = pickBranchRating(i); if (S > rate && rate != 0.0){ - sprintf(buf, " %6d %6d %s %12.6f %12.6f %8.2f %8.2f%s\n", + sprintf(buf, "%10d%10d%6s%13.6f%13.6f%13.6f%13.6f%13.6f%13.6f" + "%11.2f%10.2f%s\n", getBus1OriginalIndex(),getBus2OriginalIndex(),tags[i].c_str(), - p,q,rate,S/rate*100,"%"); + p,q,Sfrom,p2,q2,Sto,rate,S/rate*100,"%"); int len = strlen(buf); if (ilen + len < bufsize) { sprintf(string,"%s",buf); diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index 1d4dc062..e42b4233 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -1140,33 +1140,49 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) deltaPart.open(deltaPartPath.c_str(), std::ios::out | std::ios::trunc); deltaPart << std::fixed; } - // cont_event_facility: built once per contingency. - std::string facility; - if (evt.p_type == Branch && !evt.p_from.empty()) { - int outFrom = evt.p_from[0]; - int area = 0; - std::map::const_iterator mf = bus_meta.find(outFrom); - if (mf != bus_meta.end()) area = mf->second.area; - char buf[64]; - snprintf(buf, sizeof(buf), "[%d] %d %d %s", - area, outFrom, evt.p_to[0], evt.p_ckt[0].c_str()); - facility = buf; - if (evt.p_from.size() > 1) { - char suf[24]; - snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_from.size() - 1); - facility += suf; - } - } else if (evt.p_type == Generator && !evt.p_busid.empty()) { - char buf[48]; - snprintf(buf, sizeof(buf), "gen %d %s", - evt.p_busid[0], evt.p_genid[0].c_str()); - facility = buf; - if (evt.p_busid.size() > 1) { - char suf[24]; - snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_busid.size() - 1); - facility += suf; - } - } + // cont_event_facility: disabled -- join event_idx against + // _contingencies.csv instead, which names every outaged element + // rather than the first plus "(+N more)". Kept commented in case the column + // is wanted back; uncomment this block, the emission below and the header + // field together. The [area] lookup needs a complete bus_meta (groupSize=1). + // std::string facility; + // // clean2Char pads ids to two chars; trim so the label has no stray space. + // auto rtrimId = [](const std::string &in) -> std::string { + // std::string t = in; + // while (!t.empty() && (t[t.size()-1] == ' ' || t[t.size()-1] == '\t')) + // t.resize(t.size()-1); + // return t; + // }; + // // "[area] " for both kinds; the type column says which. + // if (evt.p_type == Branch && !evt.p_from.empty()) { + // int outFrom = evt.p_from[0]; + // int area = 0; + // std::map::const_iterator mf = bus_meta.find(outFrom); + // if (mf != bus_meta.end()) area = mf->second.area; + // char buf[64]; + // snprintf(buf, sizeof(buf), "[%d] %d %d %s", + // area, outFrom, evt.p_to[0], rtrimId(evt.p_ckt[0]).c_str()); + // facility = buf; + // if (evt.p_from.size() > 1) { + // char suf[24]; + // snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_from.size() - 1); + // facility += suf; + // } + // } else if (evt.p_type == Generator && !evt.p_busid.empty()) { + // int outBus = evt.p_busid[0]; + // int area = 0; + // std::map::const_iterator mg = bus_meta.find(outBus); + // if (mg != bus_meta.end()) area = mg->second.area; + // char buf[64]; + // snprintf(buf, sizeof(buf), "[%d] %d %s", + // area, outBus, rtrimId(evt.p_genid[0]).c_str()); + // facility = buf; + // if (evt.p_busid.size() > 1) { + // char suf[24]; + // snprintf(suf, sizeof(suf), " (+%zu more)", evt.p_busid.size() - 1); + // facility += suf; + // } + // } std::string ct_name = evt.p_name; while (!ct_name.empty() && ct_name[ct_name.size()-1] == ' ') ct_name.resize(ct_name.size()-1); @@ -1222,6 +1238,9 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) double a_to_c = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; double d_ang_b = bf.ang_from_deg - bf.ang_to_deg; double d_ang_c = a_from_c - a_to_c; + // Across-branch drop, same convention as the angle deltas above. + double d_v_b = bf.v_from_pu - bf.v_to_pu; + double d_v_c = v_from_c - v_to_c; deltaPart << event_idx << "," << ct_name << "," << type_str << "," << from << "," << to << "," << k.ckt << "," << std::setprecision(2) << bf.base_kv_from << "," @@ -1245,9 +1264,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << std::setprecision(4) << a_from_c << "," << std::setprecision(4) << bf.ang_to_deg << "," << std::setprecision(4) << a_to_c << "," + << std::setprecision(6) << d_v_b << "," + << std::setprecision(6) << d_v_c << "," << std::setprecision(4) << d_ang_b << "," - << std::setprecision(4) << d_ang_c << "," - << facility + << std::setprecision(4) << d_ang_c + // << "," << facility // cont_event_facility: disabled << "\n"; deltaRowCount++; } @@ -1428,6 +1449,53 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) printf("==================================================================\n\n"); } + // Decodes the event_idx used by every other CA output. One row per outaged + // element, so an N-2 event shares an event_idx; 0 is the base case. + if (world.rank() == 0) { + std::string ctgFile = outputFile + "_contingencies.csv"; + std::ofstream cout_ctg(ctgFile.c_str(), std::ios::out | std::ios::trunc); + cout_ctg << "event_idx,contingency,type,n_elements,element_seq," + "from_bus,to_bus,circuit_id,gen_bus,gen_id\n"; + size_t ctgRows = 0; + cout_ctg << "0,base_case,base,0,0,,,,,\n"; + ctgRows++; + for (size_t ei = 0; ei < events.size(); ei++) { + const gridpack::powerflow::Contingency &e = events[ei]; + int event_idx = static_cast(ei) + 1; + // Trim clean2Char padding so ids join against the other CSVs. + auto rtrim = [](const std::string &in) -> std::string { + std::string t = in; + while (!t.empty() && (t[t.size()-1] == ' ' || t[t.size()-1] == '\t')) + t.resize(t.size()-1); + return t; + }; + std::string nm = rtrim(e.p_name); + size_t n = 0; + const char *ty = "unknown"; + if (e.p_type == Branch) { n = e.p_from.size(); ty = "branch"; } + else if (e.p_type == Generator) { n = e.p_busid.size(); ty = "generator"; } + // An event with no elements (e.g. a malformed list entry) still gets a + // row, so no event_idx in the other files is left undecodable. + if (n == 0) { + cout_ctg << event_idx << "," << nm << "," << ty << ",0,0,,,,,\n"; + ctgRows++; + } + for (size_t j = 0; j < n; j++) { + cout_ctg << event_idx << "," << nm << "," << ty << "," + << n << "," << (j + 1) << ","; + if (e.p_type == Branch) + cout_ctg << e.p_from[j] << "," << e.p_to[j] << "," + << rtrim(e.p_ckt[j]) << ",,\n"; + else + cout_ctg << ",,," << e.p_busid[j] << "," + << rtrim(e.p_genid[j]) << "\n"; + ctgRows++; + } + } + cout_ctg.close(); + printf("[contingencies] wrote %zu rows to %s\n", ctgRows, ctgFile.c_str()); + } + // Print contingency details (gated on printCalcFiles; noisy for large lists) if (print_calcs && world.rank() == 0) { int idx; @@ -1702,7 +1770,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Evaluate contingencies using the task manager int task_id; - char sbuf[128]; + char sbuf[512]; // nextTask returns the same task_id on all processors in task_comm. When the // calculation runs out of task, nextTask will return false. while (taskmgr.nextTask(task_comm, &task_id)) { @@ -1848,7 +1916,11 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (print_calcs) pf_app.writeCABus(); // Report branch overload violations if (!ok2) { - sprintf(sbuf,"\nBranch Violation for contingency %s\n", + // Keep in step with the row format in PFBranch::serialWrite("flow"). + sprintf(sbuf,"\nBranch Violation for contingency %s\n" + " From Bus To Bus CKT P_from Q_from" + " MVA_from P_to Q_to MVA_to" + " Rate Loading%%\n", events[task_id].p_name.c_str()); } else if (!ok) { sprintf(sbuf,"\nNo Branch Violation for contingency %s\n", @@ -2125,7 +2197,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) "base_mva,cont_mva,base_loading_pct,cont_loading_pct," "v_from_base,v_from_cont,v_to_base,v_to_cont," "ang_from_base,ang_from_cont,ang_to_base,ang_to_cont," - "d_angle_base,d_angle_cont,cont_event_facility\n", + // ",cont_event_facility" here if re-enabling the column + "d_v_base,d_v_cont,d_angle_base,d_angle_cont\n", "csv_delta", "_delta.csv"); } @@ -2632,6 +2705,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) localBus << std::fixed; localBranch << std::fixed; localGen << std::fixed; + // Column layout must match the headers written by + // ResultsExporter::writePFCSV for the base case. for (size_t ci = 0; ci < localContingencies.size(); ci++) { const gridpack::utility::ContingencyResult& ct = localContingencies[ci]; const gridpack::utility::PowerFlowResults& r = ct.solution; @@ -2665,6 +2740,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) << std::setprecision(4) << br.mvaFrom << "," << std::setprecision(4) << br.mvaTo << "," << std::setprecision(4) << br.rateA << "," + << std::setprecision(4) << br.rateSelected << "," << std::setprecision(2) << br.loadingPercent << "\n"; } for (size_t gi = 0; gi < r.generators.size(); gi++) { From 49b15a9fb384834e8dbf383a25ed9f94c0b6c353 Mon Sep 17 00:00:00 2001 From: Yousu Chen Date: Sat, 5 Sep 2026 00:28:51 -0400 Subject: [PATCH 31/31] Apply CA monitor filters to every output format; simplify contingency lookup --- .../sphinx/Section9-ApplicationModules.rst | 4 +- .../components/pf_matrix/pf_components.cpp | 4 + .../contingency_analysis/README.md | 63 +++- .../contingency_analysis/ca_driver.cpp | 288 ++++++++++++++---- .../data_sets/input/ca/input.euro.xml | 1 - .../data_sets/input/ca/input.polish.xml | 1 - .../data_sets/input/ca/input_118.xml | 1 - .../data_sets/input/ca/input_14.xml | 1 - .../data_sets/input/ca/input_14_auto_n1.xml | 1 - .../input/ca/input_14_filters_example.xml | 10 +- .../data_sets/input/ca/input_14_qlim.xml | 1 - 11 files changed, 289 insertions(+), 86 deletions(-) diff --git a/docs/user_manual/sphinx/Section9-ApplicationModules.rst b/docs/user_manual/sphinx/Section9-ApplicationModules.rst index fef346f5..3419d602 100644 --- a/docs/user_manual/sphinx/Section9-ApplicationModules.rst +++ b/docs/user_manual/sphinx/Section9-ApplicationModules.rst @@ -572,8 +572,8 @@ Input options +----------------------------+--------+--------------------------------------------------------------+ | ``FullGeneratorN1`` | false | Auto-generate N-1 over every in-service generator. | +----------------------------+--------+--------------------------------------------------------------+ -| ``groupSize`` | 1 | MPI processes per contingency. Power flow scales poorly so | -| | | leave at 1 and add ranks to widen task parallelism. | +| ``groupSize`` | 1 | Deprecated; ignored (forced to 1). An outaged branch can | +| | | straddle a multi-rank partition. Add ranks for parallelism. | +----------------------------+--------+--------------------------------------------------------------+ | ``minVoltage`` | 0.9 | Lower voltage limit (pu) for violation checks. | +----------------------------+--------+--------------------------------------------------------------+ diff --git a/src/applications/components/pf_matrix/pf_components.cpp b/src/applications/components/pf_matrix/pf_components.cpp index 3ad04f35..05e0026f 100755 --- a/src/applications/components/pf_matrix/pf_components.cpp +++ b/src/applications/components/pf_matrix/pf_components.cpp @@ -1837,6 +1837,8 @@ bool gridpack::powerflow::PFBus::serialWrite(char *string, const int bufsize, sprintf(string, "%6d %20.12e %20.12e %d %d\n", getOriginalIndex(),0.0,0.0,use_vmag,changed); } else if (!strcmp(signal,"ca")) { + // Match checkVoltageViolations(): skip ignored buses. + if (p_ignore) return false; double pi = 4.0*atan(1.0); double angle = p_a*180.0/pi; bool found = false; @@ -3734,6 +3736,8 @@ bool gridpack::powerflow::PFBranch::serialWrite(char *string, const int bufsize, bool found = false; int ilen = 0; for (i=0; i(p_ignore.size()) && p_ignore[i]) continue; s = getComplexPower(tags[i]); double p = real(s); double q = imag(s); diff --git a/src/applications/contingency_analysis/README.md b/src/applications/contingency_analysis/README.md index cdfe7edd..61adbd36 100644 --- a/src/applications/contingency_analysis/README.md +++ b/src/applications/contingency_analysis/README.md @@ -35,7 +35,7 @@ When combined, duplicates from the file are automatically skipped. | Option | Description | Default | |--------|-------------|---------| -| `groupSize` | Number of MPI processes per contingency (parallelization) | 1 | +| `groupSize` | Deprecated; ignored. Each contingency runs on one rank (a group larger than one lets an outaged branch straddle the partition and fails the solve). Add MPI ranks to run more contingencies at once | 1 (forced) | | `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 | @@ -44,17 +44,44 @@ When combined, duplicates from the file are automatically skipped. | `outputFile` | Base name for output files | `ca_results` | | `writeStats` | Emit StatBlock summary files (vmag.txt etc.). Set false to skip and avoid the per-case StatBlock work | true | | `contingencyRating` | Loading% denominator across every CA output: `A`, `B`, or `C` with A→B→C fallback. Default `A` matches PW / PSS/E ACCC. `base_rate_mva` always uses rate-A | `A` | -| `monitorBranchesFile` | Path to a CSV allowlist (`from_bus,to_bus,ckt`). When set, overrides the area/kV gates (TARA / PSS/E convention) | (unset) | -| `monitorAreas` | Space-separated list of PSS/E area numbers. Branch is emitted if **either endpoint** is in the set | (unset) | -| `monitorKvMin` | Lower kV threshold; branch passes if `max(kv_from, kv_to) >= monitorKvMin` | 0 (unbounded) | -| `monitorKvMax` | Upper kV threshold; branch passes if `max(kv_from, kv_to) <= monitorKvMax` | 0 (unbounded) | - -### Filtering csv_flat / csv_delta output - -`csv_flat` and `csv_delta` emit per-(contingency, branch) rows. All filters -are optional — unset means "monitor everything". `monitorBranchesFile` is -authoritative when set; otherwise `monitorAreas` and the kV bounds AND -together. +| `monitorBranchesFile` | Path to a CSV allowlist (`from_bus,to_bus,ckt`). When set, overrides the area/kV gates (TARA / PSS/E convention). Applies to every output format | (unset) | +| `monitorAreas` | Space-separated list of PSS/E area numbers. A branch is monitored if **either endpoint** is in the set; a bus if its own area is. Applies to every output format | (unset) | +| `monitorKvMin` | Lower kV threshold; branch passes if `max(kv_from, kv_to) >= monitorKvMin`, bus if its base kV does. Applies to every output format | 0 (unbounded) | +| `monitorKvMax` | Upper kV threshold; branch passes if `max(kv_from, kv_to) <= monitorKvMax`, bus if its base kV does. Applies to every output format | 0 (unbounded) | + +### Monitor filters (all output formats) + +The monitor filters select which **buses and branch elements are reported**; +they never change which contingencies are simulated or how the network is +solved. All filters are optional — unset means "monitor everything". +`monitorBranchesFile` is authoritative when set; otherwise `monitorAreas` and +the kV bounds AND together. + +The same monitored set is used by every output, regardless of `outputFormat`: + +| Output | What the filter does | +|---|---| +| `.out` per-contingency text files (`printCalcFiles=true`) | Bus / branch violation listings and the "No violation" verdict cover monitored elements only | +| `_violations.csv` | Branch and voltage rows for monitored elements only | +| `json` / `csv` (`_buses.csv`, `_branches.csv`, `_generators.csv`, `.json`) | Base-case and contingency rows for monitored buses, branches and generators on monitored buses | +| `csv_flat` / `csv_delta` | One row per (contingency, monitored branch) | +| StatBlock `.txt` files (`vmag.txt`, `pflow.txt`, ...) | Rows for monitored buses / branches / generators only | +| `_summary.json` | Violation counters, worst-of values, performance indices and rosters accrue over monitored elements only | +| `_buses.csv` metadata sidecar (csv_flat / csv_delta), `_contingencies.csv`, `_convergence.csv` | Not filtered: these are lookup / bookkeeping tables and stay complete | + +The bus rule mirrors the branch rule: with an allowlist a bus is monitored when +it is an endpoint of an allowlisted branch; with area / kV gates it is monitored +when its own area is in `monitorAreas` and its own base kV lies inside the kV +bounds. Non-monitored elements are flagged "ignore" on the network after the +base-case solve, which is the same mechanism the driver already uses to exclude +buses that violate limits in the base case from the contingency checks. + +At startup the driver prints the effective filter and a count such as +`Monitor filter: 20 of 179 branch elements and 10 of 118 buses monitored`. +A filter that matches nothing produces a warning and empty outputs; check the +area numbers and kV levels against the case (legacy v23 RAW files often carry +area 1 and base kV 0 for every bus, so area / kV gates cannot select anything +there). ```xml @@ -207,6 +234,18 @@ they're omitted from `_delta.csv` / `_flat.csv`. When monitor filters are active, the data-row count of `_delta.csv` / `_flat.csv` equals `|monitored branches| × |converged contingencies|`. +### `_contingencies.csv` sidecar (every `outputFormat`) + +Lookup table decoding the `event_idx` used by every other output. Columns: +`event_idx, contingency, type, n_elements, from_bus, to_bus, circuit_id, +gen_bus, gen_id`. One row per contingency; `event_idx=0` is the base case +(`n_elements=0`, id columns blank). An N-1 row carries the single outaged +element in the id columns, so it joins directly against the other CSVs. A +multi-element (N-k) event stays on one row with `n_elements=k` and the +element ids `;`-separated inside the same columns, e.g. `from_bus=1;3`, +`to_bus=2;4`, `circuit_id=1;1`. Generator events fill `gen_bus`/`gen_id` +and leave the branch columns blank. + --- ### Aggregated `.txt` outputs (`writeStats=true`, default) diff --git a/src/applications/contingency_analysis/ca_driver.cpp b/src/applications/contingency_analysis/ca_driver.cpp index e42b4233..c38349db 100644 --- a/src/applications/contingency_analysis/ca_driver.cpp +++ b/src/applications/contingency_analysis/ca_driver.cpp @@ -17,7 +17,7 @@ * * @updated Yousu Chen * - csv_flat / csv_delta per-(contingency,branch) outputs - * - monitorBranchesFile / monitorAreas / monitorKvMin/Max filters + * - monitorBranchesFile / monitorAreas / monitorKvMin/Max filters (all formats) * @date 2026-06-21 * * @brief Driver for contingency analysis calculation that make use of the @@ -398,9 +398,19 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) util.toLower(tmp_bool); write_stats = (tmp_bool != "false"); } + // groupSize is forced to 1: an outaged branch may straddle a multi-rank + // partition. Scale by adding ranks, not by widening a group. if (!cursor->get("groupSize",&grp_size)) { grp_size = 1; } + if (grp_size != 1) { + if (world.rank() == 0) { + printf("WARNING: groupSize=%d is not supported (a contingency branch " + "may span the partition boundary); using groupSize=1\n", + grp_size); + } + grp_size = 1; + } if (!cursor->get("minVoltage",&Vmin)) { Vmin = 0.9; } @@ -444,6 +454,10 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) if (!tok[i].empty()) monitorAreas.insert(atoi(tok[i].c_str())); } } + // Any monitor filter configured; gates every output format. + bool haveMonitorFilter = !monitorAreas.empty() || + monitorKvMin > 0.0 || monitorKvMax > 0.0 || + !monitorBranchesFile.empty(); // Loading% denominator for all CA outputs (.out, _violations.csv, JSON, // csv_flat, csv_delta). A|B|C, default A to match PW/PSSE convention. // A->B->C fallback if the requested tier is zero/missing. @@ -556,6 +570,45 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) bus_meta[orig] = m; } } + // (area, base kV) per bus, all-gathered over task_comm so filter lookups + // work on gathered rows. + struct BusAreaKv { int area; double basekv; }; + std::map bus_ak; + if (wantBusSidecar || haveMonitorFilter) { + std::vector lid, larea; + std::vector lkv; + int nBus = pf_network->numBuses(); + for (int i = 0; i < nBus; i++) { + if (!pf_network->getActiveBus(i)) continue; + gridpack::powerflow::PFBus *bus = + dynamic_cast(pf_network->getBus(i).get()); + if (!bus) continue; + lid.push_back(pf_network->getOriginalBusIndex(i)); + larea.push_back(bus->getArea()); + lkv.push_back(bus->getBaseKV()); + } + MPI_Comm tc = static_cast(task_comm); + int tsize = task_comm.size(); + int nloc = static_cast(lid.size()); + std::vector counts(tsize, 0), displs(tsize, 0); + MPI_Allgather(&nloc, 1, MPI_INT, &counts[0], 1, MPI_INT, tc); + int tot = 0; + for (int p = 0; p < tsize; p++) { displs[p] = tot; tot += counts[p]; } + std::vector gid(tot > 0 ? tot : 1), garea(tot > 0 ? tot : 1); + std::vector gkv(tot > 0 ? tot : 1); + int *lid_p = nloc > 0 ? &lid[0] : NULL; + int *larea_p = nloc > 0 ? &larea[0] : NULL; + double *lkv_p = nloc > 0 ? &lkv[0] : NULL; + MPI_Allgatherv(lid_p, nloc, MPI_INT, &gid[0], &counts[0], &displs[0], MPI_INT, tc); + MPI_Allgatherv(larea_p, nloc, MPI_INT, &garea[0], &counts[0], &displs[0], MPI_INT, tc); + MPI_Allgatherv(lkv_p, nloc, MPI_DOUBLE, &gkv[0], &counts[0], &displs[0], MPI_DOUBLE, tc); + for (int i = 0; i < tot; i++) { + BusAreaKv a; + a.area = garea[i]; + a.basekv = gkv[i]; + bus_ak[gid[i]] = a; + } + } // Strip surrounding single quotes (PSS/E style) and outer whitespace. auto trim_quoted = [](const std::string &in) -> std::string { @@ -820,8 +873,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Monitor allowlist parsed from monitorBranchesFile. Empty -> emit all. std::set monitorSet; - if (!monitorBranchesFile.empty() && - (outputFormat == "csv_flat" || outputFormat == "csv_delta")) { + if (!monitorBranchesFile.empty()) { std::ifstream fin(monitorBranchesFile.c_str()); if (!fin.is_open()) { if (world.rank() == 0) { @@ -880,9 +932,13 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } } } - auto isMonitored = [&](const BranchKey &k) { - return monitorSet.empty() || monitorSet.find(k) != monitorSet.end(); - }; + // Endpoints of allowlisted branches; gates voltage rows under an allowlist. + std::set monitorBusSet; + for (std::set::const_iterator it = monitorSet.begin(); + it != monitorSet.end(); ++it) { + monitorBusSet.insert(it->from); + monitorBusSet.insert(it->to); + } // Area/kV gate. Either-endpoint match for areas (catches tie-lines). // kV is gated on max(kv_from, kv_to) so a 138/13.8 stepdown counts as 138. // Empty area set / zero kV bound = unrestricted on that dimension. @@ -922,6 +978,43 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) monitorKvMin, monitorKvMax); } } + auto busAreaKv = [&](int bus, int &area, double &kv) { + std::map::const_iterator it = bus_ak.find(bus); + area = (it != bus_ak.end()) ? it->second.area : 0; + kv = (it != bus_ak.end()) ? it->second.basekv : 0.0; + }; + // Branch monitor predicate shared by every output path (ckt padding trimmed). + auto branchMonitored = [&](int from, int to, const std::string &ckt) -> bool { + if (!monitorSet.empty()) { + BranchKey k; + k.from = from; k.to = to; k.ckt = ckt; + while (!k.ckt.empty() && (k.ckt[k.ckt.size()-1] == ' ' || + k.ckt[k.ckt.size()-1] == '\t')) + k.ckt.resize(k.ckt.size()-1); + return monitorSet.find(k) != monitorSet.end(); + } + if (!haveAreaKvFilter) return true; + int af = 0, at = 0; + double kf = 0.0, kt = 0.0; + busAreaKv(from, af, kf); + busAreaKv(to, at, kt); + return passesAreaKv(af, at, kf, kt); + }; + // Bus counterpart: allowlist endpoint, else own area / base kV. + auto busMonitored = [&](int bus) -> bool { + if (!monitorSet.empty()) { + return monitorBusSet.find(bus) != monitorBusSet.end(); + } + if (!haveAreaKvFilter) return true; + int area = 0; + double kv = 0.0; + busAreaKv(bus, area, kv); + if (!monitorAreas.empty() && monitorAreas.find(area) == monitorAreas.end()) + return false; + if (monitorKvMin > 0.0 && kv < monitorKvMin) return false; + if (monitorKvMax > 0.0 && kv > monitorKvMax) return false; + return true; + }; struct BaseFlow { double p_mw, q_mvar, mva, loading_pct; @@ -978,11 +1071,17 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) char ct_name[24]; std::strncpy(ct_name, name.c_str(), sizeof(ct_name) - 1); ct_name[sizeof(ct_name) - 1] = '\0'; - // Accrue voltage PI on every energized bus in contingency rows only. + // Voltage PI and violations on monitored buses, contingency rows only. if (!is_base) { for (std::map >::const_iterator vit = vbymag_ang.begin(); vit != vbymag_ang.end(); ++vit) { - accumVoltagePi(ct_name, vit->second.first); + if (!busMonitored(vit->first)) continue; + double v_pu = vit->second.first; + accumVoltagePi(ct_name, v_pu); + if (v_pu <= 0.0 || !std::isfinite(v_pu)) continue; + if (v_pu < Vmin || v_pu > Vmax) { + emitVoltageViolation(event_idx, ct_name, vit->first, v_pu, Vmin, Vmax); + } } } for (size_t bi = 0; bi < b_strs.size(); bi++) { @@ -1001,16 +1100,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) mk.from = from; mk.to = to; mk.ckt = ckt; while (!mk.ckt.empty() && mk.ckt[mk.ckt.size()-1] == ' ') mk.ckt.resize(mk.ckt.size()-1); - if (!monitorSet.empty() && monitorSet.find(mk) == monitorSet.end()) continue; - if (haveAreaKvFilter) { - std::map::const_iterator mf = bus_meta.find(from); - std::map::const_iterator mt = bus_meta.find(to); - int af = (mf != bus_meta.end()) ? mf->second.area : 0; - int at = (mt != bus_meta.end()) ? mt->second.area : 0; - double kf = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - double kt = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; - if (!passesAreaKv(af, at, kf, kt)) continue; - } + if (!branchMonitored(from, to, mk.ckt)) continue; std::map::const_iterator rIt = branch_rates.find(mk); double rate_sel = ratea; if (rIt != branch_rates.end()) { @@ -1086,16 +1176,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Strip trailing spaces from ckt so the key matches what flow_str // returns later (sscanf %15s already trims leading whitespace). while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); - if (!isMonitored(k)) continue; - if (haveAreaKvFilter) { - std::map::const_iterator mf = bus_meta.find(from); - std::map::const_iterator mt = bus_meta.find(to); - int af = (mf != bus_meta.end()) ? mf->second.area : 0; - int at = (mt != bus_meta.end()) ? mt->second.area : 0; - double kf = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - double kt = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; - if (!passesAreaKv(af, at, kf, kt)) continue; - } + if (!branchMonitored(from, to, k.ckt)) continue; double base_rate = ratea, cont_rate = ratea; std::map::const_iterator rIt = branch_rates.find(k); if (rIt != branch_rates.end()) { @@ -1117,12 +1198,8 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) bf.ang_from_deg = (vf != vbymag_ang.end()) ? vf->second.second : 0.0; bf.v_to_pu = (vt != vbymag_ang.end()) ? vt->second.first : 0.0; bf.ang_to_deg = (vt != vbymag_ang.end()) ? vt->second.second : 0.0; - std::map::const_iterator mf = bus_meta.find(from); - std::map::const_iterator mt = bus_meta.find(to); - bf.base_kv_from = (mf != bus_meta.end()) ? mf->second.basekv : 0.0; - bf.base_kv_to = (mt != bus_meta.end()) ? mt->second.basekv : 0.0; - bf.area_from = (mf != bus_meta.end()) ? mf->second.area : 0; - bf.area_to = (mt != bus_meta.end()) ? mt->second.area : 0; + busAreaKv(from, bf.area_from, bf.base_kv_from); + busAreaKv(to, bf.area_to, bf.base_kv_to); base_cache[k] = bf; } }; @@ -1196,10 +1273,16 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) vbymag_ang[bus_id] = std::make_pair(vmag, angle); } } - // Accrue voltage PI on every energized bus in this contingency. + // Voltage PI and violations on monitored buses. for (std::map >::const_iterator vit = vbymag_ang.begin(); vit != vbymag_ang.end(); ++vit) { - accumVoltagePi(ct_name, vit->second.first); + if (!busMonitored(vit->first)) continue; + double v_pu = vit->second.first; + accumVoltagePi(ct_name, v_pu); + if (v_pu <= 0.0 || !std::isfinite(v_pu)) continue; + if (v_pu < Vmin || v_pu > Vmax) { + emitVoltageViolation(event_idx, ct_name, vit->first, v_pu, Vmin, Vmax); + } } for (size_t bi = 0; bi < b_strs.size(); bi++) { char ckt_buf[16] = {0}; @@ -1216,7 +1299,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) k.ckt = std::string(ckt_buf); while (!k.ckt.empty() && k.ckt[k.ckt.size()-1] == ' ') k.ckt.resize(k.ckt.size()-1); - if (!isMonitored(k)) continue; + if (!branchMonitored(from, to, k.ckt)) continue; std::map::const_iterator it = base_cache.find(k); if (it == base_cache.end()) { deltaSkipCount++; continue; } const BaseFlow &bf = it->second; @@ -1314,12 +1397,75 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // Suppress voltage violations already present at base. pf_app.ignoreVoltageViolations(); + // Flag non-monitored elements "ignore" so the violation checks and .out + // listings follow the filter. + if (haveMonitorFilter) { + long cnt[4] = { 0, 0, 0, 0 }; // mon buses, buses, mon elems, elems + int nBus = pf_network->numBuses(); + for (int i = 0; i < nBus; i++) { + gridpack::powerflow::PFBus *bus = + dynamic_cast(pf_network->getBus(i).get()); + if (!bus) continue; + bool mon = busMonitored(pf_network->getOriginalBusIndex(i)); + if (!mon) bus->setIgnore(true); + if (pf_network->getActiveBus(i)) { cnt[1]++; if (mon) cnt[0]++; } + } + int nBranch = pf_network->numBranches(); + for (int i = 0; i < nBranch; i++) { + gridpack::powerflow::PFBranch *br = + dynamic_cast(pf_network->getBranch(i).get()); + if (!br) continue; + int from = br->getBus1OriginalIndex(); + int to = br->getBus2OriginalIndex(); + std::vector tags = br->getLineTags(); + bool active = pf_network->getActiveBranch(i); + for (size_t t = 0; t < tags.size(); t++) { + bool mon = branchMonitored(from, to, tags[t]); + if (!mon) br->setIgnore(tags[t], true); + if (active) { cnt[3]++; if (mon) cnt[2]++; } + } + } + long tot[4] = { 0, 0, 0, 0 }; + MPI_Allreduce(cnt, tot, 4, MPI_LONG, MPI_SUM, + static_cast(task_comm)); + if (world.rank() == 0) { + printf("Monitor filter: %ld of %ld branch elements and %ld of %ld buses " + "monitored\n", tot[2], tot[3], tot[0], tot[1]); + if (tot[2] == 0 && tot[0] == 0) { + printf("WARNING: monitor filter matches nothing; all outputs will be " + "empty. Check monitorAreas/monitorKvMin/monitorKvMax/" + "monitorBranchesFile against the case.\n"); + } + } + } + // Drop non-monitored elements from a collected result set. + auto filterResults = [&](gridpack::utility::PowerFlowResults &r) { + if (!haveMonitorFilter) return; + std::vector buses; + for (size_t i = 0; i < r.buses.size(); i++) { + if (busMonitored(r.buses[i].busId)) buses.push_back(r.buses[i]); + } + r.buses.swap(buses); + std::vector branches; + for (size_t i = 0; i < r.branches.size(); i++) { + const gridpack::utility::BranchResult &b = r.branches[i]; + if (branchMonitored(b.fromBus, b.toBus, b.circuitId)) branches.push_back(b); + } + r.branches.swap(branches); + std::vector gens; + for (size_t i = 0; i < r.generators.size(); i++) { + if (busMonitored(r.generators[i].busId)) gens.push_back(r.generators[i]); + } + r.generators.swap(gens); + }; + // Collect base case results for export. csv_flat captures rows directly // in the hot loop and skips the heavyweight collectResults() path. gridpack::utility::PowerFlowResults baseCaseResults; if (outputFormat == "json" || outputFormat == "csv" || outputFormat == "text") { baseCaseResults = pf_app.collectResults(); + filterResults(baseCaseResults); } if (outputFormat == "csv_flat") { // The base case is replicated on every task communicator. captureFlatRows @@ -1449,48 +1595,48 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) printf("==================================================================\n\n"); } - // Decodes the event_idx used by every other CA output. One row per outaged - // element, so an N-2 event shares an event_idx; 0 is the base case. + // event_idx lookup table: one row per contingency (0 = base case); N-k + // element ids are ';'-separated within the columns. if (world.rank() == 0) { std::string ctgFile = outputFile + "_contingencies.csv"; std::ofstream cout_ctg(ctgFile.c_str(), std::ios::out | std::ios::trunc); - cout_ctg << "event_idx,contingency,type,n_elements,element_seq," + cout_ctg << "event_idx,contingency,type,n_elements," "from_bus,to_bus,circuit_id,gen_bus,gen_id\n"; size_t ctgRows = 0; - cout_ctg << "0,base_case,base,0,0,,,,,\n"; + cout_ctg << "0,base_case,base,0,,,,,\n"; ctgRows++; + // Trim clean2Char padding so ids join against the other CSVs. + auto rtrim = [](const std::string &in) -> std::string { + std::string t = in; + while (!t.empty() && (t[t.size()-1] == ' ' || t[t.size()-1] == '\t')) + t.resize(t.size()-1); + return t; + }; for (size_t ei = 0; ei < events.size(); ei++) { const gridpack::powerflow::Contingency &e = events[ei]; int event_idx = static_cast(ei) + 1; - // Trim clean2Char padding so ids join against the other CSVs. - auto rtrim = [](const std::string &in) -> std::string { - std::string t = in; - while (!t.empty() && (t[t.size()-1] == ' ' || t[t.size()-1] == '\t')) - t.resize(t.size()-1); - return t; - }; std::string nm = rtrim(e.p_name); size_t n = 0; const char *ty = "unknown"; if (e.p_type == Branch) { n = e.p_from.size(); ty = "branch"; } else if (e.p_type == Generator) { n = e.p_busid.size(); ty = "generator"; } - // An event with no elements (e.g. a malformed list entry) still gets a - // row, so no event_idx in the other files is left undecodable. - if (n == 0) { - cout_ctg << event_idx << "," << nm << "," << ty << ",0,0,,,,,\n"; - ctgRows++; - } + std::ostringstream c_from, c_to, c_ckt, c_gbus, c_gid; for (size_t j = 0; j < n; j++) { - cout_ctg << event_idx << "," << nm << "," << ty << "," - << n << "," << (j + 1) << ","; - if (e.p_type == Branch) - cout_ctg << e.p_from[j] << "," << e.p_to[j] << "," - << rtrim(e.p_ckt[j]) << ",,\n"; - else - cout_ctg << ",,," << e.p_busid[j] << "," - << rtrim(e.p_genid[j]) << "\n"; - ctgRows++; + const char *sep = (j > 0) ? ";" : ""; + if (e.p_type == Branch) { + c_from << sep << e.p_from[j]; + c_to << sep << e.p_to[j]; + c_ckt << sep << rtrim(e.p_ckt[j]); + } else { + c_gbus << sep << e.p_busid[j]; + c_gid << sep << rtrim(e.p_genid[j]); + } } + // Empty events still get a row so every event_idx decodes. + cout_ctg << event_idx << "," << nm << "," << ty << "," << n << "," + << c_from.str() << "," << c_to.str() << "," << c_ckt.str() << "," + << c_gbus.str() << "," << c_gid.str() << "\n"; + ctgRows++; } cout_ctg.close(); printf("[contingencies] wrote %zu rows to %s\n", ctgRows, ctgFile.c_str()); @@ -1557,6 +1703,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) // and angle for base case for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + if (!busMonitored(atoi(tokens[0].c_str()))) continue; int not_isolated = atoi(tokens[3].c_str()); if (not_isolated == 1) { mag_ids.push_back(atoi(tokens[0].c_str())); @@ -1602,6 +1749,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } int ngen = tokens.size()/4; for (j=0; j(task_id) + 1); if (outputFormat != "text") localContingencies.push_back(ctResult); } @@ -1943,6 +2095,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) nsize = v_vals.size(); for (i=0; i tokens = util.blankTokenizer(v_vals[i]); + if (!busMonitored(atoi(tokens[0].c_str()))) continue; int not_isolated = atoi(tokens[3].c_str()); if (not_isolated == 1) { vmag.push_back(atof(tokens[2].c_str())); @@ -1973,6 +2126,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } int ngen = tokens.size()/4; for (j=0; j tokens = util.blankTokenizer(v_vals[i]); + if (!busMonitored(atoi(tokens[0].c_str()))) continue; int not_isolated = atoi(tokens[3].c_str()); if (not_isolated == 1) { vmag.push_back(0.0); @@ -2088,6 +2246,7 @@ void gridpack::contingency_analysis::CADriver::execute(int argc, char** argv) } int ngen = tokens.size()/4; for (j=0; j true contingencies_euro.xml - 1 1.1 0.9 false diff --git a/src/applications/data_sets/input/ca/input.polish.xml b/src/applications/data_sets/input/ca/input.polish.xml index 36c1ef78..c867bcad 100644 --- a/src/applications/data_sets/input/ca/input.polish.xml +++ b/src/applications/data_sets/input/ca/input.polish.xml @@ -3,7 +3,6 @@ true contingencies_polish.xml - 1 1.1 0.9 false diff --git a/src/applications/data_sets/input/ca/input_118.xml b/src/applications/data_sets/input/ca/input_118.xml index 33ec77be..80507856 100644 --- a/src/applications/data_sets/input/ca/input_118.xml +++ b/src/applications/data_sets/input/ca/input_118.xml @@ -8,7 +8,6 @@ - 1 1.1 0.9 false diff --git a/src/applications/data_sets/input/ca/input_14.xml b/src/applications/data_sets/input/ca/input_14.xml index 92e816e8..c347607e 100644 --- a/src/applications/data_sets/input/ca/input_14.xml +++ b/src/applications/data_sets/input/ca/input_14.xml @@ -8,7 +8,6 @@ true true - 1 1.1 0.9 true 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 index 65577b44..a45148d7 100644 --- a/src/applications/data_sets/input/ca/input_14_auto_n1.xml +++ b/src/applications/data_sets/input/ca/input_14_auto_n1.xml @@ -5,7 +5,6 @@ false true true - 1 1.1 0.9 false diff --git a/src/applications/data_sets/input/ca/input_14_filters_example.xml b/src/applications/data_sets/input/ca/input_14_filters_example.xml index 70a34ec6..b1ce7bfd 100644 --- a/src/applications/data_sets/input/ca/input_14_filters_example.xml +++ b/src/applications/data_sets/input/ca/input_14_filters_example.xml @@ -4,7 +4,10 @@ and contingencyRating. Uses the IEEE 14-bus case. AS WRITTEN this file demonstrates the DEFAULT "monitor everything" mode - (all filter options are commented out) + (all filter options are commented out). Note that IEEE14_ca.raw is a + legacy-format case where every bus is in area 1 with base kV 0, so the + area / kV examples below are illustrative only; use monitorBranchesFile + with this case, or a v33 case with real area / kV data for area/kV gates. To narrow output, uncomment ONE of the EXAMPLE blocks below: - monitorBranchesFile -> curated allowlist; wins over area/kV when set @@ -19,7 +22,6 @@ false true true - 1 1.1 0.9 true @@ -28,7 +30,9 @@ + The monitor filters below apply to every outputFormat (text .out + files, json, csv, csv_flat, csv_delta, _violations.csv, StatBlock + .txt files and _summary.json). --> csv_delta ca_IEEE14 diff --git a/src/applications/data_sets/input/ca/input_14_qlim.xml b/src/applications/data_sets/input/ca/input_14_qlim.xml index 52d38cdc..9f3f1157 100644 --- a/src/applications/data_sets/input/ca/input_14_qlim.xml +++ b/src/applications/data_sets/input/ca/input_14_qlim.xml @@ -4,7 +4,6 @@ false contingencies_14.xml - 1 1.1 0.9 true