From f0e7ed6f88ce2b470b0bdf334c1781e5120d413e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 17 Sep 2025 15:55:30 -0600 Subject: [PATCH 1/4] Added 3 new tools to PSLF that allow the LLM to add a bus, a transmission line, and read the voltage from the case. --- PSLF/pslf_mcp.py | 205 +++++++++++++++++++++++++++++++++++++++++++++-- psec-1.sav | Bin 44128 -> 0 bytes 2 files changed, 197 insertions(+), 8 deletions(-) delete mode 100644 psec-1.sav diff --git a/PSLF/pslf_mcp.py b/PSLF/pslf_mcp.py index df84b35..a811c46 100644 --- a/PSLF/pslf_mcp.py +++ b/PSLF/pslf_mcp.py @@ -9,7 +9,7 @@ mcp = FastMCP("PSLF Positive Sequence Load Flow Program") from PSLF_PYTHON import * -init_pslf(silent=True) +init_pslf(silent=False) @power_mcp_tool(mcp) def open_case(case: str) -> Dict[str, Any]: @@ -58,7 +58,7 @@ def solve_case() -> Dict[str, Any]: Solves a powerflow case using PSLF. Returns: - Dict with status and case information + Dict with status """ try: @@ -105,24 +105,213 @@ def solve_case() -> Dict[str, Any]: ) @power_mcp_tool(mcp) -def area_report() -> Dict[str, Any]: +def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, Any]: """ - Prints area totals and interchanges to the terminal. + Add a new bus to the power system model + + Args: + busnum: A unique identifying integer used as the primary key in the bus database. Is not necessarily consecutive and could be larger than the number of buses in the case. + busname: A human readable name of the bus no longer than 12 characters in length. + nominalkv: The nominal voltage of the bus in kilovolts. + type: An integer flag where 0 = system slack bus, 1 = a load bus, and 2 = a generator bus. (default 1) + + Returns: + Dict with status """ try: - Pslf.area_report() + iret = Pslf.add_record(1, 0, busnum, 'type basekv busnam', str(type) + " " + str(nominalkv) + " " + busname) + cp = CaseParameters() - return { - 'status': 'success' - } + # Get basic case information + bus_data = cp.Nbus + branch_data = cp.Nbrsec + gen_data = cp.Ngen + + if (iret == 0): + return { + 'status': 'success', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 1): + return { + 'status': 'error insufficient input', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 2): + return { + 'status': 'error bus number not found when using combination of bus name and voltage', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 3): + return { + 'status': 'error bus number must be positive', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == -2): + return { + 'status': 'error bus voltage must be positive', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + else: + return { + 'status': 'error bus type is out of range', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } except Exception as e: return PowerError( status='error unknown', message=str(e) ) + +@power_mcp_tool(mcp) +def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: float, reactance: float, susceptance: float, rating: float) -> Dict[str, Any]: + """ + Add a new transmission line to the power system model + Args: + frombus: Integer identifier of the transmission lines starting terminal Is not necessarily consecutive and could be larger than the number of buses in the case. + tobus: Integer identifier of the transmission lines ending terminal Is not necessarily consecutive and could be larger than the number of buses in the case. + circuit: Integer identifer uniquely identifying the circuit in the case there are multiple circuits. (default 1) + resistance: A float representing the real component of impedance of the transmission line in per unit. (default 0.0) + reactance: A float representing the imaginary component of impedance of the transmission line in per unit. + susceptance: A float representing the per unit susceptance of the transmission line. (default 0.0) + rating: The maximum safe rating of the transmission line in MVA (default 9999.0) + + Returns: + Dict with status + """ + try: + + iret = Pslf.add_record(1, 1, str(frombus) + " " + str(tobus) + " " + str(circuit) + " 1", "st zsecr zsecx bsec rate[0]", "1 " + str(resistance) + " " + str(reactance) + " " + str(susceptance) + " " + str(rating)) + + cp = CaseParameters() + + # Get basic case information + bus_data = cp.Nbus + branch_data = cp.Nbrsec + gen_data = cp.Ngen + + if (iret == 0): + return { + 'status': 'success', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 1): + return { + 'status': 'error insufficient input', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 2): + return { + 'status': 'error branch bus out of range', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 3): + return { + 'status': 'error branch bus does not exist', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + elif (iret == 4): + return { + 'status': 'error branch from and to bus are identical', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + else: + return { + 'status': 'error unknown', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0 + } + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) +@power_mcp_tool(mcp) +def get_voltage(bus: int) -> Dict[str, Any]: + """ + Queries the voltage of a bus and reports in per unit + + Args: + bus: Integer identifier of the desired bus to query voltage for. Is not necessarily consecutive and could be larger than the number of buses in the case. + + Returns: + Dict with status + """ + try: + + index = Pslf.bus_internal_index(bus) + + if (index < 0): + return { + 'status': 'error bus not found' + } + else: + return { + 'status': 'success', + 'case_info': { + 'bus_id': str(bus), + 'bus_name': str(Bus[index].Busnam), + 'base_kv': str(Bus[index].Basekv), + 'voltage_perunit_kv': str(Bus[index].Vm), + 'voltage_kv': str(Bus[index].Vm * Bus[index].Basekv), + 'voltage_angle_degrees': str(Bus[index].Va) + } + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) if __name__ == "__main__": mcp.run(transport="stdio") \ No newline at end of file diff --git a/psec-1.sav b/psec-1.sav deleted file mode 100644 index 0604eed27b188927be512b1929faf1559c9ad849..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44128 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 From f1a8fd33aa60e2470a54e18c4a16e1d0029471ce Mon Sep 17 00:00:00 2001 From: Jenkins Date: Thu, 2 Oct 2025 09:18:10 -0600 Subject: [PATCH 2/4] Add features to PSLF plug-in --- .gitignore | 28 +- PSLF/generate-otg.p | 1625 +++++++++++++++++++++++++++++++++++++++++++ PSLF/pslf_mcp.py | 604 +++++++++++++++- 3 files changed, 2230 insertions(+), 27 deletions(-) create mode 100644 PSLF/generate-otg.p diff --git a/.gitignore b/.gitignore index b6148a0..362c558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,28 @@ .venv/ -**/__pycache__/** \ No newline at end of file +**/__pycache__/** +soln.log +output.crf +output.sum +temp.wrk +cont.otg +contproc-run-status.csv +control.cntl +datacheck.txt +runs.cases +duplicate.txt +sstools.sav +term.log +case.pcrf +output.pcrf +Output.xlsx +run.bat +template.ctab +contproc-run-status_contingency_errors.csv +font-color +gediwork.drw +gediworkScan.drw +winter.sav +summer.drw +summer.sav +hs_err* +basecase.sav \ No newline at end of file diff --git a/PSLF/generate-otg.p b/PSLF/generate-otg.p new file mode 100644 index 0000000..5602eb0 --- /dev/null +++ b/PSLF/generate-otg.p @@ -0,0 +1,1625 @@ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** STANDARD EPCL PROGRAM SSTOOLS-OUTAGE-NEW FORMAT- VERSION 0002 ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** EPCL PROGRAM DESCRIPTION ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** THIS EPCL PROGRAM IS USED TO: ***/ +/*** ***/ +/*** CREATE A SIMPLIFIED CONTINGENCY LIST IN NEW FORMAT FOR USE WITH THE SSTOOLS ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** EPCL PROGRAM UPDATES ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** CREATED: S.Puchalapalli on 09/16/2016 ***/ +/*** Based on the exisiting "sstools-outage-v5.p" EPCL Program ***/ +/*** Can create bus, branch, tran, gen, load, shunts, SVD, and breaker type of events ***/ +/*** ***/ +/*** ***/ +/*** MODIFIED: +/*** VERSION: DATE: PROGRAMMER: CHANGES MADE: ***/ +/*** --------------- -------------- ------------- ----------------------------------------- ***/ +/*** V1 06/13/2017 S.Puchalapalli Increase the precision of base voltage to ***/ +/*** caputure all the buses ***/ +/*** Fix the issue with 3-winding transformer ***/ +/*** definitions ***/ +/*** 02/27/2018 S. Mahmood Divide the contingency list dialog in two to ***/ +/*** overcome blank panel inconsistency ***/ +/*** 08/26/2019 S.Mahmood Corrected busname writing option ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** EPCL PROGRAM METHODOLOGY ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** STEP 1: FOLLOW SSTOOLS MANUALS FOR PROCESS ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ +/*** ***/ +/*** EPCL PROGRAM CODE FOLLOWS: ***/ +/*** ***/ +/*************************************************************************************************/ +/*************************************************************************************************/ + + + + +/*************************************************************************************************/ +/* First Initialize All Program Variables And Set Default Program Limits */ +/*************************************************************************************************/ + +define MAXAREA 5000 +define MAXZONE 5000 +define MAXSECDD 120000 +define MAXTRAN 40000 +define MAXGEN 35000 +define MAXLOAD 75000 +define MAXBUS 100000 +define MAXSHUNT 80000 +define MAXBRKR 200000 + + +define NAME_LENGTH 12 + +dim *des[25][80], *inp[25][8], *title[1][40] +dim *descr[10][80], *inpu[10] + +dim *file[1][256], *condes[1][80], *longid[1][40], *objStr[1][256] +dim #mrk[4], #markar[MAXAREA], #arvmin[MAXAREA], #arvmax[MAXAREA] +dim #markzn[MAXZONE], #znvmin[MAXAREA], #znvmax[MAXAREA] + +dim #pksecdd[MAXSECDD], #pktran[MAXTRAN], #pkgen[MAXGEN], #pkload[MAXLOAD] +dim #pkbus[MAXBUS], #pkshunt[MAXSHUNT], #pkSVD[MAXSHUNT], #pkbrkr[MAXBRKR] +dim *redisp[1][80] + + +/*************************************************************************************************/ +/* EPCL PROGRAM TO BE ADDED AFTER EACH GENERATION OUTAGE (CHANGE THIS EPCL IF DESIRED) */ +/*************************************************************************************************/ + +*redisp[0] = "redispatch.p" + + +/*************************************************************************************************/ +/* SEND MESSAGE TO SCREEN THAT THE PROGRAM IS STARTINGING TO GENERATE A CONTINGENCY LIST */ +/*************************************************************************************************/ + +logterm("<< Running SSTOOLS-OUTAGE to Generate a Contingency List<") + + +/*************************************************************************************************/ +/* FIRST CHECK TO SEE IF A PSLF HISTORY CASE EXISTS IN MEMORY */ +/* - IF CASE EXISTS IN MEMORY THE USER HAS THE OPTION TO LOAD NEW OR USE EXISTING FILE */ +/* - IF CASE DOES NOT EXIST IN MEMORY, LOAD CASE */ +/*************************************************************************************************/ + +/*if(casepar[0].nbus < 1) + @newcase = 0 +else + @newcase = 1 + label newcase + *title[0] = "Load New/Use Current File" + *des[0] = "Load New (0) or Use Current (1) Case In Memory" + *inp[0] = "1" + logbuf(*des[1],"Current Case: ",filepar[0].getf[0]) + *inp[1] = "Click OK" + @ret = panel(*title[0],*des[0],*inp[0],2,1) + if(@ret < 0) + end + endif + @newcase = atof(*inp[0]) +endif*/ + +@newcase = 1 + +/*if(@newcase = 0) + logterm("<< Select a PSLF History Case File (*.sav) To Develop A Contingency List From<<") + @ret = pick("","*.sav","") + if(@ret < 0) + end + endif + @ret = getf(ret[0].string[0]) + if(@ret < 0) + logterm("*** ERROR Opening the PSLF History Case File - Terminating EPCL Program<<") + endif +elseif(@newcase = 1) + logterm(" Using Current PSLF History Case File In Memory<") + @ret = savf("temp.wrk") +endif*/ + +@ret = savf("temp.wrk") + +@dupBusNo = 0 +@dupBusNm = 0 + +gosub dupCheck +if( @dupBusNm = 0 ) + @busname = 1 /* DEFAULT IS TO USE BUS NAME CONVENTION */ +else + @busname = 0 +endif +/*************************************************************************************************/ +/* SECOND ASK USER TO DEFINE CONTINGENCY LIST FILE */ +/* - IF FILE ALREADY EXISTS, CHECK TO SEE IF USER WANTS TO OVERWRITE OR APPEND TO FILE */ +/* - IF FILE DOES NOT EXIST, CREATE FILE */ +/*************************************************************************************************/ + +label top +/*logterm("<< Select A Contingency List File To Write Into <<") +@ret = pick("","*.otg","") +if(@ret < 0) + logterm("*** ERROR Opening the Contingency List File - Terminating EPCL Program<<") + end +endif*/ + +*file[0] = "cont.otg" + +@ret = openlog(*file[0]) + + + +@append = 0 +@emsids = 0 +@busname = 0 + + + + +/*************************************************************************************************/ +/* LOOPING THROUGH PEAK SECDD, TRAN AND GEN ARRARYS AND NULLING THEM OUT WITH (-1) */ +/*************************************************************************************************/ + +for @i = 0 to MAXSECDD-1 + #pksecdd[@i] = -1 +next + +for @i = 0 to MAXTRAN-1 + #pktran[@i] = -1 +next + +for @i = 0 to MAXGEN-1 + #pkgen[@i] = -1 +next + +for @i = 0 to MAXBUS-1 + #pkbus[@i] = -1 +next + +for @i = 0 to MAXLOAD-1 + #pkload[@i] = -1 +next + +for @i = 0 to MAXSHUNT-1 + #pkshunt[@i] = -1 +next + +for @i = 0 to MAXSHUNT-1 + #pkSVD[@i] = -1 +next + +for @i = 0 to MAXBRKR-1 + #pkbrkr[@i] = -1 +next + + + + @eventTime = 1.000000 /* DEFAULT TIME FOR CONTINGENCY EVENT */ +@number = 1 /* DEFAULT STARTING CONTINGENCY NUMBER */ +$arzo = "area" /* DEFAULT IS USING AREA FOR LIST */ +#mrk[0] = 0 /* DEFAULT STARTING AREA/ZONE NUMBER */ +#mrk[1] = 999 /* DEFAULT ENDING AREA/ZONE NUMBER */ +#mrk[2] = -1 /* DEFAULT STARTING ZONE NUMBER WITHIN AREA RANGE ABOVE */ +#mrk[3] = -1 /* DEFAULT ENDING ZONE NUMBER WITHIN AREA RANGE ABOVE */ +@vstart = 100 /* DEFAULT LOWER VOLTAGE THRESHOLD */ +@vend = 999 /* DEFAULT UPPER VOLTAGE THRESHOLD */ +@genout = 1 /* DEFAULT IS TO ALLOW GENERATOR OUTAGES */ +@busout = 0 /* DEFAULT IS TO NOT ALLOW BUS OUTAGES */ +@loadout = 0 /* DEFAULT IS TO NOT ALLOW LOAD OUTAGES */ +@shuntout = 0 /* DEFAULT IS TO NOT ALLOW SHUNT OUTAGES */ +@SVDout = 0 /* DEFAULT IS TO NOT ALLOW SVD OUTAGES */ +@brkrout = 0 /* DEFAULT IS TO NOT ALLOW BREAKER OUTAGES */ +@stckbrkr = 0 /* DEFAULT IS TO NOT ALLOW STUCK BREAKER EVENTS */ +@redepcl = 0 /* DEFAULT IS SET TO NOT INCLUDE REDISPATCH EPCL */ +@smin = 100.0 /* DEFAULT IS 100 MVABASE MINIMUM */ + +@lineRfact = 1.0 /* Default ranking factor for Line outages */ +@tranRfact = 1.0 /* Default ranking factor for Transformer outages */ +@genRfact = 1.0 /* Default ranking factor for Generator outages */ +@otherRfact = 1.0 /* Default ranking factor for all other outages */ +@skip = 0 /* Default do no skip any contingency */ + + + if(#mrk[0] < 0) + logterm("*** ERROR: Range of Areas - Illegally Entered - Try Again!!!<") + endif + + if(#mrk[1] = 0) + #mrk[1] = #mrk[0] + endif + + if(#mrk[3] = 0 or #mrk[3]<0) + #mrk[3] = #mrk[2] + endif + + if(#mrk[2] >=0 and #mrk[3] >=0 and $arzo = "area") + @zfilter = 1 + endif + +/*************************************************************************************************/ +/* START SELECTION OF ELEMENTS: TAG ELEMENTS AND WILL WRITE OUT THE RECORDS UPON EXITING EPCL */ +/*************************************************************************************************/ + + if(@zfilter = 1) + logprint(*file[0],"# Contingency Selection Criteria: From ",$arzo, " ",#mrk[0]," to ",#mrk[1],"; ","zone", " ",#mrk[2]," to ",#mrk[3],"; ",@vstart," kV to ", @vend," kV <") + else + logprint(*file[0],"# Contingency Selection Criteria: From ",$arzo, " ",#mrk[0]," to ",#mrk[1],"; ",@vstart," kV to ", @vend," kV <") + endif + + /*********************************************************************************************/ + /* LOOP THROUGH SECDD (TRANSMISSION LINE IMPEDANCE) EDIT TABLE */ + /* - THREE CONDITIONS MUST BE MET IN ORDER TO ADD LINE INTO CONTINGENCY LIST */ + /* 1. THE ELEMENT, THE FROM BUS, OR THE TO BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /* 2. THE FROM BUS VOLTAGE MUST FALL WITHIN THE VOLTAGE RANGE SET */ + /* 3. THE LINE IMPEDANCE MUST BE GREATER THAN THE JUMPER IMPEDANCE THRESHOLD */ + /*********************************************************************************************/ + + for @i = 0 to casepar[0].nbrsec-1 + + if(#pksecdd[@i] > 0) /* DESIGNATES THAT LINE HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @from = secdd[@i].ifrom + @to = secdd[@i].ito + + if($arzo = "area") + @tt = secdd[@i].area + @t1 = busd[@from].area + @t2 = busd[@to].area + else + @tt = secdd[@i].zone + @t1 = busd[@from].zone + @t2 = busd[@to].zone + endif + + if(@zfilter = 1) + @z1 = secdd[@i].zone + @z2 = busd[@from].zone + @z3 = busd[@to].zone + endif + + @opt1 = 0 + @opt2 = 0 + @opt3 = 0 + @temp1 = 0 + @temp2 = 0 + @temp3 = 0 + + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + if(@t2 >= #mrk[0] and @t2 <= #mrk[1]) + @opt1 = 1 + @temp3 = 1 + endif + + if((busd[@from].basekv >= @vstart) and (busd[@from].basekv <= @vend)) + @opt2 = 1 + endif + + if(abs(secdd[@i].zsecx) > solpar[0].zeps) + @opt3 = 1 + endif + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + elseif(@temp3 and @z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp3) + if(@z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + + endif + + if(@opt1 and @opt2 and @opt3) + #pksecdd[@i] = 1 + endif + + next + + + /*********************************************************************************************/ + /* LOOP THROUGH TRAN (TRANSFORMER) EDIT TABLE */ + /* - TWO CONDITIONS MUST BE MET IN ORDER TO ADD TRANSFORMER INTO CONTINGENCY LIST */ + /* 1. THE ELEMENT, THE FROM BUS, OR THE TO BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /* 2. BOTH THE FROM AND TO BUS VOLTAGES MUST FALL WITHIN THE VOLTAGE RANGE SET */ + /*********************************************************************************************/ + + for @i = 0 to casepar[0].ntran-1 + + if(#pktran[@i] > 0) /* DESIGNATES THAT XFMR HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @from = tran[@i].ifrom + @to = tran[@i].ito + + if($arzo = "area") + @tt = tran[@i].area + @t1 = busd[@from].area + @t2 = busd[@to].area + else + @tt = tran[@i].zone + @t1 = busd[@from].zone + @t2 = busd[@to].zone + endif + + if(@zfilter = 1) + @z1 = tran[@i].zone + @z2 = busd[@from].zone + @z3 = busd[@to].zone + endif + + @opt1 = 0 + @opt2 = 0 + @temp1 = 0 + @temp2 = 0 + @temp3 = 0 + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + if(@t2 >= #mrk[0] and @t2 <= #mrk[1]) + @opt1 = 1 + @temp3 = 1 + endif + + if((busd[@from].basekv >= @vstart) and (busd[@from].basekv <= @vend)) + if((busd[@to].basekv >= @vstart) and (busd[@to].basekv <= @vend)) + @opt2 = 1 + endif + endif + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + elseif(@temp3 and @z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp3) + if(@z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + + endif + + if(@opt1 and @opt2) + #pktran[@i] = 1 + endif + + next + + + /*********************************************************************************************/ + /* LOOP THROUGH GENS (GENERATOR) EDIT TABLE */ + /* - THREE CONDITIONS MUST BE MET IN ORDER TO ADD GENERATOR INTO CONTINGENCY LIST */ + /* 1. THE USER WANTS TO INCLUDE GENERATOR OUTAGES */ + /* 2. THE ELEMENT OR THE FROM BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /* 3. THE GENERATOR MVABASE MUST BE LARGER THAN THE MINIMUM MVABASE TO TRIP */ + /*********************************************************************************************/ + + if(@genout) + + for @i = 0 to casepar[0].ngen-1 + + if(#pkgen[@i] > 0) /* DESIGNATES THAT GEN HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @ibs = gens[@i].ibgen + + if($arzo = "area") + @tt = gens[@i].area + @t1 = busd[@ibs].area + else + @tt = gens[@i].zone + @t1 = busd[@ibs].zone + endif + + if(@zfilter = 1) + @z1 = gens[@i].zone + @z2 = busd[@ibs].zone + endif + + @opt1 = 0 + @opt2 = 0 + @temp1 = 0 + @temp2 = 0 + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + + if(gens[@i].mbase >= @smin) + @opt2 = 1 + endif + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + endif + + if(@opt1 and @opt2) + #pkgen[@i] = 1 + endif + + next + + endif + + + /*********************************************************************************************/ + /* LOOP THROUGH BUS EDIT TABLE */ + /* - TWO CONDITIONS MUST BE MET IN ORDER TO ADD BUS INTO CONTINGENCY LIST */ + /* 1. THE USER WANTS TO INCLUDE BUS OUTAGES */ + /* 2. THE BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /*********************************************************************************************/ + + if(@busout) + + for @i = 0 to casepar[0].nbus-1 + + if(#pkbus[@i] > 0) /* DESIGNATES THAT BUS HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + if($arzo = "area") + @t1 = busd[@i].area + else + @t1 = busd[@i].zone + endif + + if(@zfilter = 1) + @z1 = busd[@i].zone + endif + + @opt1 = 0 + @opt2 = 0 + @temp1 = 0 + @temp2 = 0 + + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + endif + + if(@opt1) + #pkbus[@i] = 1 + endif + + next + + endif + + + /*********************************************************************************************/ + /* LOOP THROUGH LOAD EDIT TABLE */ + /* - TWO CONDITIONS MUST BE MET IN ORDER TO ADD LOAD INTO CONTINGENCY LIST */ + /* 1. THE USER WANTS TO INCLUDE LOAD OUTAGES */ + /* 2. THE ELEMENT OR THE FROM BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /*********************************************************************************************/ + + if(@loadout) + + for @i = 0 to casepar[0].nload-1 + + if(#pkload[@i] > 0) /* DESIGNATES THAT LOAD HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @lbs = load[@i].lbus + + if($arzo = "area") + @tt = load[@i].area + @t1 = busd[@lbs].area + else + @tt = load[@i].zone + @t1 = busd[@lbs].zone + endif + + if(@zfilter = 1) + @z1 = load[@i].zone + @z2 = busd[@lbs].zone) + endif + + @opt1 = 0 + @opt2 = 0 + @temp1 = 0 + @temp2 = 0 + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + endif + + if(@opt1) + #pkload[@i] = 1 + endif + + next + + endif + + + /*********************************************************************************************/ + /* LOOP THROUGH SHUNT EDIT TABLE (INCLUDES BOTH BUS AND LINE CONNECTED */ + /* - TWO CONDITIONS MUST BE MET IN ORDER TO ADD SHUNT INTO CONTINGENCY LIST */ + /* 1. THE ELEMENT, THE FROM BUS, OR THE TO BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /* 2. THE ELEMENT OR THE FROM BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /*********************************************************************************************/ + + if(@shuntout) + for @i = 0 to casepar[0].nshunt-1 + + if(#pkshunt[@i] > 0) /* DESIGNATES THAT SHUNT HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @from = shunt[@i].ifrom + @to = shunt[@i].ito + + if($arzo = "area") + @tt = shunt[@i].area + @t1 = busd[@from].area + if(@to >= 0) + @t2 = busd[@to].area + endif + else + @tt = shunt[@i].zone + @t1 = busd[@from].zone + if(@to >= 0) + @t2 = busd[@to].zone + endif + endif + + if(@zfilter = 1) + @z1 = shunt[@i].zone + @z2 = busd[@from].zone + if(@to >= 0) + @z3 = busd[@to].zone + endif + endif + + @opt1 = 0 + @opt2 = 0 + @opt3 = 0 + @temp1 = 0 + @temp2 = 0 + @temp3 = 0 + + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + if(@to >= 0) + if(@t2 >= #mrk[0] and @t2 <= #mrk[1]) + @opt1 = 1 + @temp3 = 1 + endif + endif + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + elseif(@temp3 and @z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp3) + if(@z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + + endif + + if(@opt1) + #pkshunt[@i] = 1 + endif + + next + endif + + + + /*********************************************************************************************/ + /* LOOP THROUGH SVD EDIT TABLE */ + /* - TWO CONDITIONS MUST BE MET IN ORDER TO ADD SVD INTO CONTINGENCY LIST */ + /* 1. THE USER WANTS TO INCLUDE SVD OUTAGES */ + /* 2. THE ELEMENT OR THE FROM BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /*********************************************************************************************/ + + if(@SVDout) + + for @i = 0 to casepar[0].nsvd-1 + + if(#pkSVD[@i] > 0) /* DESIGNATES THAT SVD HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @ibs = svd[@i].ibus + + if($arzo = "area") + @tt = svd[@i].area + @t1 = busd[@ibs].area + else + @tt = svd[@i].zone + @t1 = busd[@ibs].zone + endif + + if(@zfilter = 1) + @z1 = svd[@i].zone + @z2 = busd[@ibs].zone) + endif + + @opt1 = 0 + @opt2 = 0 + @temp1 = 0 + @temp2 = 0 + + if(@tt >= #mrk[0] and @tt <= #mrk[1]) + @opt1 = 1 + @temp1 = 1 + endif + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + + + if(@zfilter = 1) + if(@temp1) + if(@z1 >= #mrk[2] and @z1 <= #mrk[3]) + @opt1 = 1 + elseif(@temp2 and @z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + endif + + if(@opt1) + #pkSVD[@i] = 1 + endif + + next + + endif + + + /*********************************************************************************************/ + /* LOOP THROUGH BREAKER EDIT TABLE */ + /* - THREE CONDITIONS MUST BE MET IN ORDER TO ADD BREAKER INTO CONTINGENCY LIST */ + /* 1. THE USER WANTS TO INCLUDE BREAKER OUTAGES */ + /* 2. THE ELEMENT, THE FROM BUS, OR THE TO BUS MUST FALL WITHIN THE AREA/ZONE RANGE SET */ + /* 3. THE FROM BUS VOLTAGE MUST FALL WITHIN THE VOLTAGE RANGE SET */ + /*********************************************************************************************/ + + if(@brkrout) + + for @i = 0 to casepar[0].nbreaker-1 + + if(#pkbrkr[@i] > 0) /* DESIGNATES THAT BREAKER HAS ALREADY BEEN PICKED FOR CONTINGENCY */ + continue + endif + + @from = brkr[@i].ifrom + @to = brkr[@i].ito + + if($arzo = "area") + @t1 = busd[@from].area + @t2 = busd[@to].area + else + @t1 = busd[@from].zone + @t2 = busd[@to].zone + endif + + if(@zfilter = 1) + @z2 = busd[@from].zone + @z3 = busd[@to].zone + endif + + @opt1 = 0 + @opt2 = 0 + + @temp2 = 0 + @temp3 = 0 + + /* include tie */ + if(@t1 >= #mrk[0] and @t1 <= #mrk[1]) + @opt1 = 1 + @temp2 = 1 + endif + if(@t2 >= #mrk[0] and @t2 <= #mrk[1]) + @opt1 = 1 + @temp3 = 1 + endif + + if((busd[@from].basekv >= @vstart) and (busd[@from].basekv <= @vend)) + @opt2 = 1 + endif + + + if(@zfilter = 1) + if(@temp2) + if(@z2 >= #mrk[2] and @z2 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + elseif(@temp3) + if(@z3 >= #mrk[2] and @z3 <= #mrk[3]) + @opt1 = 1 + else + @opt1 = 0 + endif + endif + endif + + if(@opt1 and @opt2) + #pkbrkr[@i] = 1 + endif + + next + + endif + + + /*********************************************************************************************/ + /* RETURN VALUE FROM THE PANEL CALL ABOVE, IF < 0, QUIT FOR LOOP */ + /********************************************************************************************* + + if(@pan-ret < 0 or @pan-ret1 < 0) + quitfor + endif + + +*************************************************************************************************/ +/* END SELECTION OF ELEMENTS LOOP */ +/*************************************************************************************************/ + + + + +/*************************************************************************************************/ +/* ALL ELEMENTS ARE TAGGED FROM SELECTION LOOP, WRITE OUT THE CONTINGENCY LIST FILE (*.OTG) */ +/* - CHECK TO DETERMINE IF FILE IS BEING OVERWRITTEN OR APPENDED TO */ +/*************************************************************************************************/ + +@nnew = 0 + + +/*************************************************************************************************/ +/* LOOP THROUGH SECDD TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nbrsec-1 + + if(#pksecdd[@i] < 0) + continue + endif + + @from = secdd[@i].ifrom + @to = secdd[@i].ito + $ck = secdd[@i].ck + @nsec = secdd[@i].nsec + + + /*********************************************************************************************/ + /* CHECK FOR MULTI-SECTION LINES, DO NOT ENTER MORE THAN ONE TIME */ + /*********************************************************************************************/ + + if(@i > 0) + if(#pksecdd[@i-1] > 0) + if((@from = secdd[@i-1].ifrom) and (@to = secdd[@i-1].ito) and ($ck = secdd[@i-1].ck)) + continue + endif + endif + endif + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((secdd[@i].lid = *longid[0]) or (secdd[@i].lid = $space) or (secdd[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "line_" + $txt + if(@useems) + logbuf(*condes[0],"Line ID = ",secdd[@i].lid) + else + logbuf(*condes[0],"Line ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:5:1," to ",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:5:1," Circuit ",$ck) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@lineRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + + if(@useems) + logprint(*file[0]," ^secdd '",secdd[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^secdd '",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",$ck:2:0,"' ",@nsec:2:0,"^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^secdd ",busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," '",$ck:2:0,"' ",@nsec:2:0,"^ ^open^ ",@eventTime:7:6 ,"<") + endif + endif + + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH TRAN TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].ntran-1 + + if(#pktran[@i] < 0) + continue + endif + + @from = tran[@i].ifrom + @to = tran[@i].ito + $ck = tran[@i].ck + + @itert = tran[@i].itert + if( @itert >= 0 ) + $tertname = busd[@itert].busnam:NAME_LENGTH:0 + @tertkv = busd[@itert].basekv + @tert = busd[@itert].extnum + else + $tertname = " " + @tertkv = 0.0 + @tert = -1 + endif + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((tran[@i].lid = *longid[0]) or (tran[@i].lid = $space) or (tran[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "tran_" + $txt + if(@useems) + logbuf(*condes[0],"Tran ID = ",tran[@i].lid) + else + logbuf(*condes[0],"Tran ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:2," to ",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:2," Circuit ",$ck," ",$tertname:NAME_LENGTH:0," ",@tertkv:6:2) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@tranRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^tran '",tran[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + if(@itert >= 0) + logbuf(*objStr[0], "'",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",busd[@itert].busnam:NAME_LENGTH:0," ",busd[@itert].basekv:6:5,"' '",$ck:2:0,"'") + else + logbuf(*objStr[0], "'",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",$ck:2:0,"'") + endif + else + if(@itert >= 0) + logbuf(*objStr[0], busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," ",busd[@itert].extnum:8:0," '",$ck:2:0,"'") + else + logbuf(*objStr[0], busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," '",$ck:2:0,"'") + endif + endif + logprint(*file[0]," ^tran ",*objStr[0],"^ ^open^ ",@eventTime:7:6 ,"<") + endif + + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH GENS TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].ngen-1 + + if(#pkgen[@i] < 0) + continue + endif + + @bus = gens[@i].ibgen + $id = gens[@i].id + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((gens[@i].lid = *longid[0]) or (gens[@i].lid = $space) or (gens[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "gen_" + $txt + if(@useems) + logbuf(*condes[0],"Gen ID = ",gens[@i].lid) + else + logbuf(*condes[0],"Gen ",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:5:1," Unit ID ",$id) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@genRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^gen '",gens[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^gen '",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:6:5,"' '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^gen ",busd[@bus].extnum:8:0," '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + endif + endif + + + /*********************************************************************************************/ + /* REDISPATCHING EPCL DEFINED EARLY IN THIS EPCL ALSO PRINTED OUT IN CONTINGENCY LIST */ + /*********************************************************************************************/ + if( @redepcl ) + logprint(*file[0]," epcl ^", *redisp[0],"^ <") + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH BUS TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nbus-1 + + if(#pkbus[@i] < 0) + continue + endif + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((busd[@i].lid = *longid[0]) or (busd[@i].lid = $space) or (busd[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "bus_" + $txt + if(@useems) + logbuf(*condes[0],"BUS ID = ",busd[@i].lid) + else + logbuf(*condes[0],"Bus ",busd[@i].busnam:NAME_LENGTH:0," ",busd[@i].basekv:5:1) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@otherRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^bus '",busd[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^bus '",busd[@i].busnam:NAME_LENGTH:0," ",busd[@i].basekv:6:5,"'","^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^bus ",busd[@i].extnum:8:0,"^ ^open^ ",@eventTime:7:6 ,"<") + endif + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH LOAD TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nload-1 + + if(#pkload[@i] < 0) + continue + endif + + @bus = load[@i].lbus + $id = load[@i].id + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((load[@i].lid = *longid[0]) or (load[@i].lid = $space) or (load[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "load_" + $txt + if(@useems) + logbuf(*condes[0],"Load ID = ",load[@i].lid) + else + logbuf(*condes[0],"Load ",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:5:1," ID ",$id) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@otherRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^load '",load[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^load '",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:6:5,"' '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^load ",busd[@bus].extnum:8:0," '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + endif + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH SHUNT TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nshunt-1 + + if(#pkshunt[@i] < 0) + continue + endif + + @from = shunt[@i].ifrom + @to = shunt[@i].ito + $id = shunt[@i].id + $ck = shunt[@i].ck + @nsec = shunt[@i].nsecsh + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((shunt[@i].lid = *longid[0]) or (shunt[@i].lid = $space) or (shunt[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "shunt_" + $txt + if(@useems) + logbuf(*condes[0],"Shunt ID = ",shunt[@i].lid) + else + if(@to >= 0) + logbuf(*condes[0],"Line Shunt ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:5:1," to ",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:5:1," ID ",$id) + else + logbuf(*condes[0],"Bus Shunt ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:5:1," ID ",$id) + endif + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@otherRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + + if(@useems) + logprint(*file[0]," ^shunt '",shunt[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + if(@to >= 0) + logbuf(*objStr[0], "'",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",$ck:2:0,"' '",$id:2:0,"' ",@nsec:2:0) + else + logbuf(*objStr[0], "'",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",$id:2:0,"'") + endif + else + if(@to >= 0) + logbuf(*objStr[0], busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," '",$ck:2:0,"' '",$id:2:0,"' ",@nsec:2:0) + else + logbuf(*objStr[0], busd[@from].extnum:8:0," '",$id:2:0,"'") + endif + endif + logprint(*file[0]," ^shunt ",*objStr[0],"^ ^open^ ",@eventTime:7:6 ,"<") + endif + + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH SVD TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nsvd-1 + + if(#pkSVD[@i] < 0) + continue + endif + + @bus = svd[@i].ibus + $id = svd[@i].id + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((svd[@i].lid = *longid[0]) or (svd[@i].lid = $space) or (svd[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "svd_" + $txt + if(@useems) + logbuf(*condes[0],"SVD ID = ",svd[@i].lid) + else + logbuf(*condes[0],"SVD ",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:5:1," ID ",$id) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@otherRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^svd '",svd[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^svd '",busd[@bus].busnam:NAME_LENGTH:0," ",busd[@bus].basekv:6:5,"' '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^svd ",busd[@bus].extnum:8:0," '",$id:2:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + endif + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + + +/*************************************************************************************************/ +/* LOOP THROUGH BREAKER TABLE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nbreaker-1 + + if(#pkbrkr[@i] < 0) + continue + endif + + @from = brkr[@i].ifrom + @to = brkr[@i].ito + $ck = brkr[@i].id + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((brkr[@i].lid = *longid[0]) or (brkr[@i].lid = $space) or (brkr[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "brkr_" + $txt + if(@useems) + logbuf(*condes[0],"Breaker ID = ",brkr[@i].lid) + else + logbuf(*condes[0],"brkr ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:5:1," to ",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:5:1," Circuit ",$ck) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@lineRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^breaker '",brkr[@i].lid,"'^ ^open^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^breaker '",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",$ck:8:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^breaker ",busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," '",$ck:8:0,"'","^ ^open^ ",@eventTime:7:6 ,"<") + endif + + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 + +next + +if(@stckbrkr) +/*************************************************************************************************/ +/* LOOP THROUGH BREAKER TABLE FOR STUCK BREAKER EVENT TYPE */ +/*************************************************************************************************/ + +for @i = 0 to casepar[0].nbreaker-1 + + if(#pkbrkr[@i] < 0) + continue + endif + + @from = brkr[@i].ifrom + @to = brkr[@i].ito + $ck = brkr[@i].id + + + /*********************************************************************************************/ + /* CONTINGENCY LABEL AND CONTINGENCY DESCRIPTION DEFINED AND PRINTED OUT */ + /*********************************************************************************************/ + *longid[0] = " " + $space = " " + $blank = "" + if(@emsids) + @useems = 1 + if((brkr[@i].lid = *longid[0]) or (brkr[@i].lid = $space) or (brkr[@i].lid = $blank)) + @useems = 0 + endif + endif + + logbuf($txt,@number) + $connam = "stckbrkr_" + $txt + if(@useems) + logbuf(*condes[0],"Breaker ID = ",brkr[@i].lid) + else + logbuf(*condes[0],"brkr ",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:5:1," to ",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:5:1," Circuit ",$ck) + endif + logprint(*file[0],"^",$connam,"^ ^",*condes[0],"^ ",@lineRfact:6:2," ^^", " ^^"," ^^"," ", @skip,"<") + + + /*********************************************************************************************/ + /* CONTINGENCY OUTAGE PRINTED OUT BASED ON BUS NAME+kV OR BUS NUMBER */ + /*********************************************************************************************/ + if(@useems) + logprint(*file[0]," ^breaker '",brkr[@i].lid,"'^ ^stuck^ ",@eventTime:7:6 ,"<") + else + if(@busname) + logprint(*file[0]," ^breaker '",busd[@from].busnam:NAME_LENGTH:0," ",busd[@from].basekv:6:5,"' '",busd[@to].busnam:NAME_LENGTH:0," ",busd[@to].basekv:6:5,"' '",$ck:8:0,"'","^ ^stuck^ ",@eventTime:7:6 ,"<") + else + logprint(*file[0]," ^breaker ",busd[@from].extnum:8:0," ",busd[@to].extnum:8:0," '",$ck:8:0,"'","^ ^stuck^ ",@eventTime:7:6 ,"<") + endif + + endif + logprint(*file[0],"0<") + + + /*********************************************************************************************/ + /* COUNTERS */ + /*********************************************************************************************/ + + @number = @number + 1 + @nnew = @nnew + 1 +next +endif +/*************************************************************************************************/ +/* FINISH CONTINGENCY LIST FILE */ +/*************************************************************************************************/ + +logprint(*file[0],"end<# End of Contingency List, ",@nnew," Contingencies Added to List<") + +@zz = close(*file[0]) + +logterm(" The Automated Contingency EPCL Program Created ",@nnew," New Contingencies<") +end + + +subroutine dupCheck + + $file = "duplicate.txt" + @ret = openlog( $file ) + if( @ret < 0 ) + logterm("Cannot open file ",$file,"<") + endif + @dupBusNo = 0 + @dupBusNm = 0 + + logterm("<<<") + @ret = sort("busd.","01","ff") + @dupBusNo = 0 + for @i = 1 to casepar[0].nbusd-1 + if( busd[@i].extnum = busd[@i-1].extnum ) + logprint( $file,"busd table same bus number at ",busident(@i,0, 7, 12, 7.3),"<") + @dupBusNo = 1 + endif + next + if( @dupBusNo = 1 ) + @ret = beep() + logterm("<<****Duplicate bus numbers found<") + logterm("Using [Bus Name + kv] as bus identifier to create contingencies****<") + logterm("Duplicate bus info written to file ",$file,"<") + endif + + logterm("<<<") + @ret = sort("busd","0012","ffff") /* sort names then basekv */ + @dupBusNm = 0 + for @i = 1 to casepar[0].nbus-1 + if( busd[@i].busnam = busd[@i-1].busnam ) + if ( busd[@i].basekv=busd[@i-1].basekv) + logprint( $file,"busd table same bus name at ",busident(@i,0, 7, 12, 7.3)," and ",busident(@i-1,0, 7, 12, 7.3),"<") + @dupBusNm = 1 + endif + endif + next + if( @dupBusNm = 1 ) + @ret = beep() + logterm("<<****Duplicate bus names found<") + logterm("Using [Bus Number] as bus identifier to create contingencies****<") + logterm("Duplicate bus info written to file ",$file,"<") + endif + + if( (@dupBusNm = 1) and (@dupBusNo = 1) ) + logterm("<<******Duplicate [bus numbers] as well as [busnames and voltage] found in the case<") + logterm("Please get rid of duplications or it would cause inaccuries in results<") + logterm("Terminating EPCL<*******<<") + @ret = beep() + end + endif + + @ret = close( $file ) + + logterm("<<") + @ret = getf( filepar[0].getf ) + +return diff --git a/PSLF/pslf_mcp.py b/PSLF/pslf_mcp.py index a811c46..b5e1daa 100644 --- a/PSLF/pslf_mcp.py +++ b/PSLF/pslf_mcp.py @@ -1,6 +1,8 @@ import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import pandas as pd +import subprocess from mcp.server.fastmcp import FastMCP from common.utils import PowerError, power_mcp_tool from typing import Dict, List, Optional, Tuple, Any, Union @@ -9,7 +11,7 @@ mcp = FastMCP("PSLF Positive Sequence Load Flow Program") from PSLF_PYTHON import * -init_pslf(silent=False) +init_pslf(silent=True, working_directory=os.getcwd()) @power_mcp_tool(mcp) def open_case(case: str) -> Dict[str, Any]: @@ -29,8 +31,10 @@ def open_case(case: str) -> Dict[str, Any]: # Get basic case information bus_data = cp.Nbus - branch_data = cp.Nbrsec + branch_data = cp.Nbrsec + cp.Ntran gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd if (iret == 0): return { @@ -39,7 +43,9 @@ def open_case(case: str) -> Dict[str, Any]: 'path': os.getcwd() + "\\" + case, 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } else: @@ -51,6 +57,32 @@ def open_case(case: str) -> Dict[str, Any]: status='error unknown', message=str(e) ) + +@power_mcp_tool(mcp) +def save_case() -> Dict[str, Any]: + """ + Save a PSLF case file to temp.sav + + Returns: + Dict with status and case information + """ + try: + + iret = Pslf.save_case(os.getcwd() + "\\temp.sav") + + if (iret == 0): + return { + 'status': 'success' + } + else: + return { + 'status': 'error failed to save case' + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) @power_mcp_tool(mcp) def solve_case() -> Dict[str, Any]: @@ -105,7 +137,7 @@ def solve_case() -> Dict[str, Any]: ) @power_mcp_tool(mcp) -def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, Any]: +def add_bus(busnum: int, busname: str, nominalkv: float, type: int = 1) -> Dict[str, Any]: """ Add a new bus to the power system model @@ -125,8 +157,10 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, # Get basic case information bus_data = cp.Nbus - branch_data = cp.Nbrsec + branch_data = cp.Nbrsec + cp.Ntran gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd if (iret == 0): return { @@ -134,7 +168,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 1): @@ -143,7 +179,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 2): @@ -152,7 +190,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 3): @@ -161,7 +201,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == -2): @@ -170,7 +212,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } else: @@ -179,7 +223,9 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } except Exception as e: @@ -189,32 +235,56 @@ def add_bus(busnum: int, busname: str, nominalkv: float, type: int) -> Dict[str, ) @power_mcp_tool(mcp) -def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: float, reactance: float, susceptance: float, rating: float) -> Dict[str, Any]: +def add_branch(frombus: int, tobus: int, reactance: float, circuit: str = "1 ", resistance: float = 0.0, susceptance: float = 0.0, rating: float = 9999.0, section: int = 1) -> Dict[str, Any]: """ - Add a new transmission line to the power system model + Add a new branch (transmission line, transformer, series capacitor, or series reactor) to the power system model Args: - frombus: Integer identifier of the transmission lines starting terminal Is not necessarily consecutive and could be larger than the number of buses in the case. - tobus: Integer identifier of the transmission lines ending terminal Is not necessarily consecutive and could be larger than the number of buses in the case. - circuit: Integer identifer uniquely identifying the circuit in the case there are multiple circuits. (default 1) + frombus: Integer identifier of the starting terminal Is not necessarily consecutive and could be larger than the number of buses in the case. + tobus: Integer identifier of the ending terminal Is not necessarily consecutive and could be larger than the number of buses in the case. + reactance: A float representing the imaginary component of impedance of the transmission line in per unit. Positive is inductive and negative is capacitive. If the user gives a value in Ohms, ask to get a value in per unit. + circuit: 2 character identifer uniquely identifying the circuit in the case there are multiple circuits. (default "1 ") resistance: A float representing the real component of impedance of the transmission line in per unit. (default 0.0) - reactance: A float representing the imaginary component of impedance of the transmission line in per unit. susceptance: A float representing the per unit susceptance of the transmission line. (default 0.0) rating: The maximum safe rating of the transmission line in MVA (default 9999.0) + section: An integer that identifies a series element. A series capacitor or series reactor would be an additional section for example a series component with section=2 Returns: Dict with status """ try: - iret = Pslf.add_record(1, 1, str(frombus) + " " + str(tobus) + " " + str(circuit) + " 1", "st zsecr zsecx bsec rate[0]", "1 " + str(resistance) + " " + str(reactance) + " " + str(susceptance) + " " + str(rating)) + # determine if it is a transformer or transmission line + index = Pslf.bus_internal_index(frombus) + if (index < 0): + return { + 'status': 'error from bus does not exist' + } + else: + from_volt = Bus[index].Basekv + index = Pslf.bus_internal_index(tobus) + if (index < 0): + return { + 'status': 'error to bus does not exist' + } + else: + to_volt = Bus[index].Basekv + + if (from_volt == to_volt): + # transmission line + iret = Pslf.add_record(1, 1, str(frombus) + " " + str(tobus) + " " + circuit + " 1", "st zsecr zsecx bsec rate[0]", "1 " + str(resistance) + " " + str(reactance) + " " + str(susceptance) + " " + str(rating)) + else: + # 2 winding transformer (tertiary kbus is -1 in the identifier) + iret = Pslf.add_record(1, 2, str(frombus) + " " + str(tobus) + " " + circuit + " -1", "tbase st zpsr zpsx rate[0]", "100 1 " + str(resistance) + " " + str(reactance) + " " + str(rating)) cp = CaseParameters() # Get basic case information bus_data = cp.Nbus - branch_data = cp.Nbrsec + branch_data = cp.Nbrsec + cp.Ntran gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd if (iret == 0): return { @@ -222,7 +292,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 1): @@ -231,7 +303,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 2): @@ -240,7 +314,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 3): @@ -249,7 +325,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } elif (iret == 4): @@ -258,7 +336,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } else: @@ -267,7 +347,9 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl 'case_info': { 'num_buses': bus_data if bus_data is not None else 0, 'num_branches': branch_data if branch_data is not None else 0, - 'num_generators': gen_data if gen_data is not None else 0 + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 } } except Exception as e: @@ -276,10 +358,250 @@ def add_transmission_line(frombus: int, tobus: int, circuit: int, resistance: fl message=str(e) ) +@power_mcp_tool(mcp) +def add_generator(bus: int, power_scheduled_mw: float, genid: str = "1 ", power_max: float = 9999.0, reactive_power_max: float = 9999.0, reactive_power_min: float = -9999.0) -> Dict[str, Any]: + """ + Add a new generator to the case. + + Args: + bus: Integer identifier of the terminal bus of the generator + power_scheduled_mw: Amount of real power output (in megawatts) scheduled to be generated by the unit. + genid: 2 character string that uniquely identifies generators when there are multiple units attached to the same bus. (default "1 ") + power_max: Maximum real power that the generator can output in megawatts. (default 9999.0) + reactive_power_max: Maximum reactive power that the generator can output in megavars. (default 9999.0) + reactive_power_min: Minimum reactive power that the generator can output in megavars. Should be a negative number. If positive, change the input to be negative. (default -9999.0) + + Returns: + Dict with status + """ + try: + + iret = Pslf.add_record(1, 3, str(bus) + " " + str(genid), "st mbase pgen pmax qmax qmin", "1 100 " + str(power_scheduled_mw) + " " + str(power_max) + " " + str(reactive_power_max) + " " + str(reactive_power_min)) + + cp = CaseParameters() + + # Get basic case information + bus_data = cp.Nbus + branch_data = cp.Nbrsec + cp.Ntran + gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd + + if (iret == 0): + return { + 'status': 'success', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 1): + return { + 'status': 'error insufficient input', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 2): + return { + 'status': 'error generator bus out of range', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + else: + return { + 'status': 'error generator bus does not exist', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + +@power_mcp_tool(mcp) +def add_load(bus: int, real_power: float, reactive_power: float, loadid: str = "1 ") -> Dict[str, Any]: + """ + Add a new load to the case. + + Args: + bus: Integer identifier of the bus the load is located at + real_power: Amount of real power consumed by the load in megawatts. If user gives a per unit value, multiply by the system base MVA of 100 to get the quantity in megawatts. + reactive_power: Amount of reactive power consumed by the load in megawatts. Inductive loads are positive and capacitive loads are negative. If the user does not specify inductive, capacitive, or provide the sign explicitly, assume the load is inductive. If user gives a per unit value, multiply by the system base MVA of 100 to get the quantity in megavars. + loadid: 2 character string that uniquely identifies the load when there are multiple loads attached to the same bus. (default "1 ") + + Returns: + Dict with status + """ + try: + + iret = Pslf.add_record(1, 4, str(bus) + " " + loadid, "st p q", "1 " + str(real_power) + " " + str(reactive_power)) + + cp = CaseParameters() + + # Get basic case information + bus_data = cp.Nbus + branch_data = cp.Nbrsec + cp.Ntran + gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd + + if (iret == 0): + return { + 'status': 'success', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 1): + return { + 'status': 'error insufficient input', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 2): + return { + 'status': 'error load bus out of range', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + else: + return { + 'status': 'error load bus does not exist', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + +@power_mcp_tool(mcp) +def add_shunt(bus: int, reactive_power: float, variable_flag: int = 0, reactive_max: float = 0.0, reactive_min: float = 0.0, shuntid: str = "1 ") -> Dict[str, Any]: + """ + Add a new fixed or variable shunt to the case. + + Args: + bus: Integer identifier of the bus the shunt is located at + reactive_power: Amount of reactive power (in per unit) injected or absorbed by the shunt. Injections (capacitive) is positive and absorptions (inductive) is negative. If the user does not specify inductive, capacitive, or provide the sign explicitly, assume the shunt is an injection (capacitive). If the user specifies MVAR, divide by the system base MVA of 100 to get the value in per unit. + variable_flag: 0 if shunt is fixed output and cannot change. 1 if shunt is continuously variable (SVC/STATCOM). (default 0) + reactive_max: Only used when variable_flag = 1. Numeric value representing the maximum injection of a variable shunt in MVAR. (default 0.0) + reactive_min: Only used when variable_flag = 1. Numeric value representing the minimum output (maximum absorbtion) of a variable shunt in MVAR. (default 0.0) + shuntid: 2 character string that uniquely identifies the shunt when there are multiple shunts attached to the same bus. (default "1 ") + + Returns: + Dict with status + """ + try: + if (variable_flag == 0): + # Fixed shunt + iret = Pslf.add_record(1, 5, str(bus) + " " + shuntid, "st b", "1 " + str(reactive_power / 100.0)) # Divide by system base MVA to get in per unit + else: + # SVD + iret = Pslf.add_record(1, 6, str(bus) + " " + shuntid, "type st b bmax bmin", "2 1 " + str(reactive_power / 100.0) + " " + str(reactive_max / 100.0) + " " + str(reactive_min / 100.0)) # Divide by system base MVA to get in per unit + + cp = CaseParameters() + + # Get basic case information + bus_data = cp.Nbus + branch_data = cp.Nbrsec + cp.Ntran + gen_data = cp.Ngen + load_data = cp.Nload + shunt_data = cp.Nshunt + cp.Nsvd + + if (iret == 0): + return { + 'status': 'success', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 1): + return { + 'status': 'error insufficient input', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + elif (iret == 2): + return { + 'status': 'error load bus out of range', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + else: + return { + 'status': 'error load bus does not exist', + 'case_info': { + 'num_buses': bus_data if bus_data is not None else 0, + 'num_branches': branch_data if branch_data is not None else 0, + 'num_generators': gen_data if gen_data is not None else 0, + 'num_loads': load_data if load_data is not None else 0, + 'num_shunts': shunt_data if shunt_data is not None else 0 + } + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + + @power_mcp_tool(mcp) def get_voltage(bus: int) -> Dict[str, Any]: """ - Queries the voltage of a bus and reports in per unit + Queries the voltage of a single bus and reports in per unit. If checking multiple buses with thresholds, use get_voltage_violations function instead. Args: bus: Integer identifier of the desired bus to query voltage for. Is not necessarily consecutive and could be larger than the number of buses in the case. @@ -312,6 +634,236 @@ def get_voltage(bus: int) -> Dict[str, Any]: status='error unknown', message=str(e) ) + +@power_mcp_tool(mcp) +def get_voltage_violations(overvoltage_threshold: float = 1.05, undervoltage_threshold: float = 0.95) -> Dict[str, Any]: + """ + Queries all bus voltages, compares against a threshold (default +/- 5%), and reports buses with voltage violations. For querying a single bus without thresholds, use get_voltage instead. + + Args: + overvoltage_threshold: A float value representing the maximum tolerable voltage to check for in per unit. If the user provides percent, divide their input by 100 first. (default 1.05) + undervoltage_threshold: A float value representing the lowest tolerable voltage to check for in per unit. If the user provides percent, divide their input by 100 first. (default 0.95) + + Returns: + Dict with status + """ + try: + + bus_count = CaseParameters().Nbus + + overvoltage = {} + undervoltage = {} + for i in range(bus_count): + if Bus[i].Vm > overvoltage_threshold: + overvoltage.update({ + 'bus_id': str(Bus[i].Extnum), + 'bus_name': Bus[i].Busnam, + 'base_kv': str(Bus[i].Basekv), + 'voltage_perunit_kv': str(Bus[i].Vm), + 'voltage_kv': str(Bus[i].Vm * Bus[i].Basekv), + 'voltage_angle_degrees': str(Bus[i].Va)}) + elif Bus[i].Vm < undervoltage_threshold: + undervoltage.update({ + 'bus_id': str(Bus[i].Extnum), + 'bus_name': Bus[i].Busnam, + 'base_kv': str(Bus[i].Basekv), + 'voltage_perunit_kv': str(Bus[i].Vm), + 'voltage_kv': str(Bus[i].Vm * Bus[i].Basekv), + 'voltage_angle_degrees': str(Bus[i].Va)}) + + if (bus_count <= 0): + return { + 'status': 'error case not loaded into memory' + } + else: + return { + 'status': 'success', + 'overvoltage': overvoltage, + 'undervoltage': undervoltage + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + +@power_mcp_tool(mcp) +def get_overload_violations(overload_threshold: float = 1.0) -> Dict[str, Any]: + """ + Queries a list of branches (transmission lines and transformers) with loading above a threshold (default 100%). + + Args: + overload_threshold: A float value representing the maximum tolerable loading in per unit. (default 1.0) If user gives in percent, divide their input by 100 first. + + Returns: + Dict with status + """ + try: + + branch_count = CaseParameters().Nbrsec + if (branch_count <= 0): + return { + 'status': 'error case not loaded into memory' + } + + iret = Pslf.calculate_ac_flow(1) + if (iret != 0) : + return { + 'status': 'error calculating flox table' + } + + overload = {} + for i in range(branch_count): + if Flox[i].Pul > overload_threshold: + overload.update({ + 'type' : 'transmission_line' if Flox[i].Flag == 0 else 'transformer', + 'from_bus': Flox[i].From, + 'to_bus': Flox[i].To, + 'circuit_id': Flox[i].Ck, + 'percent_loading': Flox[i].Pul * 100}) + + return { + 'status': 'success', + 'overload': overload + } + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + +@power_mcp_tool(mcp) +def run_contingency_analysis() -> Dict[str, Any]: + """ + Generates N-1 contingencies, uses SSTOOLS to run all contingencies, and generates a cross tabulated table of results. + + Returns: + Dict with status + """ + + # Save the case into a temporary file + try: + iret = Pslf.save_case(os.getcwd() + "\\sstools.sav") + except Exception as e: + return PowerError( + status='error unknown', + message=str(e) + ) + + # Generate a list of N-1 contingencies based on the case stored in the temporary file + try: + iret = Pslf.run_epcl(os.getcwd() + "\\PSLF\generate-otg.p") + except Exception as e: + return PowerError( + status='error generate-otg.p not found', + message=str(e) + ) + + # Generate a list of default criteria to evaluate the case with. + try: + with open("control.cntl", 'w') as f: + f.write("rating 1 2\n") + f.write("monitor voltage area 1 999 1 999 0.95 1.05 0.90 1.10 0.07 0.0 0.0\n") + f.write("monitor flows area 1 999 1 999 0.0 30.0 100.0 1 2\n") + f.write("monitor interface 1 999 100.0 1 2\n") + f.write("monitor svd area 1 999 1 999\n") + f.write("monitor load area 1 999\n") + f.write("monitor gens area 1 999 1\n") + f.write("monitor area 1 999\n") + f.write("LTC 1 0\n") + f.write("SVD 1 0\n") + f.write("PAR 1 0\n") + f.write("dctap 1 0\n") + f.write("area 0 0\n") + + + except Exception as e: + return PowerError( + status='error creating control.cntl', + message=str(e) + ) + + # Generate the batch contingency run. + try: + with open("runs.cases", 'w') as f: + f.write('CASE "output" 0\n') + f.write('{\n') + f.write('SAV "sstools.sav"\n') + f.write('OTG "cont.otg"\n') + f.write('CNTL "control.cntl"\n') + f.write('OUTPUT "output.crf"\n') + f.write('}\n') + + + except Exception as e: + return PowerError( + status='error creating runs.cases', + message=str(e) + ) + + # Run the batch of contingencies + try: + iret = Pslf.run_sstools("runs.cases") + + except Exception as e: + return PowerError( + status='error running SSTOOLS', + message=str(e) + ) + + # Generate the ProvisoHD batch data file + try: + with open("template.ctab", 'w') as f: + f.write('runtype "cont-process"\n') + f.write('report 1\n') + f.write('postproc 2\n') + f.write('ratingunits 0\n') + f.write('"' + os.getcwd() + '\\output.crf" "' + os.getcwd() + '\\output.xlsx"\n') + f.write('end\n') + + except Exception as e: + return PowerError( + status='error creating template.ctab', + message=str(e) + ) + + + # Generate a ProvisoHD call + try: + with open(os.getcwd()+"\\run.bat", 'w') as f: + f.write('@SET ctab=%cd%\\template.ctab\n') + f.write('cd "C:\\Program Files (x86)\\ProvisoHD\\Release"\n') + f.write('@SET CURRENTDIR=C:\\Program Files (x86)\\ProvisoHD\\Release\n') + f.write('@SET CLAZZPATH=%CURRENTDIR%\\dist\\ProvisioPlotting.jar;%CURRENTDIR%\\dist\\ProvisoHD.jar\n') + f.write('"%CURRENTDIR%\\jre\\bin\\java.exe" -classpath "%CLAZZPATH%" gui.Face1 -batch "%ctab%"\n') + f.write('exit\n') + + except Exception as e: + return PowerError( + status='error creating run.bat', + message=str(e) + ) + + # Generate a system call to run ProvisoHD + try: + iret = subprocess.run(os.getcwd()+"\\run.bat", capture_output=True, text=True, check=True, shell=True) + except Exception as e: + return PowerError( + status='error running ProvisoHD', + message=str(e) + ) + + # Return the contingency analysis results. + VoltageViolations = pd.read_excel("Output.xlsx", sheet_name="VoltageViolations").to_dict(orient='dict') + PerUnitFlowViolations = pd.read_excel("Output.xlsx", sheet_name="PerUnitFlowViolations").to_dict(orient='dict') + UnsolvedContingencies = pd.read_excel("Output.xlsx", sheet_name="UnsolvedDescriptor").to_dict(orient='dict') + return { + 'status': 'success', + 'VoltageViolations': VoltageViolations, + 'PerUnitFlowViolations': PerUnitFlowViolations, + 'UnsolvedContingencies': UnsolvedContingencies + } + if __name__ == "__main__": mcp.run(transport="stdio") \ No newline at end of file From 78fb407e315d1c0946df9668fd3845da6a59c4ab Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 3 Oct 2025 15:44:20 -0600 Subject: [PATCH 3/4] Add automated test process to prompt the LLM for Design Project 5 and instructions on running with Google Gemini. --- PSLF/pslf_mcp.py | 2 +- README.md | 9 +++++++-- pslf_test.prompt | 12 ++++++++++++ system-prompt.md | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 pslf_test.prompt create mode 100644 system-prompt.md diff --git a/PSLF/pslf_mcp.py b/PSLF/pslf_mcp.py index b5e1daa..b360268 100644 --- a/PSLF/pslf_mcp.py +++ b/PSLF/pslf_mcp.py @@ -11,7 +11,7 @@ mcp = FastMCP("PSLF Positive Sequence Load Flow Program") from PSLF_PYTHON import * -init_pslf(silent=True, working_directory=os.getcwd()) +init_pslf(silent=False, working_directory=os.getcwd()) @power_mcp_tool(mcp) def open_case(case: str) -> Dict[str, Any]: diff --git a/README.md b/README.md index 07991c1..5b90804 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,14 @@ Or for PowerWorld. } } ``` -4) Start the MCP server, replacing the model name and config file with your preferred option. +4) Start the MCP server in interactive mode, use the following command. Replace the model name and config file with your preferred option. ``` -mcphost -m ollama:qwen3:4b --config .\config.json +mcphost -m ollama:qwen3:4b --config .\config.json --system-prompt .\system-prompt.md +``` + +To run a test prompt that runs the LLM through a test and saves the output to a report, use the following command. +``` +mcphost script pslf_test.prompt ``` ### Video Demos diff --git a/pslf_test.prompt b/pslf_test.prompt new file mode 100644 index 0000000..3c198bd --- /dev/null +++ b/pslf_test.prompt @@ -0,0 +1,12 @@ +--- +mcpServers: + pslf: + command: ["python", "PSLF/pslf_mcp.py"] +model: "google:gemini-2.0-flash" +provider-api-key: "get your key at https://aistudio.google.com/app/apikey" +stream: false +compact: true +prompt: | + You are a practical assistant focused on concise, direct, and correct answers. Do not generate long chains of reasoning or philosophical discussions. If asked a direct question or given a command (e.g., “Open the powerflow case psec-1.sav”), respond with the specific action, result, or step-by-step instructions, nothing more. When unsure, state the uncertainty briefly and provide the best next step, without speculation. Keep responses short, clear, and action-oriented, like a domain expert or technical operator. Only generate a single tool call at a time and make sure it's successful before proceeding. +--- +Use PSLF to open basecase.sav and determine the amount of shunt compensation required at the 230 and 345 kV buses (Buses 4, 10 are 345 and Buses 1, 5, 6, 7, 8, 9 are 230.) such that the voltage magnitude lies between 0.99 and 1.02 per unit for all buses in the case. \ No newline at end of file diff --git a/system-prompt.md b/system-prompt.md new file mode 100644 index 0000000..529d16f --- /dev/null +++ b/system-prompt.md @@ -0,0 +1,5 @@ +You are a practical assistant focused on concise, direct, and correct answers. +- Do not generate long chains of reasoning or philosophical discussions. +- If asked a direct question or given a command (e.g., “Open the powerflow case psec-1.sav”), respond with the specific action, result, or step-by-step instructions, nothing more. +- When unsure, state the uncertainty briefly and provide the best next step, without speculation. +- Keep responses short, clear, and action-oriented, like a domain expert or technical operator. \ No newline at end of file From 16f1bdd069daa7b9a12db99af123948653ee8f20 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 3 Oct 2025 15:45:43 -0600 Subject: [PATCH 4/4] Add basecase.sav --- basecase.sav | Bin 0 -> 278528 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 basecase.sav diff --git a/basecase.sav b/basecase.sav new file mode 100644 index 0000000000000000000000000000000000000000..5aac970ff38b456de429e082b52e8f0a03c06ec1 GIT binary patch literal 278528 zcmeI53z!^LnfJTq*4;^FCLx3&AktiufdOVF_eAJPB* zPc)No6HzXr`{1JM9Tt=oWj_I3$*0-HRo4sZdf7)8QE@>PW4RQ|3$Am*AA^r$@Qydm|2xroG^^UKqiw&BsRg%>F^_8 z8zIpmGw^Ak^mNHhiIZP@Wh<1XI)A{9S>O4m&SyG**ZJ$tUv@s$`D0<6?&SU5o?;!i zxzd{KTDHvi=%&!jSFF%C9rn}q>I=7SzIbcq;>}ym-I`&R%*vcy9Lv~FXq7F$Clls^ z(5!mOFfw8nGx>^}@5$ID->s?=J9Jg1Fq8?cv9KrO`R+D2Bd#zAaoSOtaJy%z+>mdT zRn8t~%|`jq@=-lD)UFCm-!fHh+;vd3Vcra^3>;AD$&7n}uaaYNGE~+uwub81Fp$=o z;{qB{8%qZ$9Y$%>DOcn%<*HeZ%z2gc&Cu$N%q-o9()}pC3Z+-0^cs|2i_+_&v{`Wj z*|M-|%EpIP+lh{-Av2_7dk|WlY=anZgFPz6Q3ik+Hwgf%Qgr5XMotD{DJk*h1|n!XSZ79mEap z29B>jOEt=5aAd$6K4dXe-NR!Q0 z9_4GYn8pFlgg6t7h$-y)br_AXxx8k`ZeIc^6#N}UllI3wy9IwOqF0M3OGc{*TL8KA5(o@0%uT%hGb zEeG8%R3j8syOvR*=j^ahmf!*i%l`YxYp(p`B*x+~8} zcjX!Bt~?`WKF{E52Kb_ZcGW<;Y=|!#;>(8kvLU`~h%X!B%ZB)}p?29Yx@@NVn$0bI zyK{rZw>vKb4glXocT1@8MH54 zTRK@h@1&pA8PN0)N1*9}hO2w9DcQArxiN7fr&#j?+jYcyvqn(ZGApa*Hs7kLaRi^! z@@%iEN?`%AGC}CW5O+_kH@=JL^rIdrCO|~@Us<0BWOk-=4)|AG_WLB~T zsA}eF7Vm^=M8ynXe{a?-b~!6r5V;YaJo|d!t$PK$9h-r-BVGY-ha3WL9gE>Dorbqm zYdgFfo%bi;AH9eG5g-CYfCvx)B0vO)01+SpM1Tkofxj{WXBe%>Y5!ed?2g5f8sZ@8@)aQTrqkno4 z0U|&IhyW2F0z`la5CI}U1c(3;AOZ(40rva<1?h_tofma{to?u5&t3Gfg)c0;c)>^0 ze*z=DhyW2F0z`la5CI}U1m;3u;z?Rco~;xpZ@yScS9qxPnAkn z@WvP`TR(2BJ1)yJGrUbs))!73m)@&(rl6gfV|!cLUU$=3=U%<}8;9(4Mn}`o&J9sB zGl|FdX**9&MeQ`E<(Xsv;e~l7p`DBFS#ia!f4Qf4)`uTo*7p8Y?v*KM=X%x7rs*3x z(6RoIfqG^Q@{PbI_X=#=;GWfgIMeI+g#F8j^WO2RZ;$wQ{q`?OXz)7K;OX1jplO<^ zQ9IlAs-3NadrrOYxVL?z^@@$pzWP09eeT%LHVt3U2JKw?Vzsk%uiDuHJa_%F^QpJ| z>5l8q{@CEkLl45-#_8Lkq-?1u^HOAO1ATm8Rk>=x83*d z);zLtrh%XKG_6Uq1$Cb7h2}2%eCK_;|NR?>o%_Hc>mEMxz8!C9g68(W$jv>$Sd(h$ zNgvmgnti=Wsr7$w&#Lc!{WZ7kIpyAOo_)i2297&r;4^1JYx`ttVO9OzZMS_0sAYdG z?gH>%_g8P^tN*}Zhdwyan5Sl)auZ<3_gmBbnaRn?V)otlzwS|X z`s47JI^_vv#(EPS%b|wJw!?nW`W*iTtM}yI_}|XMoA(bo@Las!KA$v(a3Z$C7%^YGH<DaLQG7pOboVXQW1ji$ z%I}?hef3}O%%1a4hweK>O>NU7V$c7{1@|O6H+9t7|L>yzS=7JKgDky>01+SpM1Tko z0U|&Ih`?U~fr)G~&z_f;u%~7AV>Bh3;Lj|?^N#pi3iu-c_H@gBuV%nQ^wei$^~lWr zID8#}B#VyIoH=^<|nSi$>4DWtZLZD07vYCtagwPF@5@A}>0%ne`ye<^g-XDDwcm z))=_uEPvybho4%0?dP95^l$FDYxw){mCwFt|CXjae0Pyz?dRWo8OeP=kD>WDe|Y1u zlVAV0Wdnb6(0lJ_`iI-!z7Cqd`)JR<0@x3Ph1czXPow8A-<{haup`kRY%%ii<-&{J ziJLmU(ec^y*0-E<>u);#{yUYW&#m2uonX)Zt%jNC+}iOU?YAztz%Y%=!9=fpMS%4G zA~{3S|0DgsSjmM83X=XG)A?jtgHnKt^#5W#POdm5HJ^A!N=)&LR5s%oscgnGQrV1W zB-lEwsiG$Nr)5b0kM#dY{|`iZnd~6x|FIRC^#7zZn-KPsx@q)V$U3YOh+11r$+??c^kDy9iEb_NSnxZ2&%}1NlF8MB!bhblR z$vqZ^q&~FoZUdS3*blJsX%HeMS_iJL4%5X>$DvG$DzmhFJk7In#M;jG08;gprR9E{ zrR9px(sDP>(sDD-(sD1((sC=#(o$oXONvL=nn`N3;*?dRLJ(;!+o)HN2YJoX6bq7c zRzk7X7*oW^F&$lX!oH9%WskItRPO6+ByvB)LvGz;+Vl@*`U6~@MmG|H?a44@_L6jS zmVpTs=@U2BWtX=hyt|409%R$w$zg+@`XDI0M%C4da@Ifrnm<@V8!KpQ0@|^D_QYFL zd|BD`daPJ?f3d zwQi{R)3&B>f5kV03TKg;I)c7Dm`J)E)PcO;bLA|i+OZNF^>*{wM%%L2SW39sHsVN>S`Xk z8m|oQ@3Abos1RG=YB8FEv}B1Es#kr zvo=eMXZZ!T#gKVXL!Gr|%1K#;q<#@F)L#ZnQ!WIysg*Xmm}np9dh;HtyJp4pWXFbU z%mGA;;Xt^SSqSYK7JTN`Am^FkpmGK{7P@ZLo55DGv425@xUEbej@{dU7+EfStIjFM^| zqgBK6<W@%+2C(D#Sm))ACXWx@C%eumSPo`*tJBNkAx&s--ifb0@%%<(EV0tQpT~}U}42P?7z#2I>lU4^`R`qa$JA(F)Y)jLt4q#vD%WX3& zLXW|$NbV+B5qg+qMRKCDA|z|iisT-f6|L5UMc;MNW~FXJQE;6wS9ObM z1&(i%)9(vyHKA-ZqedJ%)IFs%VpW(04VpBe!ICC4NQDr5xk1~weX5^xvuYF10yJ10 zOBEAsI8;G|d!{vAyJ4^`ZFJ5wo34^?7IMRawpf;Gpk&hyW2F z0z`la5CI}U1c(3;AOb|-z$HM>{|D~aQqPD05g-CYfCvx)B0vO)01+SpM1TlL0_^!e zo&I;G|M!XZXWGwL^uC3UFI>Ce8Zgm|2oM1xKm>>Y5g-CYV80tx6<0o$gy>o_ z#GQy3&qL%v9Qm1I$U~@j^@1qgD=)p})bIVnC0D)XgTJY5%KWH1Os4uW+1nyXsxSTS zE`}%j$XnJ7z-RM+vD{3rrOQw#t&>vww!NVJx9JuC^UTRzAO7K;=UwxwH(&6zrdu9v zOG10!|94;90nN(pdBZC{#6G|CFLyi&pR>1j@jMwMiwo+ z>+I_j51#y!XKS~%Fn{=<5Ch0Qd}0$cGm-uGYfogKv$r>WFm7+_zHKkW6WsX1UGAH! zuX*J+Pyf`=ogaSql9Jcb3hn*ypud~7^I!k;%PZLD4_|Xn20mwRZ)zcKZ_B=IFGLUA z`1GEoCqDl0ozJ}H12@0{^II49oF`$C;T(+tgYGd$U6s1%^-oW_w8*t1hw~srt?|6KoQ4(+Vu>yGaAJE zl#3W%&j$BoZ+@&N-0_NI?>zTj`@%2p`JC~?HfZhNeX+)9X1}sHKfpe}UwJ3{oZTBb zabu11SzN$b8p|S-P7O}lxUn1WzGLvs+rReh&z||7FaPNq&wT#uQ<>|^rT5DhJ-eGV z_K|P>;)*#nc7bXvd;T{%&q%;OdJzF4Km>>Y5g-CYfCvx)B0vO)01+SpGZL6sXB@+H z2Km>jN%($3s&`JbgS-KMriX7I2KTJG=Ck+y;@h`;_R3e^`f2Cl$9{b2jhiprx_PFm zvnTA}#k>0>(SoalXhGsDO_!bJY_lLpjq7B}zB}U0RL!(y?D;=w{4&x0y+ykhHl?lR zpEU0>erf#Yv?ox(enx;tE8MkwF%K(t%@Q70D;$Rp{&uhyeTSRo6MiQ|A#p+(@}wja zp^S7=l8I1;3n|G&DC3%xWFnN|OiD5l$|xo!nFwX@l9Eh>GG<9hCgR}5B^iP(l=0U} zG7-j6*h+CqMY5@n9i|1|*o#<74Fhy^U*5^XB26sgLsN4rA^TaX4Ggo?k4sI){iw+p zJhf^R38ZH7ME5&eI+9(7A8x!i$LlUYcpcC5qmP5MD@4Zni$G|&iWS9Ff|xE4)l0-z zasopK(*> zD;6r~$YKFE&<;x`L^V@a&DZ7ml8Va91$3d3mUCPml{wo+aHJwYqg}>_0AJ{;zBR6A zD<~gW!w_o-O);n7;j^v6c6Bn3N~A}XDUMXD=!jA{3Nh;9i$A-!X5GPTIV?D#Uad0L9UZ#h0(ytEcy)WRl8aMNgf14)tYU7ria7}H zloO6C8FcSDtX9&xXiKp0yLkllL_s_Qlm-`pcx4c)4TA7RHw0vYsENqP zgLdi1MJ99{G)(pdg1LcL;u*Ow=!rU3tBVK&ie)AYD1up|NNsf?!~(Bwt%_qSqF{>M zyEZLKb}dyd1m6^hDrZkpp)(i`$g5GaKB~a#}D_X7qxoHrVt|HNvh$H#YQ*`uQV% z|Np5(=co3w$69JG5g-CYfCvx)B0vO)01+SpM1TkofdiGm>)^i>@V_!hM*mHq;~@B( zg1=q)>{}lE@y9PX|MOe^@%UqYUw!44 z8UOkp{!@SS_x}SA?s#JcE;R7fA3Vjb*RlA7-hK7={|5Z>pML+(sM3oF5CI}U1c(3; zAOb{y2oM1xKm>@uUjc#F8Hd6b4CgIlzx|KG3W(SO3HURBdsn=2Yx5(|eR}gpy0-R? zIpqznef{}cFU(B;g7OQ+@BhT`lFC9E@XfIzo6r$MCWsz-|D=h^KG4% zboO*McRbed02t{-1c(3;AOb{y2oM1xKm>>Y5g-CY;6);^VnG}K$~2w*&uvTB-G0rr z*`Ig*^r*KU_D|U-#vj}b7tQ{rsWt$gE7Ib?RQBh0^}T)lvtjlJJ$GK%)BNArUp{>w zJ0Sb$Z@D7rktudyI+@Hqo_q55ZD;&0`}m{pfAphQ?Z`gy*5^OOYD$Wl+Okhvc-#+u zf8VLuuO9pBpSk>Ox$v#p1 z*$#H#lRuhdpDS9$fi2m`jz8pur#7Us-+I-dpZU@HbFx4C=|>-B2ma{Ehj+6BTf~9Q z+4~pYeD&I+C$qo#=j(s4{r+!ffAj0h_<_$}eGU6u(JT&Z%6{;nYu@$TGoQ`=_J#A` z_rWLb&OV(;WZ;18bDwx_2Yepi)COIJ&dzdNo6!beM5j~i`9HCkz0r#Z5CI}U1c(3; zAOb{y2oM1xKm>>Y5jao@u;>4_MY|H6)s7!_oYDT7_AbcKiwF<_B0vO)01+SpM1Tko z0V43?5SUofw%KUuF_xtHi~U!aCgZ##(hxCCzFOda(y;$GV@N~YXS#LpCpSKK#K29L z_rCW(&i|Vip5HS1!}Cv1^<@%YG3V20V`b~djdjOmCnpYVozkY{JloX5+VuVN?!WIn zmsqd8?E~wcfA$X-{Bsi8^uB=$k1t&@pLW5qs%cAFrZjE7xSCnh7Wby^xa!GwZ~fdU ze@>qB#IFx)gQh*deLl@nZCc!1-zH2$QiH z{sVssd-~qA>8a6$-~GzLM{oY<3!8HzlP??zO}Z)ZrMWhT9Tzu=KmQxc6N~Ohf4k+M zS`KL*FqRujjLq|@>_9s<#X5A=v4B)#wMcOWc$a}0H3pu3B%!Z)=gO{H9s5G;RMasq8DMeBI@Pq91ZhQ?lp7bhRS_tW zTdn2RXt}joZXM5cPh7Mt+11@`>=f$3x(2nLJ4|yMy{z7SRA~;$th)JaRv`>RKN6$0 z)nN*NZxwR7kT|p0#W8ir$cSCc^kZwW2X?TR+mw! z<p*yU;!kfIkZS{t;>gHal_%q}8FZ>VM3ef^T9$*!eKja$=vj)E*I{5w=@ z7#QkX6y^eu8b@Np_@bIjI8YL7Jm1|0nRtHSo&+Hh$JW6QX=<)qa5t`kYZAw~JetPk zh(%t0R&6IvnqyJ22FlV!X%0fm>y0vw$f&AWz7H4nsgC4Xp_~x>Oe99k3+4Rag-Do~ z7s~0v3(>sgg>v@rLL}wQ3*}_ug-F($7mB&nli`JEZu7$RxGU@Hy0QTmZa{^woFR-X z7n7{S!|Z_2d9OOj<5}UPtO#*qMKM!IBWkQDCJJf9j1@_qv#*zpB&CaHXo0r7Y9lwe zBWUfeSBwq=v~Z6VB0zbd5y)AW2N);=CCc}pL}qGmQ&13jE{0gF73E=eFgW6bh@&td zdcuI12?HV}49KH|0eO)yAkPs7?>|@PJ&imb0T=O`YQ6rJ{&4Gu^1PwJ$8@_|SlsSZUAa7AIs2FXl1&ue8Y`7TFAbYc5^F746^E$-TPbnnm`r zO!H}GnU70X{+BoVxjLm6b#XZQw57q8mg(KJP~jT|!HeC5)NrfC0L}lN%s5+^t6J>Qb#CgoPQi6b>UcXG^tC~R zJocNOj5#u<@5yYis0zGd*^-;9VU*@}wM8hHm4fS`o2;kG;7Lu^;5o2F3U?k>$@xzi zuO!EBI8NEhXe(>Y5g-CYfCvzQsRZcxe<~BQ5CI}U1c(3;AOb{y2oM1xKm>>Y5jfBY zu;+iH^Zo?D zG4$7Db8|Kq{&@hJp1u8)&t%{;ecEHIKd=K5r#-WB{T1*jp8qdRbY2P>dJzF4Km>>Y z5g-CYfCvx)B0vO)01+Sp^B^#>!Z;Hi&)Ewe(39|jhxDWo$ydT-dm0K-Y5BySM!J@) z3_r$CE^jU#+|-y{&I+{sCmxK~W-l=gCHI zTqWw`rAnLzbWyekQlp_K zGes^&7?7fpFd#)GVL*yX!hjT&gaIikO|Mf?)QSS8glMVO?WG{dODVR+u7$eJbhzY* zu_6S470Cf)MMz+Z70K~rMM!Fj70KbP(=4eM;+Z0o6YF*`ohR^ip&v4hxC>I#V=lG6 zC&NBAh>yPB=(8`=z3Z66`NX{DBrcd!Eac(eQFRkDKc9&Sx`56IQh);86=+Q)H^hXo zhH6pUb-|UHrXZHRtJm0Lh?SFRVqUePoU8MmD0}Iwvka=5x!Ul~Hi+F5ryRz`x^|w* zM+qbu3q$-Zqm7g>t<*&vr^py0-$06nVwx+EGf6$o-uUi`3zmx(zn!<(tC&v6-;UH; z%uTa;SG`L$?g8^HP@a>{W%^|+RC&=qq&BB_a4uac9?B&=cQuQt0!p4$6C{Y>$MQj6 z59MB|by6>zwYO+cP)EH%*Ri5mWWjY~IZn6gna=j81RR5jXgN`qEx;h(CRCxO@iIF) zC=yfZzRvC9g0?qr;N1+|3LGHbU!iqN*y_7uhmK!G%1Mwf>iKe@=S$o5yyMrf;4Pew z^PyXD^8@MFbFSgU}jlN@}vQkHj1vrs4pQb~(vq?#5~RoWh%0R?e--16kfwMyO} ziVlGbn6e%(P!}_qx|F=I>6yiCY8H8cmNjiHYk7rAse*K^?2w5>#j;ifC#>XH17Zub zN6C2xqk8SdLP4t@`V^xA9jcn?BZWsWKzDRQtr#!Oz6oM{@nsq4Q1;ydZ7o0-^|)LB zePYxFCL|0M>*`v}=mk@4wqs8<7Bxw?o(yaZVeg|KXF6pIi^@TJ2Gwd07{_>4b->nS zxj-PSXxCK#yg)0i6dhkP*cHq4(ZJY)UO}C07p?pVLI6@>!LX0y$4nbWQqvnO+2Bmr zQDAT?c9ofgH|I?@?J6OxQW;?lZ5ugo41_1 zHKU}p>u>Y5g-CYfCvx)B0vO)01;?JfIa`WG(DYI z@ZGPSD2P^;Rq`f6NhRaRPvk1NE-fiRd; zj?RIUVq8-qW3>>QnKD?^h|o8YZ`hzWrQFN}87Y+uwH%X5L<)FJjZzAjvLYn@#EPW! z6DvZ(PpnAFKCvPs`oxN)LP5j zE-*+e5R~???j}+L@|<{y7*YCmmZQVca&TB$jtxu8pvnxmn)z$S;3mKjbDBXuq^=YW^DLy{O*G}@IvohT~dtS-aH?La9y)4h1 zjK3YH`4~6N$Hhvz=yKRlba^_RN$%*~$BM zv5XAt_K+LW^ahKCTAgLC_f|W;5h{H}&x~)5aSHSb2jvZ~Y*yo&pO*^?gH|ybLtD$$ zw49x*f@W)cm-7ptY#N;g_g5$96+$`Dc^=B;y_R1*r|ytZKHlE&2BGpOAD7pPHbW|I zuVbiU(6VIr+Nc0$YGxsab~gYAXG2uP)G%`~C!UL$LQX5nsS?#2v<(P@(2AiTJ8TtV zaCir0=h!k6w?C-5ZoG$6yC#9FD78=pojA+9EeE-HA*>TqF1P`VjM~oFPA$LPGeNFA zKEt;Pp%5pIO;hgMns#^*?}SNKoasWc%@x1@^<>z#YagkKN3Jo=njP;Uy6N}-`~7n* zb$|#E0U|&IhyW2F0z`la5CI}U1c<<_1nBvHRz!3F5g-CYfCvx)B0vO)01+SpM1Tko zf&HEUJ^%0bLr5JU0z`la5CI}U1c(3;AOb{y2oM1xFe?Fi{+|^Q9Y6$#01+SpM1Tko z0U|&IhyW2F0z_cHC(yp|&O|bCb0YbLwv$?SH=Wec)xIThbNjV(>==)!w(w5liLN7$ zG_E~d#6DtCX{?G>wPoTYE#yjUZWwrF*YXv~uA`1JuD()K=4>nb9)D4q-! zG+Z51$v9-1Cu1~0u}a2a(>xjA3W`-S4xQ%7kXKNwl5zMnPX@t)VwH4c(vxAapjagx zne=3!EGSk<5k8~>p+scpEGSk<2q_}s+;}qH6cnqZBa@ztKn2Ar>ByuP$E1RCly>5@ z7e}{ZM__a^?e!^QV<_#!X|GQ~8$)R)PJ4Y47Q2c{J8|0Ulf2jwD(%E+uTRk%Lun^Y zdwr4`t3#!oIPLXGaO?<`cH*?xC)u$hRN9Htvrqd*L9|drvhJZ{l3j-%ZcLoS=VIO; zViBdHj}x^y85!#*C0ZTgs4zX5hI6QU*P^5O)vk7$e>;Nyh4t8ruf)usf} zM3=YqAY?WULI%B(s}M8}t-x+Y3n}E_GFx^zULxZ)MJ)z)Fp^hQv#3gPNnmN|xGXJQ zm!+kXvb1zTmX^-O($d9PS~^fz%=^)7M58zj?Y<6#6&FHNiIomw zQpITqpoMZc9;%AMIgFEWz%6eyz6)wW*M{S}W24zwgyGIX9kJ7KgFAwpmc@BO0q>Y z5g-CYfCvx)B0vP@Mu49G=f;VSA_7E!2oM1xKm>>Y5g-CYfCvx)BCtObpy&VndGx65 zM1Tko0U|&IhyW2F0z`la5CI}U1m;G7p8w~@iH;%yM1Tko0U|&IhyW2F0z`la5CI~v zKNDcj|E-DtNVI=7eOFt)*re69jpsH+T)h* zmMUgh9bo6`^L48BwYmdpxw-?Yt^*v9j<7Hw^rP`QUw+Uqy;Pt zNC8+F)~l`w!vGwmP>d3`KgT1)<>@T4P~PChzCKe@YeP zAz%X4Xw6je%|aMmgP)D4@;t)I^N1_YqoL<{H1a%;2A=29xbr+3cAiJ09*smimBm0w zZoITF9@QwTmGvjr53F!VmFhb4L!~laJBCUX*F>wwP${VC*})M9)$KX41p2T_ZnsLZ z-;k8+s8TB*R?buOB3{MeC}G3miCU`(p4)-84!S;S8}tj-Nmz)~$)4jDEt&1UX6p&b zu4T)N8>)O6uGo&nepYEqFtbcqfOA1;V(nmCSH!ik9B##O%Hg1N0y)KCY-BrnGM-xz zV%j~KQM_!9258od258ER257#F257PrT&LJ8=P{)FP`V$bSE2OkC=Gow3sUc$XQaYA z&-6!~;F(o86YI7Y@~T$>>R}L~`;q6n+iWyY(Q=coxcQ!p3A07jG42BW*pGcPv>mh# z*;TKAN~0D@_sU0kSm2|zPbTs*0lMo`RF%z&8wAlAQ^c!<0gXxpIwID|q8VDC(5%|S z4X{os3lpeE>mpOUmn~sMs9IJe*|Q=v^sGpZ1S>+0z>4I!up;CZtSBBONTcPH6|IRn z%+5k9D=S)yiqIm?(2@|=JV(#u?woYq0fH$wRyxWv(6)=2W@K;H>O#jCMo8cJ@U4;<-D=(ptcZkXRiX`GPi7dUJ*n_rk^U2Ua(pXZbaLn+n>5Ph z!I!sC*v$!_%@<60kXZ?3ZIG1SkylBuO2+`JOwPfF`3)1fm^cMFmM|c95(eZ%!hl>y z7?8uLnKJHqe$+ik%IRvuWGuutT9K1?M;r9jtE!CWWEm3|S7jn6%L31`e*1w=JhPBUDi-ddXQdLr~f{UcDonEw^mbnNAxNCC_U7d2aLsu zYIG>l9TL@aqs5T`78P!OkVQyylUvUJR>vPTkDarafLRwlcSIKsG2 zJeP&O=>%}Ym!G}bIw7s|Su0BV?Y9)K79});Lwr*X<3mDpNKhEGin;1RbqOg}R4 z)j_NW)2=&H7AUJC@3efHMH$De%3FRTXB4vs-7ACv-64eGMW3|wduef5^aK?(Y-&@d zw6$+aWBaBoF@5nG(@nqspW&MhDk1_zfCvx)B0vO)01+SpM1Tko0U~gK5TNJ(17skn z8$^Hz5CI}U1c(3;AOb{y2oM1xKm=wWz@GmXEc!#D^S;hEcKo^HfeyRlsP><>*V@;B zkzPcA2oM1xKm>>Y5g-CYfCvzQ{hh#R3l23}QYj+^uVgZnWS>T}0k8C{?p^$*$?Qte z!))IGPh_9}^Y3>=s8-Pj%vMEu?L(ymU zxhVgIPo`O_Is2XE8?S)G?ygeFgU^BJlT|!9d0G;+mGw66Ya3kZ^mYAH(;w4Vv20GC6rhn>s(4Or!H#;rt#r|GVt`-iLbOBzQe^!WH~8`|x2O8h}(Q z)RRt6PM+V2>%r}4fqHg8J)dUvtUT>o{Pe_WkBZNMMdI_JKWw-x1IZSsudQuza!X58 z9~%N~44a|83!uImCLelLbnf;Gcw%>JX`I#340SX&Pfot6S=7OY5$Z_C!_f4qdzUPR zItJM=tdIS{-p%V7NPOa0mU!ryCq9{BpU zj>L_L1*!DH)RoC!C*R%naO-W&4;aTMZtTo;JkoJ}$NHCs0{`zV69=zNcJ=ic6Blu@ zsKSWPKXW!nTogjPW=*vWXu=j#%QGv%=37pfi>h*p)|k?wu~l-LO2*d{+8S3cUX$!v zvBG$Vs6(qz%Vg^tqb@u}Y6J8IBbkaC2$D(B8J3npA1p1UK3G}`hOo4h8DeRvE5y=L zNQk9{a!^l(rPoL58Z5m5rTerC<12%bgitJDr+|nA%E*h`Wjj`~Vrh9gOUtuaTAs|( z_*~g=W|s|TY1wd=mJMfV*>IMQ8_qNM%5lSa3BEQ6Ux;4t5`6VOeEB|n{XX1)KHP$S ziH_CQk6X}>JKv96(2rZtk6X}>ThNbN(2rZtFIv#O>&>gVbltVfxLcR2MNqdC@26=_ zC~Q-_&kkcXw1Qg&oiw4i;#eiysvy}gCm6{GA(8`gxUMHQFfkdN513dTGC&JJ_6Mm! zfuU+vDlU>0bH>Y>6qO$i`c^RLLQ}+uF@?BuxyakO?x^KoTvt3)(?z9*Kp0T3Ky-}h z(C9~&?6D~gtQCh!Oe`yxfmJ$$@`WMD;Yy`KUU8@rS2|RY zm6m-^wRODeM>NN)Qg3Q|Y}+2$x+hNW=i+EPhXkz}NYcX3t8`LenU#|il9a8<_vYj@ zhoqe3kd#v#l5&DWQciD3%E=8$Ikh1vCpIMIw1#9fso&Jc$9!U`=mnccy?i$HZqj?g zOXk^^?um|GQT-^do=Hvy!{U9TR=u#zTK)FkSS$>Z@#rX(&+UE6s?cXi3QYu-4b;3~S+qWlmzCICITm{x zFRO~namC|h)rfM`h;H{J;TkWQs5AU8+=rN`GpRGjHyNRujMTb>Ar|ZE$p`}y1QZ4& z2q+9l5KtJ9POmT^Z81`3MiYtDnJYb+IjA#Vu(7S{m}3l^FWf>gHpkTVQ1Wzb*+d0u z@$AXO3Vq#YY-nThg|Jb~SFF724pm`GBtKuR)g`T&l|$C{XlKC|M7BJ3Pb^q3mNJ*u zB({CxeT!C0w9{Rgvy13?Fs$D=IU`lI?81|ySia%9(Y_Saa>EhOJC3rhQ&#!ziC3;m zcAa>lF#%G>&@GS+h6Fr{ifh+pH*3wSD^$>gJiXp5zG**06PS<5!m?qO9n~mM|8>f+ z`DYC#{lAyy%MH2;5g-CYfCvx)B0vO)01+SpM1TkoftQs4{r&&Tik~h{1c(3;AOb{y z2oM1xKm>>Y5g-CY;3XkI&;Kt8X1WLwAOb{y2oM1xKm>>Y5g-CYfCvzQmz4lL|G%vG z>Ec9y2oM1xKm>>Y5g-CYfCvx)B0vOQ5(4!6|B_&)ix2@KKm>>Y5g-CYfCvx)B0vO) z01E=~l901+SpM1Tko0U|&IhyW2F z0z}{?AwbXnF9~M42oWFxM1Tko0U|&IhyW2F0z`la5P_GK0DJyl)Sgdt{%hwgop0zo zy5q@?&vcA;Z0T6g{z&`#p@3dQfCvx)B0vO)01+SpM1Tko0U|I*0&5pF8^a4PG(+1d zXZrf*cvd2@b|IX%K%Un(_wyFOd1-lG@7&Kz!+ELbybW_cEwMHQ=Ov@_*3aX-B%Ie4 zowsft=e5Ckt