diff --git a/cloud/servers/version.gridlabd.us/aws_requirements.txt b/cloud/servers/version.gridlabd.us/aws_requirements.txt index 641d3ac76..c5c0a3b6f 100644 --- a/cloud/servers/version.gridlabd.us/aws_requirements.txt +++ b/cloud/servers/version.gridlabd.us/aws_requirements.txt @@ -1 +1,2 @@ pandas>=1.3.5 +numpy>=1.22.2 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/converters/Makefile.mk b/converters/Makefile.mk index 4d071b29b..1945af3cb 100644 --- a/converters/Makefile.mk +++ b/converters/Makefile.mk @@ -31,6 +31,9 @@ dist_pkgdata_DATA += converters/mdb-table2glm-player.py # omd -> glm dist_pkgdata_DATA += converters/omd2glm.py +# py->glm +dist_pkgdata_DATA += converters/py2glm.py + # tmy3 -> glm dist_pkgdata_DATA += converters/tmy32glm.py diff --git a/converters/autotest/.gitignore b/converters/autotest/.gitignore index 48c76d731..19f417ff7 100644 --- a/converters/autotest/.gitignore +++ b/converters/autotest/.gitignore @@ -9,3 +9,5 @@ solver_nr_profile.csv table2glm_input_noclass.glm table2glm_input_noname.glm table2glm_input.glm +pypower_casedata.py +pypower_results.py diff --git a/converters/csv-ami2glm-player.py b/converters/csv-ami2glm-player.py index 50625f238..447e8eb41 100644 --- a/converters/csv-ami2glm-player.py +++ b/converters/csv-ami2glm-player.py @@ -50,9 +50,19 @@ def write_player(file, obj, node_ID, phase) : file.write('object player {\n') file.write('\tparent "' + obj + '";\n') file.write('\tfile "' + os.path.join(folder_name,str(node_ID)) + '.csv";\n') - file.write(f'\tproperty constant_power_{phase};\n') + for p in phase : + file.write(f'\tproperty constant_power_{p};\n') file.write('}\n') +def filter_dict_by_min_value(input_dict, patterns): + result_dict = {} + for pattern in patterns: + pattern_dictionary = {key: value for key, value in input_dict.items() if pattern in key} + min_value = min(pattern_dictionary.values()) + min_dict = {key: value for key, value in pattern_dictionary.items() if value == min_value} + result_dict.update(min_dict) + return list(result_dict.keys()) + def convert(input_files, output_file, options={}): if type(input_files) is dict: @@ -106,6 +116,8 @@ def convert(input_files, output_file, options={}): df.to_csv(os.path.join(folder_name,os.path.basename(output_file)), index=False) elif os.path.splitext(output_file)[1]=='.glm' : phase_dict = {} + load_list = {} + load_list_filtered = {} with open(output_file, mode='w') as file : file.write('module tape;\n') @@ -114,11 +126,27 @@ def convert(input_files, output_file, options={}): continue for obj,val in network["objects"].items() : if "load" in val["class"] and node_ID in obj: - node_phase = ''.join([x for x in 'ABC' if x in val['phases']]) + volts = float(val['nominal_voltage'].split(' ')[0]) + if 'k' in val['nominal_voltage'].split(' ')[1] : + load_list[obj] = volts*1000 + elif 'M' in val['nominal_voltage'].split(' ')[1] : + load_list[obj] = volts*1000000 + else : + load_list[obj] = volts + load_phase = ''.join([x for x in 'ABC' if x in val['phases']]) + phase_dict[node_ID]=load_phase + + + # Grabbing only loads on the low side of the Transformer + load_list_filtered = filter_dict_by_min_value(load_list,node_ID_set) + for load_ID in load_list_filtered : + for obj, val in network["objects"].items() : + if load_ID==obj : + load_phase = ''.join([x for x in 'ABC' if x in val['phases']]) parent = val["parent"] - phase_dict[node_ID]=node_phase - for p in node_phase : - write_player(file, obj, node_ID, p) + for node_ID in node_ID_set : + if node_ID in load_ID : + write_player(file, obj, node_ID, load_phase) new_column_names = { 'reading_dttm': 'timestamp', diff --git a/converters/py2glm.py b/converters/py2glm.py new file mode 100644 index 000000000..4bae44384 --- /dev/null +++ b/converters/py2glm.py @@ -0,0 +1,133 @@ +import json +import os +import sys, getopt +import datetime +import importlib, copy +from importlib import util + + +config = {"input":"py","output":"glm","type":["pypower"]} + +def help(): + return """py2glm.py -i <inputfile> -o <outputfile> [options...] + -c|--config output converter configuration data + -h|--help output this help + -i|--ifile <filename> [REQUIRED] PY input file + -o|--ofile <filename> [OPTIONAL] GLM output file name + -t|--type type of input file + -N|--name do not autoname objects +""" + +def main(): + filename_py = None + filename_glm = None + py_type = 'pypower' + autoname = True + try : + opts, args = getopt.getopt(sys.argv[1:], + "chi:o:t:N", + ["config","help","ifile=","ofile=","type=","name"], + ) + except getopt.GetoptError: + sys.exit(2) + if not opts : + print('ERROR [py2glm.py]: missing command arguments') + sys.exit(2) + for opt, arg in opts: + if opt in ("-c","--config"): + print(config) + sys.exit() + elif opt in ("-h","--help"): + print(help()) + sys.exit() + elif opt in ("-i", "--ifile"): + filename_py = arg + elif opt in ("-o", "--ofile"): + filename_glm = arg + elif opt in ("-t", "--type"): + py_type = arg + elif opt in ("-N","--name"): + autoname = False + else : + print(f"ERROR [py2glm.py]: {opt}={arg} is not a valid option") + sys.exit(1) + + if not filename_py: + print(f"ERROR [py2glm.py]: input filename not specified") + sys.exit(1) + + try: + convert( + ifile = filename_py, + ofile = filename_glm, + options = dict( + py_type = py_type, + autoname = autoname), + ) + except Exception as err: + print(f"ERROR [py2glm.py]: {err}") + import traceback + traceback.print_exception(err,file=sys.stderr) + sys.exit(9) + +def convert(ifile,ofile,options={}): + """Default converter is pypower case""" + + py_type = options['py_type'] if 'py_type' in options else "pypower" + autoname = options['autoname'] if 'autoname' in options else True + + assert(py_type in ['pypower']) + + modspec = util.spec_from_file_location("glm",ifile) + modname = os.path.splitext(ifile)[0] + mod = importlib.import_module(os.path.basename(modname)) + casedef = getattr(mod,os.path.basename(modname)) + data = casedef() + + NL='\n' + with open(ofile,"w") as glm: + glm.write(f"""// generated by {' '.join(sys.argv)} +module pypower +{{ + version {data['version']}; + baseMVA {data['baseMVA']}; +}} +""") + + for name,spec in dict( + # pypower properties must be in the save order as the case array columns + bus = "bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin", + gen = "bus Pg Qg Qmax Qmin Vg mBase status Pmax Pmin Pc1 Pc2 Qc1min"\ + + " Qc1max Qc2min Qc2max ramp_agc ramp_10 ramp_30 ramp_q apf", + branch = "fbus tbus r x b rateA rateB rateC ratio angle status angmin angmax", + ).items(): + glm.write(f"{NL}//{NL}// {name}{NL}//{NL}") + for n,line in enumerate(data[name]): + oname = f"{NL} name pp_{name}_{n+1};" if autoname else "" + glm.write(f"""object pypower.{name} +{{{oname} +{NL.join([f" {x} {line[n]};" for n,x in enumerate(spec.split())])} +}} +""") + if 'gencost' in data: + glm.write("\n//\n// gencost\n//\n") + for n,line in enumerate(data['gencost']): + model = line[0] + startup = line[1] + shutdown = line[2] + count = line[3] + costs = line[4:] + assert(len(costs)==count) + oname = f"{NL} name pp_gencost_{n};" if autoname else "" + glm.write(f"""object pypower.gencost +{{{oname} + model {int(model)}; + startup {startup}; + shutdown {shutdown}; + costs "{','.join([str(x) for x in costs])}"; +}} +""") + +if __name__ == '__main__': + main() + diff --git a/docs/Converters/Import/PyPower_cases.md b/docs/Converters/Import/PyPower_cases.md new file mode 100644 index 000000000..ead0b1a18 --- /dev/null +++ b/docs/Converters/Import/PyPower_cases.md @@ -0,0 +1,24 @@ +[[/Converters/Import/Ami_data]] -- AMI data import + +# Synopsis + +GLM: + +~~~ +#input "casefile.py" -t pypower [-N|--name] +~~~ + +Shell: + +~~~ +$ gridlabd convert -i inputfile.py -o outputfile.glm -t pypower [-N|--name] +~~~ + +# Description + +The `py2glm.py` converter support conversion of PyPower case files to GLM +models. The `-N|--name` option suppresses autonaming of PyPower objects. + +# See also + +* [[/Module/Pypower]] diff --git a/docs/Module/Pypower.md b/docs/Module/Pypower.md new file mode 100644 index 000000000..04efa4e01 --- /dev/null +++ b/docs/Module/Pypower.md @@ -0,0 +1,259 @@ +[[/Module/Pypower]] -- Module pypower + +# Synopsis + +GLM: + +~~~ +module pypower +{ + set {QUIET=65536, WARNING=131072, DEBUG=262144, VERBOSE=524288} message_flags; // module message control flags + int32 version; // Version of pypower used (default is 2) + enumeration {NR=1, FD_XB=2, FD_BX=3, GS=4} solver_method; // PyPower solver method to use + int32 maximum_timestep; // Maximum timestep allowed between solutions (default is 0, meaning no maximum timestep) + double baseMVA[MVA]; // Base MVA value (default is 100 MVA) + bool enable_opf; // Flag to enable solving optimal powerflow problem instead of just powerflow (default is FALSE) + bool stop_on_failure; // Flag to stop simulation on solver failure (default is FALSE) + bool save_case; // Flag to enable saving case data and results (default is FALSE) + char1024 controllers; // Python module containing controller functions + double solver_update_resolution; // Minimum difference before a value is considered changed + enumeration {INIT=0, SUCCESS=1, FAILED=2} solver_status; // Result of the last pypower solver run +} +~~~ + +# Description + +The `pypower` module links `gridlabd` with the `pypower` powerflow solver. The +objects used to link the two solvers are supported by the `bus`, `branch`, +and `gen` classes. For details on these objects' properties, see the +[PyPower documentation]([https://pypi.org/project/PYPOWER/). + +If `enable_opf` is `TRUE`, then the OPF solver is used when `gencost` objects +are defined. + +If `save_case` is `TRUE`, then the case data and solver results are stored in +`pypower_casedata.py` and `pypower_results.py` files. + +If you have convergence iteration limit issues when larger models, try +increasing the value of `solver_update_resolution`. The larger this value +is, the larger a difference between an old value and new value from the +solver must be to be considered a change necessitating additional iteration. +The default value is `1e-8`, which should be sufficient for most models. + +The following `pypower` data elements are implemented using the corresponding +GridLAB-D `pypower` module classes. + +## Bus Objects + +~~~ +class bus { + int32 bus_i; // bus number (1 to 29997) + complex S[MVA]; // base load demand not counting child objects, copied from Pd,Qd by default (MVA) + enumeration {PQREF=1, NONE=4, REF=3, PV=2, PQ=1, UNKNOWN=0} type; // bus type (1 = PQ, 2 = PV, 3 = ref, 4 = isolated) + double Pd[MW]; // real power demand (MW) + double Qd[MVAr]; // reactive power demand (MVAr) + double Gs[MW]; // shunt conductance (MW at V = 1.0 p.u.) + double Bs[MVAr]; // shunt susceptance (MVAr at V = 1.0 p.u.) + int32 area; // area number, 1-100 + double baseKV[kV]; // voltage magnitude (p.u.) + double Vm[pu*V]; // voltage angle (degrees) + double Va[deg]; // base voltage (kV) + int32 zone; // loss zone (1-999) + double Vmax[pu*V]; // maximum voltage magnitude (p.u.) + double Vmin[pu*V]; // minimum voltage magnitude (p.u.) + double lam_P; // Lagrange multiplier on real power mismatch (u/MW) + double lam_Q; // Lagrange multiplier on reactive power mismatch (u/MVAr) + double mu_Vmax; // Kuhn-Tucker multiplier on upper voltage limit (u/p.u.) + double mu_Vmin; // Kuhn-Tucker multiplier on lower voltage limit (u/p.u.) +} +~~~ + +## Branch Objects + +~~~ +class branch { + int32 fbus; // from bus number + int32 tbus; // to bus number + double r[pu*Ohm]; // resistance (p.u.) + double x[pu*Ohm]; // reactance (p.u.) + double b[pu/Ohm]; // total line charging susceptance (p.u.) + double rateA[MVA]; // MVA rating A (long term rating) + double rateB[MVA]; // MVA rating B (short term rating) + double rateC[MVA]; // MVA rating C (emergency term rating) + double ratio[pu]; // transformer off nominal turns ratio + double angle[pu]; // transformer phase shift angle (degrees) + enumeration {IN=1, OUT=0} status; // initial branch status, 1 - in service, 0 - out of service + double angmin[deg]; // minimum angle difference, angle(Vf) - angle(Vt) (degrees) + double angmax[deg]; // maximum angle difference, angle(Vf) - angle(Vt) (degrees) +} +~~~ + +## Generator Objects + +~~~ +class gen { + int32 bus; // bus number + double Pg[MW]; // real power output (MW) + double Qg[MVAr]; // reactive power output (MVAr) + double Qmax[MVAr]; // maximum reactive power output (MVAr) + double Qmin[MVAr]; // minimum reactive power output (MVAr) + double Vg[pu*V]; // voltage magnitude setpoint (p.u.) + double mBase[MVA]; // total MVA base of machine, defaults to baseMVA + enumeration {OUT_OF_SERVICE=0, IN_SERVICE=1} status; // 1 - in service, 0 - out of service + double Pmax[MW]; // maximum real power output (MW) + double Pmin[MW]; // minimum real power output (MW) + double Pc1[MW]; // lower real power output of PQ capability curve (MW) + double Pc2[MW]; // upper real power output of PQ capability curve (MW) + double Qc1min[MVAr]; // minimum reactive power output at Pc1 (MVAr) + double Qc1max[MVAr]; // maximum reactive power output at Pc1 (MVAr) + double Qc2min[MVAr]; // minimum reactive power output at Pc2 (MVAr) + double Qc2max[MVAr]; // maximum reactive power output at Pc2 (MVAr) + double ramp_agc[MW/min]; // ramp rate for load following/AGC (MW/min) + double ramp_10[MW]; // ramp rate for 10 minute reserves (MW) + double ramp_30[MW]; // ramp rate for 30 minute reserves (MW) + double ramp_q[MVAr/min]; // ramp rate for reactive power (2 sec timescale) (MVAr/min) + double apf; // area participation factor + double mu_Pmax[pu/MW]; // Kuhn-Tucker multiplier on upper Pg limit (p.u./MW) + double mu_Pmin[pu/MW]; // Kuhn-Tucker multiplier on lower Pg limit (p.u./MW) + double mu_Qmax[pu/MVAr]; // Kuhn-Tucker multiplier on upper Qg limit (p.u./MVAr) + double mu_Qmin[pu/MVAr]; // Kuhn-Tucker multiplier on lower Qg limit (p.u./MVAr) +} +~~~ + +## Generator Cost Objects + +~~~ +class gencost { + enumeration {POLYNOMIAL=2, PIECEWISE=1, UNKNOWN=0} model; // cost model (1=piecewise linear, 2=polynomial) + double startup[$]; // startup cost ($) + double shutdown[$]; // shutdown cost($) + char1024 costs; // cost model (comma-separate values) +} +~~~ + +# Integration Objects + +Integration objects are used to link assets and control models with `pypower` +objects. An integrated object specified its parent `bus` or `gen` object and +updates it as needed prior to solving the powerflow problem. + +## Loads + +~~~ +class load { + complex S[VA]; // power demand (VA) + complex Z[Ohm]; // constant impedance load (Ohm) + complex I[A]; // constant current load (A) + complex P[VA]; // constant power load (VA) + complex V[V]; // bus voltage (V) + double Vn[V]; // nominal voltage (V) + enumeration {CURTAILED=2, ONLINE=1, OFFLINE=0} status; // load status + double response[pu]; // curtailment response as fractional load reduction + char256 controller; // controller python function name +} +~~~ + +Using the `load` object allows integration of one or more quasi-static load +models with `bus` objects. The `ZIP` values are used to calculate the `S` +value based on the current voltage. When the load is `ONLINE`, the `S` +value's real and imaginary is then added to the parent `bus` object's `Pd` +and `Qd` values, respectively. When the load is `CURTAILED`, the load is +reduced by the fractional quantity specified by the `response` property. When +the load is `OFFLINE`, the values of `S` is zero regardless of the value of +`P`. + +## Powerplants + +~~~ +class powerplant { + char32 city; // City in which powerplant is located + char32 state; // State in which powerplant is located + char32 zipcode; // Zipcode in which powerplant is located + char32 country; // Country in which powerplant is located + char32 naics_code; // Powerplant NAICS code + char256 naics_description; // Powerplant NAICS description + int16 plant_code; // Generator plant code number + set {CC=1024, PV=512, CT=256, ES=128, WT=64, FW=32, IC=16, AT=8, ST=4, HT=2, UNKNOWN=1} generator; // Generator type + set {NG=32768, COAL=16384, WATER=8192, NUC=4096, GAS=2048, OTHER=1024, WOOD=512, UNKNOWN=256, OIL=128, BIO=64, WASTE=32, COKE=16, GEO=8, SUN=4, WIND=2, ELEC=1} fuel; // Generator fuel type + enumeration {ONLINE=1, OFFLINE=0} status; // Generator status + double operating_capacity[MW]; // Generator normal operating capacity (MW) + double summer_capacity[MW]; // Generator summer operating capacity (MW) + double winter_capacity[MW]; // Generator winter operating capacity (MW) + double capacity_factor[pu]; // Generator capacity factor (pu) + char256 substation_1; // Substation 1 id + char256 substation_2; // Substation 2 id + complex S[VA]; // power generation (VA) + char256 controller; // controller python function name +} +~~~ + +Using `powerplant` objects allows integration of one or more quasi-static +generator models with both `bus` and `gen` objects. When integrating with a +`bus` object, the `S` value real and imaginary values are added to the `bus` +properties `Pd` and `Qd`, respectively, when the plant is `ONLINE`. + +When integrated with a `gen` object, both the `Pd` and `Qd` values are updated +based on the powerplant's generator status and type. + +## Powerlines + +~~~ +class powerline { + double length[mile]; // (REQUIRED) length (miles) + complex impedance[Ohm/mile]; // (REQUIRED) line impedance (Ohm/mile) + enumeration {OUT=0, IN=1} status; // line status (IN or OUT) + enumeration {PARALLEL=2, SERIES=1} composition; // parent line composition (SERIES or PARALLEL) +} +~~~ + +Using `powerline` object allows composite lines to be constructed and +assembled into `branch` objects. A `powerline` may either have a `branch` +parent or another `powerline` object, in which case the parent must specify +whether its `composition` is either `SERIES` or `PARALLEL`. When a +`powerline` is not a composite line you must specify its `impedance` in Ohms +per mile and its length in `miles`. Only lines with `status` values `IN` are +assembled in the parent line. Line with `status` values `OUT` are ignored. + +The `status` value, `impedance`, `length`, and `composition` may be changed at +any time during a simulation. However, these values are only checked for +consistency and sanity during initialization. + +## Controllers + +Controllers may be added by specifying the `controllers` global in the +`pypower` module globals, e.g., + +~~~ +module pypower +{ + controllers "my_controllers"; +} +~~~ + +This will load the file `my_controllers.py` and link the functions defined in +it. + +If the `on_init` function is defined in the Python `controllers` module, it +will be called when the simulation is initialized. Note that many `gridlabd` +module functions are not available until after initialization is completed. + +Any `load` or `powerplant` object may specify a `controller` property. When +this property is defined, the corresponding controller function will be +called if it is defined in the `controllers` module. + +Controller functions use the following call/return prototype + +~~~ +def my_controller(obj,**kwargs): + return dict(name=value,...) +~~~ + +where `kwargs` contains a dictionary of properties for the object and `name` +is any valid property of the calling object. A special return name `t` is +used to specify the time at which the controller is to be called again, +specify in second of the Unix epoch. + +# See also + +* [PyPower documentation](https://pypi.org/project/PYPOWER/) +* [[/Converters/Import/PyPower_cases]] diff --git a/module/Makefile.mk b/module/Makefile.mk index 050d092c5..ab1b743cc 100644 --- a/module/Makefile.mk +++ b/module/Makefile.mk @@ -9,6 +9,7 @@ include module/market/Makefile.mk include module/mysql/Makefile.mk include module/optimize/Makefile.mk include module/powerflow/Makefile.mk +include module/pypower/Makefile.mk include module/reliability/Makefile.mk include module/residential/Makefile.mk include module/resilience/Makefile.mk diff --git a/module/powerflow/pole.cpp b/module/powerflow/pole.cpp index c02524c1c..f06034f00 100644 --- a/module/powerflow/pole.cpp +++ b/module/powerflow/pole.cpp @@ -575,23 +575,29 @@ TIMESTAMP pole::postsync(TIMESTAMP t0) //// down_time = gl_globalclock; verbose("down_time = %lld", down_time); } - - // // M = a * V^2 + b * V + c - // // TODO - // pole_stress_polynomial_a = pole_moment_per_wind + equipment_moment_nowind + wire_moment_nowind; - // pole_stress_polynomial_b = 0.0; - // pole_stress_polynomial_c = wire_tension; - - TIMESTAMP next_event = pole_status == PS_FAILED ? down_time + (int)(repair_time*3600) : TS_NEVER; - verbose("next_event = %lld", next_event); //// should return repair time recalc = false; - return stop_on_pole_failure && pole_status == PS_FAILED ? TS_INVALID : next_event; } else { verbose("no pole recalculation flagged"); - return TS_NEVER; } + + if ( pole_status == PS_FAILED ) + { + if ( stop_on_pole_failure ) + { + return TS_INVALID; + } + TIMESTAMP next_event = down_time + (int)(repair_time*3600); + if ( t0 == next_event ) + { + return TS_NEVER; + } + verbose("next_event = %lld", next_event); //// should return repair time + return next_event; + + } + return TS_NEVER; } TIMESTAMP pole::commit(TIMESTAMP t1, TIMESTAMP t2) diff --git a/module/powerflow/substation.cpp b/module/powerflow/substation.cpp index 02c32fc48..8e44c3709 100644 --- a/module/powerflow/substation.cpp +++ b/module/powerflow/substation.cpp @@ -243,6 +243,25 @@ int substation::init(OBJECT *parent) //Flag us as pw_load connected has_parent = 1; } + else if ( gl_object_isa(parent,"load","pypower") ) + { + fetch_complex(&pPositiveSequenceVoltage,"V",parent); + fetch_complex(&pConstantPowerLoad,"P",parent); + fetch_complex(&pConstantCurrentLoad,"I",parent); + fetch_complex(&pConstantImpedanceLoad,"Z",parent); + fetch_double(&pTransNominalVoltage,"Vn",parent); + if (fabs(*pTransNominalVoltage-nominal_voltage)>0.001) + { + gl_error("pypower load bus nominal voltage (Vn %.1f V) and substation (nominal_voltage %.1f V) do not match to within 0.001 V",*pTransNominalVoltage,nominal_voltage); + return 0; + } + if (bustype != SWING) + { + warning("substation attached to pypower load and not a SWING bus - forcing bustype SWING"); + bustype = SWING; + } + has_parent = 1; + } else //Parent isn't a pw_load, so we just become a normal node - let it handle things { has_parent = 2; //Flag as "normal" - let node checks sort out if we are really valid or not diff --git a/module/pypower/Makefile.mk b/module/pypower/Makefile.mk new file mode 100644 index 000000000..fe8e0a2d1 --- /dev/null +++ b/module/pypower/Makefile.mk @@ -0,0 +1,28 @@ +# module/pypower/Makefile.mk +# Copyright (C) 2024 Regents of the Leland Stanford Junior University + +pkglib_LTLIBRARIES += module/pypower/pypower.la + +module_pypower_pypower_la_CPPFLAGS = +module_pypower_pypower_la_CPPFLAGS += $(AM_CPPFLAGS) + +module_pypower_pypower_la_LDFLAGS = +module_pypower_pypower_la_LDFLAGS += $(AM_LDFLAGS) + +module_pypower_pypower_la_LIBADD = + +module_pypower_pypower_la_SOURCES = +module_pypower_pypower_la_SOURCES += module/pypower/pypower.cpp module/pypower/pypower.h + +module_pypower_pypower_la_SOURCES += module/pypower/branch.cpp module/pypower/branch.h +module_pypower_pypower_la_SOURCES += module/pypower/bus.cpp module/pypower/bus.h +module_pypower_pypower_la_SOURCES += module/pypower/gen.cpp module/pypower/gen.h +module_pypower_pypower_la_SOURCES += module/pypower/gencost.cpp module/pypower/gencost.h +module_pypower_pypower_la_SOURCES += module/pypower/load.cpp module/pypower/load.h +module_pypower_pypower_la_SOURCES += module/pypower/powerline.cpp module/pypower/powerline.h +module_pypower_pypower_la_SOURCES += module/pypower/powerplant.cpp module/pypower/powerplant.h +module_pypower_pypower_la_SOURCES += module/pypower/relay.cpp module/pypower/relay.h +module_pypower_pypower_la_SOURCES += module/pypower/scada.cpp module/pypower/scada.h +module_pypower_pypower_la_SOURCES += module/pypower/transformer.cpp module/pypower/transformer.h + +dist_pkgdata_DATA += module/pypower/pypower_solver.py diff --git a/module/pypower/README.md b/module/pypower/README.md new file mode 100644 index 000000000..fc6c43dec --- /dev/null +++ b/module/pypower/README.md @@ -0,0 +1,43 @@ +This module was developed by [David Chassin](https://github.com/dchassin) at SLAC National Accelerator Laboratory as part of the REGROW project (see https://github.com/slacgismo/regrow). + +# Theory of Operation + +The `pypower` module provides PF and OPF solvers that are suitable for reduced-order modeling of transmission and substransmission systems. The solvers are integrated at the module level, meaning that module event handlers initiate the powerflow solvers. The two events that are handled are the `on_init` and `on_sync` events. In addition class-level event handlers are implemented as follows. + +| Class | `create` | `init` | `precommit` | `presync` | `sync` | `postsync` | `commit` | +| ------------- | :------: | :----: | :---------: | :-------: | :----: | :--------: | :------: | +| Module | X | X | | | X [1] | | | +| `branch` | X | X | | | | | | +| `bus` | X | X | | X | | | | +| `gen` | X | X | | | | | | +| `gencost` | X | X | | | | | | +| `load` | X | X | | X | X | X | | +| `powerline` | X | X | X | | | | | +| `powerplant` | X | X | | X | X | X | | +| `relay` | X | X | X | | | | | +| `scada` | X | X | | | X | | | +| `transformer` | X | X | X | | | | | + +[1] Only called when `on_sync(data)` is defined in `controllers` python file. + +# Modeling + +The model has three layers. + +1. Solver layer (`bus`, `branch`, `gen`, and `gencost` classes) which are used to transfer data from the model to solver data arrays. +2. Model layer (`powerplant`, `powerline`, and `load` classes) which are used to implement model reductions from detailed system models to solver data. +3. Control layer (`controllers` python file) which are used to implement controls for specific model reductions classes. + +Data transfer from control layer to model layer are performed by passing object data to control functions defined in the `controllers` python file and copying returned values back to the model layer objects. Data transfer from the model layer to the solver layer is performed automatically by the `on_sync` solver code. Some model data is not copied unless the OPF solver is enabled and `gencost` objects are defined. + + + +Figure 1: Module structure diagram [[Edit](https://lucid.app/lucidspark/56584160-b3c6-4798-9558-ce9f991d4ce0/edit?viewport_loc=-701%2C54%2C3413%2C1701%2C0_0&invitationId=inv_66ba35f7-3f3c-4b15-8cde-31ac0933cf77)] + +# Control + +The `controllers` file may contain the following functions + +* `on_init()` which is called when the model is initialized at the start time of the simulation. +* `on_sync(data)` which is called when the timestep is advanced to the next simulation step. The `data` object contains the data which will be sent to the solver. This data may be changed. Caveat: this can seriously mess up the solver and cause it fail. +* `my_controller(obj,**kwargs)` which is called for any `load` or `powerplant` object which defines its `controller` property with the corresponding function name. The value `obj` contains the name of the object and `kwargs` contains a `dict` of the objects properties. The return value is a `dict` which can contain any property to be copied back to the object, as well as a time `t` at which the next event occurs, if any. diff --git a/module/pypower/autotest/.gitignore b/module/pypower/autotest/.gitignore new file mode 100644 index 000000000..ff7eee7a0 --- /dev/null +++ b/module/pypower/autotest/.gitignore @@ -0,0 +1,6 @@ +case*.glm +case*.json +case*_ref.json +gridlabd.diff +pypower_casedata.py +pypower_results.py diff --git a/module/pypower/autotest/case.glm b/module/pypower/autotest/case.glm new file mode 100644 index 000000000..b66c1200d --- /dev/null +++ b/module/pypower/autotest/case.glm @@ -0,0 +1,19 @@ +#ifexist ../case${CASE}.py +#system cp ../case${CASE}.py . +#define DIR=.. +#endif + +clock +{ + timezone "PST+8PDT"; + starttime "2020-01-01 00:00:00 PST"; + stoptime "2020-02-01 00:00:00 PST"; +} + +#input "case${CASE}.py" -t pypower + +#gridlabd -C "case${CASE}.glm" -D "starttime=${starttime}" -o "case${CASE}_ref.json" + +#set savefile=case${CASE}.json + +#on_exit 0 python3 ${DIR:-.}/check_case.py case${CASE}.json case${CASE}_ref.json pp_bus_1/Vm > gridlabd.diff diff --git a/module/pypower/autotest/case118.py b/module/pypower/autotest/case118.py new file mode 100644 index 000000000..169be9ada --- /dev/null +++ b/module/pypower/autotest/case118.py @@ -0,0 +1,469 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for IEEE 118 bus test case. +""" + +from numpy import array + +def case118(): + """Power flow data for IEEE 118 bus test case. + Please see L{caseformat} for details on the case file format. + + This data was converted from IEEE Common Data Format + (ieee118cdf.txt) on 20-Sep-2004 by cdf2matp, rev. 1.11 + See end of file for warnings generated during conversion. + + Converted from IEEE CDF file from: + U{http://www.ee.washington.edu/research/pstca/} + + With baseKV data take from the PSAP format file from the same site, + added manually on 10-Mar-2006. + + 08/25/93 UW ARCHIVE 100.0 1961 W IEEE 118 Bus Test Case + + @return: Power flow data for IEEE 118 bus test case. + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 2, 51, 27, 0, 0, 1, 0.955, 10.67, 138, 1, 1.06, 0.94], + [2, 1, 20, 9, 0, 0, 1, 0.971, 11.22, 138, 1, 1.06, 0.94], + [3, 1, 39, 10, 0, 0, 1, 0.968, 11.56, 138, 1, 1.06, 0.94], + [4, 2, 39, 12, 0, 0, 1, 0.998, 15.28, 138, 1, 1.06, 0.94], + [5, 1, 0, 0, 0, -40, 1, 1.002, 15.73, 138, 1, 1.06, 0.94], + [6, 2, 52, 22, 0, 0, 1, 0.99, 13, 138, 1, 1.06, 0.94], + [7, 1, 19, 2, 0, 0, 1, 0.989, 12.56, 138, 1, 1.06, 0.94], + [8, 2, 28, 0, 0, 0, 1, 1.015, 20.77, 345, 1, 1.06, 0.94], + [9, 1, 0, 0, 0, 0, 1, 1.043, 28.02, 345, 1, 1.06, 0.94], + [10, 2, 0, 0, 0, 0, 1, 1.05, 35.61, 345, 1, 1.06, 0.94], + [11, 1, 70, 23, 0, 0, 1, 0.985, 12.72, 138, 1, 1.06, 0.94], + [12, 2, 47, 10, 0, 0, 1, 0.99, 12.2, 138, 1, 1.06, 0.94], + [13, 1, 34, 16, 0, 0, 1, 0.968, 11.35, 138, 1, 1.06, 0.94], + [14, 1, 14, 1, 0, 0, 1, 0.984, 11.5, 138, 1, 1.06, 0.94], + [15, 2, 90, 30, 0, 0, 1, 0.97, 11.23, 138, 1, 1.06, 0.94], + [16, 1, 25, 10, 0, 0, 1, 0.984, 11.91, 138, 1, 1.06, 0.94], + [17, 1, 11, 3, 0, 0, 1, 0.995, 13.74, 138, 1, 1.06, 0.94], + [18, 2, 60, 34, 0, 0, 1, 0.973, 11.53, 138, 1, 1.06, 0.94], + [19, 2, 45, 25, 0, 0, 1, 0.963, 11.05, 138, 1, 1.06, 0.94], + [20, 1, 18, 3, 0, 0, 1, 0.958, 11.93, 138, 1, 1.06, 0.94], + [21, 1, 14, 8, 0, 0, 1, 0.959, 13.52, 138, 1, 1.06, 0.94], + [22, 1, 10, 5, 0, 0, 1, 0.97, 16.08, 138, 1, 1.06, 0.94], + [23, 1, 7, 3, 0, 0, 1, 1, 21, 138, 1, 1.06, 0.94], + [24, 2, 13, 0, 0, 0, 1, 0.992, 20.89, 138, 1, 1.06, 0.94], + [25, 2, 0, 0, 0, 0, 1, 1.05, 27.93, 138, 1, 1.06, 0.94], + [26, 2, 0, 0, 0, 0, 1, 1.015, 29.71, 345, 1, 1.06, 0.94], + [27, 2, 71, 13, 0, 0, 1, 0.968, 15.35, 138, 1, 1.06, 0.94], + [28, 1, 17, 7, 0, 0, 1, 0.962, 13.62, 138, 1, 1.06, 0.94], + [29, 1, 24, 4, 0, 0, 1, 0.963, 12.63, 138, 1, 1.06, 0.94], + [30, 1, 0, 0, 0, 0, 1, 0.968, 18.79, 345, 1, 1.06, 0.94], + [31, 2, 43, 27, 0, 0, 1, 0.967, 12.75, 138, 1, 1.06, 0.94], + [32, 2, 59, 23, 0, 0, 1, 0.964, 14.8, 138, 1, 1.06, 0.94], + [33, 1, 23, 9, 0, 0, 1, 0.972, 10.63, 138, 1, 1.06, 0.94], + [34, 2, 59, 26, 0, 14, 1, 0.986, 11.3, 138, 1, 1.06, 0.94], + [35, 1, 33, 9, 0, 0, 1, 0.981, 10.87, 138, 1, 1.06, 0.94], + [36, 2, 31, 17, 0, 0, 1, 0.98, 10.87, 138, 1, 1.06, 0.94], + [37, 1, 0, 0, 0, -25, 1, 0.992, 11.77, 138, 1, 1.06, 0.94], + [38, 1, 0, 0, 0, 0, 1, 0.962, 16.91, 345, 1, 1.06, 0.94], + [39, 1, 27, 11, 0, 0, 1, 0.97, 8.41, 138, 1, 1.06, 0.94], + [40, 2, 66, 23, 0, 0, 1, 0.97, 7.35, 138, 1, 1.06, 0.94], + [41, 1, 37, 10, 0, 0, 1, 0.967, 6.92, 138, 1, 1.06, 0.94], + [42, 2, 96, 23, 0, 0, 1, 0.985, 8.53, 138, 1, 1.06, 0.94], + [43, 1, 18, 7, 0, 0, 1, 0.978, 11.28, 138, 1, 1.06, 0.94], + [44, 1, 16, 8, 0, 10, 1, 0.985, 13.82, 138, 1, 1.06, 0.94], + [45, 1, 53, 22, 0, 10, 1, 0.987, 15.67, 138, 1, 1.06, 0.94], + [46, 2, 28, 10, 0, 10, 1, 1.005, 18.49, 138, 1, 1.06, 0.94], + [47, 1, 34, 0, 0, 0, 1, 1.017, 20.73, 138, 1, 1.06, 0.94], + [48, 1, 20, 11, 0, 15, 1, 1.021, 19.93, 138, 1, 1.06, 0.94], + [49, 2, 87, 30, 0, 0, 1, 1.025, 20.94, 138, 1, 1.06, 0.94], + [50, 1, 17, 4, 0, 0, 1, 1.001, 18.9, 138, 1, 1.06, 0.94], + [51, 1, 17, 8, 0, 0, 1, 0.967, 16.28, 138, 1, 1.06, 0.94], + [52, 1, 18, 5, 0, 0, 1, 0.957, 15.32, 138, 1, 1.06, 0.94], + [53, 1, 23, 11, 0, 0, 1, 0.946, 14.35, 138, 1, 1.06, 0.94], + [54, 2, 113, 32, 0, 0, 1, 0.955, 15.26, 138, 1, 1.06, 0.94], + [55, 2, 63, 22, 0, 0, 1, 0.952, 14.97, 138, 1, 1.06, 0.94], + [56, 2, 84, 18, 0, 0, 1, 0.954, 15.16, 138, 1, 1.06, 0.94], + [57, 1, 12, 3, 0, 0, 1, 0.971, 16.36, 138, 1, 1.06, 0.94], + [58, 1, 12, 3, 0, 0, 1, 0.959, 15.51, 138, 1, 1.06, 0.94], + [59, 2, 277, 113, 0, 0, 1, 0.985, 19.37, 138, 1, 1.06, 0.94], + [60, 1, 78, 3, 0, 0, 1, 0.993, 23.15, 138, 1, 1.06, 0.94], + [61, 2, 0, 0, 0, 0, 1, 0.995, 24.04, 138, 1, 1.06, 0.94], + [62, 2, 77, 14, 0, 0, 1, 0.998, 23.43, 138, 1, 1.06, 0.94], + [63, 1, 0, 0, 0, 0, 1, 0.969, 22.75, 345, 1, 1.06, 0.94], + [64, 1, 0, 0, 0, 0, 1, 0.984, 24.52, 345, 1, 1.06, 0.94], + [65, 2, 0, 0, 0, 0, 1, 1.005, 27.65, 345, 1, 1.06, 0.94], + [66, 2, 39, 18, 0, 0, 1, 1.05, 27.48, 138, 1, 1.06, 0.94], + [67, 1, 28, 7, 0, 0, 1, 1.02, 24.84, 138, 1, 1.06, 0.94], + [68, 1, 0, 0, 0, 0, 1, 1.003, 27.55, 345, 1, 1.06, 0.94], + [69, 3, 0, 0, 0, 0, 1, 1.035, 30, 138, 1, 1.06, 0.94], + [70, 2, 66, 20, 0, 0, 1, 0.984, 22.58, 138, 1, 1.06, 0.94], + [71, 1, 0, 0, 0, 0, 1, 0.987, 22.15, 138, 1, 1.06, 0.94], + [72, 2, 12, 0, 0, 0, 1, 0.98, 20.98, 138, 1, 1.06, 0.94], + [73, 2, 6, 0, 0, 0, 1, 0.991, 21.94, 138, 1, 1.06, 0.94], + [74, 2, 68, 27, 0, 12, 1, 0.958, 21.64, 138, 1, 1.06, 0.94], + [75, 1, 47, 11, 0, 0, 1, 0.967, 22.91, 138, 1, 1.06, 0.94], + [76, 2, 68, 36, 0, 0, 1, 0.943, 21.77, 138, 1, 1.06, 0.94], + [77, 2, 61, 28, 0, 0, 1, 1.006, 26.72, 138, 1, 1.06, 0.94], + [78, 1, 71, 26, 0, 0, 1, 1.003, 26.42, 138, 1, 1.06, 0.94], + [79, 1, 39, 32, 0, 20, 1, 1.009, 26.72, 138, 1, 1.06, 0.94], + [80, 2, 130, 26, 0, 0, 1, 1.04, 28.96, 138, 1, 1.06, 0.94], + [81, 1, 0, 0, 0, 0, 1, 0.997, 28.1, 345, 1, 1.06, 0.94], + [82, 1, 54, 27, 0, 20, 1, 0.989, 27.24, 138, 1, 1.06, 0.94], + [83, 1, 20, 10, 0, 10, 1, 0.985, 28.42, 138, 1, 1.06, 0.94], + [84, 1, 11, 7, 0, 0, 1, 0.98, 30.95, 138, 1, 1.06, 0.94], + [85, 2, 24, 15, 0, 0, 1, 0.985, 32.51, 138, 1, 1.06, 0.94], + [86, 1, 21, 10, 0, 0, 1, 0.987, 31.14, 138, 1, 1.06, 0.94], + [87, 2, 0, 0, 0, 0, 1, 1.015, 31.4, 161, 1, 1.06, 0.94], + [88, 1, 48, 10, 0, 0, 1, 0.987, 35.64, 138, 1, 1.06, 0.94], + [89, 2, 0, 0, 0, 0, 1, 1.005, 39.69, 138, 1, 1.06, 0.94], + [90, 2, 163, 42, 0, 0, 1, 0.985, 33.29, 138, 1, 1.06, 0.94], + [91, 2, 10, 0, 0, 0, 1, 0.98, 33.31, 138, 1, 1.06, 0.94], + [92, 2, 65, 10, 0, 0, 1, 0.993, 33.8, 138, 1, 1.06, 0.94], + [93, 1, 12, 7, 0, 0, 1, 0.987, 30.79, 138, 1, 1.06, 0.94], + [94, 1, 30, 16, 0, 0, 1, 0.991, 28.64, 138, 1, 1.06, 0.94], + [95, 1, 42, 31, 0, 0, 1, 0.981, 27.67, 138, 1, 1.06, 0.94], + [96, 1, 38, 15, 0, 0, 1, 0.993, 27.51, 138, 1, 1.06, 0.94], + [97, 1, 15, 9, 0, 0, 1, 1.011, 27.88, 138, 1, 1.06, 0.94], + [98, 1, 34, 8, 0, 0, 1, 1.024, 27.4, 138, 1, 1.06, 0.94], + [99, 2, 42, 0, 0, 0, 1, 1.01, 27.04, 138, 1, 1.06, 0.94], + [100, 2, 37, 18, 0, 0, 1, 1.017, 28.03, 138, 1, 1.06, 0.94], + [101, 1, 22, 15, 0, 0, 1, 0.993, 29.61, 138, 1, 1.06, 0.94], + [102, 1, 5, 3, 0, 0, 1, 0.991, 32.3, 138, 1, 1.06, 0.94], + [103, 2, 23, 16, 0, 0, 1, 1.001, 24.44, 138, 1, 1.06, 0.94], + [104, 2, 38, 25, 0, 0, 1, 0.971, 21.69, 138, 1, 1.06, 0.94], + [105, 2, 31, 26, 0, 20, 1, 0.965, 20.57, 138, 1, 1.06, 0.94], + [106, 1, 43, 16, 0, 0, 1, 0.962, 20.32, 138, 1, 1.06, 0.94], + [107, 2, 50, 12, 0, 6, 1, 0.952, 17.53, 138, 1, 1.06, 0.94], + [108, 1, 2, 1, 0, 0, 1, 0.967, 19.38, 138, 1, 1.06, 0.94], + [109, 1, 8, 3, 0, 0, 1, 0.967, 18.93, 138, 1, 1.06, 0.94], + [110, 2, 39, 30, 0, 6, 1, 0.973, 18.09, 138, 1, 1.06, 0.94], + [111, 2, 0, 0, 0, 0, 1, 0.98, 19.74, 138, 1, 1.06, 0.94], + [112, 2, 68, 13, 0, 0, 1, 0.975, 14.99, 138, 1, 1.06, 0.94], + [113, 2, 6, 0, 0, 0, 1, 0.993, 13.74, 138, 1, 1.06, 0.94], + [114, 1, 8, 3, 0, 0, 1, 0.96, 14.46, 138, 1, 1.06, 0.94], + [115, 1, 22, 7, 0, 0, 1, 0.96, 14.46, 138, 1, 1.06, 0.94], + [116, 2, 184, 0, 0, 0, 1, 1.005, 27.12, 138, 1, 1.06, 0.94], + [117, 1, 20, 8, 0, 0, 1, 0.974, 10.67, 138, 1, 1.06, 0.94], + [118, 1, 33, 15, 0, 0, 1, 0.949, 21.92, 138, 1, 1.06, 0.94] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [1, 0, 0, 15, -5, 0.955, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [4, 0, 0, 300, -300, 0.998, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [6, 0, 0, 50, -13, 0.99, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [8, 0, 0, 300, -300, 1.015, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [10, 450, 0, 200, -147, 1.05, 100, 1, 550, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [12, 85, 0, 120, -35, 0.99, 100, 1, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [15, 0, 0, 30, -10, 0.97, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [18, 0, 0, 50, -16, 0.973, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [19, 0, 0, 24, -8, 0.962, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [24, 0, 0, 300, -300, 0.992, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [25, 220, 0, 140, -47, 1.05, 100, 1, 320, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [26, 314, 0, 1000, -1000, 1.015, 100, 1, 414, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [27, 0, 0, 300, -300, 0.968, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [31, 7, 0, 300, -300, 0.967, 100, 1, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [32, 0, 0, 42, -14, 0.963, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [34, 0, 0, 24, -8, 0.984, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [36, 0, 0, 24, -8, 0.98, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [40, 0, 0, 300, -300, 0.97, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [42, 0, 0, 300, -300, 0.985, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [46, 19, 0, 100, -100, 1.005, 100, 1, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [49, 204, 0, 210, -85, 1.025, 100, 1, 304, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [54, 48, 0, 300, -300, 0.955, 100, 1, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [55, 0, 0, 23, -8, 0.952, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [56, 0, 0, 15, -8, 0.954, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [59, 155, 0, 180, -60, 0.985, 100, 1, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [61, 160, 0, 300, -100, 0.995, 100, 1, 260, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [62, 0, 0, 20, -20, 0.998, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [65, 391, 0, 200, -67, 1.005, 100, 1, 491, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [66, 392, 0, 200, -67, 1.05, 100, 1, 492, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [69, 516.4, 0, 300, -300, 1.035, 100, 1, 805.2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [70, 0, 0, 32, -10, 0.984, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [72, 0, 0, 100, -100, 0.98, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [73, 0, 0, 100, -100, 0.991, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [74, 0, 0, 9, -6, 0.958, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [76, 0, 0, 23, -8, 0.943, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [77, 0, 0, 70, -20, 1.006, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [80, 477, 0, 280, -165, 1.04, 100, 1, 577, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [85, 0, 0, 23, -8, 0.985, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [87, 4, 0, 1000, -100, 1.015, 100, 1, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [89, 607, 0, 300, -210, 1.005, 100, 1, 707, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [90, 0, 0, 300, -300, 0.985, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [91, 0, 0, 100, -100, 0.98, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [92, 0, 0, 9, -3, 0.99, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [99, 0, 0, 100, -100, 1.01, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [100, 252, 0, 155, -50, 1.017, 100, 1, 352, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [103, 40, 0, 40, -15, 1.01, 100, 1, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [104, 0, 0, 23, -8, 0.971, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [105, 0, 0, 23, -8, 0.965, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [107, 0, 0, 200, -200, 0.952, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [110, 0, 0, 23, -8, 0.973, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [111, 36, 0, 1000, -100, 0.98, 100, 1, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [112, 0, 0, 1000, -100, 0.975, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [113, 0, 0, 200, -100, 0.993, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [116, 0, 0, 1000, -1000, 1.005, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.0303, 0.0999, 0.0254, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 3, 0.0129, 0.0424, 0.01082, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 5, 0.00176, 0.00798, 0.0021, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 5, 0.0241, 0.108, 0.0284, 9900, 0, 0, 0, 0, 1, -360, 360], + [5, 6, 0.0119, 0.054, 0.01426, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 7, 0.00459, 0.0208, 0.0055, 9900, 0, 0, 0, 0, 1, -360, 360], + [8, 9, 0.00244, 0.0305, 1.162, 9900, 0, 0, 0, 0, 1, -360, 360], + [8, 5, 0, 0.0267, 0, 9900, 0, 0, 0.985, 0, 1, -360, 360], + [9, 10, 0.00258, 0.0322, 1.23, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 11, 0.0209, 0.0688, 0.01748, 9900, 0, 0, 0, 0, 1, -360, 360], + [5, 11, 0.0203, 0.0682, 0.01738, 9900, 0, 0, 0, 0, 1, -360, 360], + [11, 12, 0.00595, 0.0196, 0.00502, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 12, 0.0187, 0.0616, 0.01572, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 12, 0.0484, 0.16, 0.0406, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 12, 0.00862, 0.034, 0.00874, 9900, 0, 0, 0, 0, 1, -360, 360], + [11, 13, 0.02225, 0.0731, 0.01876, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 14, 0.0215, 0.0707, 0.01816, 9900, 0, 0, 0, 0, 1, -360, 360], + [13, 15, 0.0744, 0.2444, 0.06268, 9900, 0, 0, 0, 0, 1, -360, 360], + [14, 15, 0.0595, 0.195, 0.0502, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 16, 0.0212, 0.0834, 0.0214, 9900, 0, 0, 0, 0, 1, -360, 360], + [15, 17, 0.0132, 0.0437, 0.0444, 9900, 0, 0, 0, 0, 1, -360, 360], + [16, 17, 0.0454, 0.1801, 0.0466, 9900, 0, 0, 0, 0, 1, -360, 360], + [17, 18, 0.0123, 0.0505, 0.01298, 9900, 0, 0, 0, 0, 1, -360, 360], + [18, 19, 0.01119, 0.0493, 0.01142, 9900, 0, 0, 0, 0, 1, -360, 360], + [19, 20, 0.0252, 0.117, 0.0298, 9900, 0, 0, 0, 0, 1, -360, 360], + [15, 19, 0.012, 0.0394, 0.0101, 9900, 0, 0, 0, 0, 1, -360, 360], + [20, 21, 0.0183, 0.0849, 0.0216, 9900, 0, 0, 0, 0, 1, -360, 360], + [21, 22, 0.0209, 0.097, 0.0246, 9900, 0, 0, 0, 0, 1, -360, 360], + [22, 23, 0.0342, 0.159, 0.0404, 9900, 0, 0, 0, 0, 1, -360, 360], + [23, 24, 0.0135, 0.0492, 0.0498, 9900, 0, 0, 0, 0, 1, -360, 360], + [23, 25, 0.0156, 0.08, 0.0864, 9900, 0, 0, 0, 0, 1, -360, 360], + [26, 25, 0, 0.0382, 0, 9900, 0, 0, 0.96, 0, 1, -360, 360], + [25, 27, 0.0318, 0.163, 0.1764, 9900, 0, 0, 0, 0, 1, -360, 360], + [27, 28, 0.01913, 0.0855, 0.0216, 9900, 0, 0, 0, 0, 1, -360, 360], + [28, 29, 0.0237, 0.0943, 0.0238, 9900, 0, 0, 0, 0, 1, -360, 360], + [30, 17, 0, 0.0388, 0, 9900, 0, 0, 0.96, 0, 1, -360, 360], + [8, 30, 0.00431, 0.0504, 0.514, 9900, 0, 0, 0, 0, 1, -360, 360], + [26, 30, 0.00799, 0.086, 0.908, 9900, 0, 0, 0, 0, 1, -360, 360], + [17, 31, 0.0474, 0.1563, 0.0399, 9900, 0, 0, 0, 0, 1, -360, 360], + [29, 31, 0.0108, 0.0331, 0.0083, 9900, 0, 0, 0, 0, 1, -360, 360], + [23, 32, 0.0317, 0.1153, 0.1173, 9900, 0, 0, 0, 0, 1, -360, 360], + [31, 32, 0.0298, 0.0985, 0.0251, 9900, 0, 0, 0, 0, 1, -360, 360], + [27, 32, 0.0229, 0.0755, 0.01926, 9900, 0, 0, 0, 0, 1, -360, 360], + [15, 33, 0.038, 0.1244, 0.03194, 9900, 0, 0, 0, 0, 1, -360, 360], + [19, 34, 0.0752, 0.247, 0.0632, 9900, 0, 0, 0, 0, 1, -360, 360], + [35, 36, 0.00224, 0.0102, 0.00268, 9900, 0, 0, 0, 0, 1, -360, 360], + [35, 37, 0.011, 0.0497, 0.01318, 9900, 0, 0, 0, 0, 1, -360, 360], + [33, 37, 0.0415, 0.142, 0.0366, 9900, 0, 0, 0, 0, 1, -360, 360], + [34, 36, 0.00871, 0.0268, 0.00568, 9900, 0, 0, 0, 0, 1, -360, 360], + [34, 37, 0.00256, 0.0094, 0.00984, 9900, 0, 0, 0, 0, 1, -360, 360], + [38, 37, 0, 0.0375, 0, 9900, 0, 0, 0.935, 0, 1, -360, 360], + [37, 39, 0.0321, 0.106, 0.027, 9900, 0, 0, 0, 0, 1, -360, 360], + [37, 40, 0.0593, 0.168, 0.042, 9900, 0, 0, 0, 0, 1, -360, 360], + [30, 38, 0.00464, 0.054, 0.422, 9900, 0, 0, 0, 0, 1, -360, 360], + [39, 40, 0.0184, 0.0605, 0.01552, 9900, 0, 0, 0, 0, 1, -360, 360], + [40, 41, 0.0145, 0.0487, 0.01222, 9900, 0, 0, 0, 0, 1, -360, 360], + [40, 42, 0.0555, 0.183, 0.0466, 9900, 0, 0, 0, 0, 1, -360, 360], + [41, 42, 0.041, 0.135, 0.0344, 9900, 0, 0, 0, 0, 1, -360, 360], + [43, 44, 0.0608, 0.2454, 0.06068, 9900, 0, 0, 0, 0, 1, -360, 360], + [34, 43, 0.0413, 0.1681, 0.04226, 9900, 0, 0, 0, 0, 1, -360, 360], + [44, 45, 0.0224, 0.0901, 0.0224, 9900, 0, 0, 0, 0, 1, -360, 360], + [45, 46, 0.04, 0.1356, 0.0332, 9900, 0, 0, 0, 0, 1, -360, 360], + [46, 47, 0.038, 0.127, 0.0316, 9900, 0, 0, 0, 0, 1, -360, 360], + [46, 48, 0.0601, 0.189, 0.0472, 9900, 0, 0, 0, 0, 1, -360, 360], + [47, 49, 0.0191, 0.0625, 0.01604, 9900, 0, 0, 0, 0, 1, -360, 360], + [42, 49, 0.0715, 0.323, 0.086, 9900, 0, 0, 0, 0, 1, -360, 360], + [42, 49, 0.0715, 0.323, 0.086, 9900, 0, 0, 0, 0, 1, -360, 360], + [45, 49, 0.0684, 0.186, 0.0444, 9900, 0, 0, 0, 0, 1, -360, 360], + [48, 49, 0.0179, 0.0505, 0.01258, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 50, 0.0267, 0.0752, 0.01874, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 51, 0.0486, 0.137, 0.0342, 9900, 0, 0, 0, 0, 1, -360, 360], + [51, 52, 0.0203, 0.0588, 0.01396, 9900, 0, 0, 0, 0, 1, -360, 360], + [52, 53, 0.0405, 0.1635, 0.04058, 9900, 0, 0, 0, 0, 1, -360, 360], + [53, 54, 0.0263, 0.122, 0.031, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 54, 0.073, 0.289, 0.0738, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 54, 0.0869, 0.291, 0.073, 9900, 0, 0, 0, 0, 1, -360, 360], + [54, 55, 0.0169, 0.0707, 0.0202, 9900, 0, 0, 0, 0, 1, -360, 360], + [54, 56, 0.00275, 0.00955, 0.00732, 9900, 0, 0, 0, 0, 1, -360, 360], + [55, 56, 0.00488, 0.0151, 0.00374, 9900, 0, 0, 0, 0, 1, -360, 360], + [56, 57, 0.0343, 0.0966, 0.0242, 9900, 0, 0, 0, 0, 1, -360, 360], + [50, 57, 0.0474, 0.134, 0.0332, 9900, 0, 0, 0, 0, 1, -360, 360], + [56, 58, 0.0343, 0.0966, 0.0242, 9900, 0, 0, 0, 0, 1, -360, 360], + [51, 58, 0.0255, 0.0719, 0.01788, 9900, 0, 0, 0, 0, 1, -360, 360], + [54, 59, 0.0503, 0.2293, 0.0598, 9900, 0, 0, 0, 0, 1, -360, 360], + [56, 59, 0.0825, 0.251, 0.0569, 9900, 0, 0, 0, 0, 1, -360, 360], + [56, 59, 0.0803, 0.239, 0.0536, 9900, 0, 0, 0, 0, 1, -360, 360], + [55, 59, 0.04739, 0.2158, 0.05646, 9900, 0, 0, 0, 0, 1, -360, 360], + [59, 60, 0.0317, 0.145, 0.0376, 9900, 0, 0, 0, 0, 1, -360, 360], + [59, 61, 0.0328, 0.15, 0.0388, 9900, 0, 0, 0, 0, 1, -360, 360], + [60, 61, 0.00264, 0.0135, 0.01456, 9900, 0, 0, 0, 0, 1, -360, 360], + [60, 62, 0.0123, 0.0561, 0.01468, 9900, 0, 0, 0, 0, 1, -360, 360], + [61, 62, 0.00824, 0.0376, 0.0098, 9900, 0, 0, 0, 0, 1, -360, 360], + [63, 59, 0, 0.0386, 0, 9900, 0, 0, 0.96, 0, 1, -360, 360], + [63, 64, 0.00172, 0.02, 0.216, 9900, 0, 0, 0, 0, 1, -360, 360], + [64, 61, 0, 0.0268, 0, 9900, 0, 0, 0.985, 0, 1, -360, 360], + [38, 65, 0.00901, 0.0986, 1.046, 9900, 0, 0, 0, 0, 1, -360, 360], + [64, 65, 0.00269, 0.0302, 0.38, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 66, 0.018, 0.0919, 0.0248, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 66, 0.018, 0.0919, 0.0248, 9900, 0, 0, 0, 0, 1, -360, 360], + [62, 66, 0.0482, 0.218, 0.0578, 9900, 0, 0, 0, 0, 1, -360, 360], + [62, 67, 0.0258, 0.117, 0.031, 9900, 0, 0, 0, 0, 1, -360, 360], + [65, 66, 0, 0.037, 0, 9900, 0, 0, 0.935, 0, 1, -360, 360], + [66, 67, 0.0224, 0.1015, 0.02682, 9900, 0, 0, 0, 0, 1, -360, 360], + [65, 68, 0.00138, 0.016, 0.638, 9900, 0, 0, 0, 0, 1, -360, 360], + [47, 69, 0.0844, 0.2778, 0.07092, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 69, 0.0985, 0.324, 0.0828, 9900, 0, 0, 0, 0, 1, -360, 360], + [68, 69, 0, 0.037, 0, 9900, 0, 0, 0.935, 0, 1, -360, 360], + [69, 70, 0.03, 0.127, 0.122, 9900, 0, 0, 0, 0, 1, -360, 360], + [24, 70, 0.00221, 0.4115, 0.10198, 9900, 0, 0, 0, 0, 1, -360, 360], + [70, 71, 0.00882, 0.0355, 0.00878, 9900, 0, 0, 0, 0, 1, -360, 360], + [24, 72, 0.0488, 0.196, 0.0488, 9900, 0, 0, 0, 0, 1, -360, 360], + [71, 72, 0.0446, 0.18, 0.04444, 9900, 0, 0, 0, 0, 1, -360, 360], + [71, 73, 0.00866, 0.0454, 0.01178, 9900, 0, 0, 0, 0, 1, -360, 360], + [70, 74, 0.0401, 0.1323, 0.03368, 9900, 0, 0, 0, 0, 1, -360, 360], + [70, 75, 0.0428, 0.141, 0.036, 9900, 0, 0, 0, 0, 1, -360, 360], + [69, 75, 0.0405, 0.122, 0.124, 9900, 0, 0, 0, 0, 1, -360, 360], + [74, 75, 0.0123, 0.0406, 0.01034, 9900, 0, 0, 0, 0, 1, -360, 360], + [76, 77, 0.0444, 0.148, 0.0368, 9900, 0, 0, 0, 0, 1, -360, 360], + [69, 77, 0.0309, 0.101, 0.1038, 9900, 0, 0, 0, 0, 1, -360, 360], + [75, 77, 0.0601, 0.1999, 0.04978, 9900, 0, 0, 0, 0, 1, -360, 360], + [77, 78, 0.00376, 0.0124, 0.01264, 9900, 0, 0, 0, 0, 1, -360, 360], + [78, 79, 0.00546, 0.0244, 0.00648, 9900, 0, 0, 0, 0, 1, -360, 360], + [77, 80, 0.017, 0.0485, 0.0472, 9900, 0, 0, 0, 0, 1, -360, 360], + [77, 80, 0.0294, 0.105, 0.0228, 9900, 0, 0, 0, 0, 1, -360, 360], + [79, 80, 0.0156, 0.0704, 0.0187, 9900, 0, 0, 0, 0, 1, -360, 360], + [68, 81, 0.00175, 0.0202, 0.808, 9900, 0, 0, 0, 0, 1, -360, 360], + [81, 80, 0, 0.037, 0, 9900, 0, 0, 0.935, 0, 1, -360, 360], + [77, 82, 0.0298, 0.0853, 0.08174, 9900, 0, 0, 0, 0, 1, -360, 360], + [82, 83, 0.0112, 0.03665, 0.03796, 9900, 0, 0, 0, 0, 1, -360, 360], + [83, 84, 0.0625, 0.132, 0.0258, 9900, 0, 0, 0, 0, 1, -360, 360], + [83, 85, 0.043, 0.148, 0.0348, 9900, 0, 0, 0, 0, 1, -360, 360], + [84, 85, 0.0302, 0.0641, 0.01234, 9900, 0, 0, 0, 0, 1, -360, 360], + [85, 86, 0.035, 0.123, 0.0276, 9900, 0, 0, 0, 0, 1, -360, 360], + [86, 87, 0.02828, 0.2074, 0.0445, 9900, 0, 0, 0, 0, 1, -360, 360], + [85, 88, 0.02, 0.102, 0.0276, 9900, 0, 0, 0, 0, 1, -360, 360], + [85, 89, 0.0239, 0.173, 0.047, 9900, 0, 0, 0, 0, 1, -360, 360], + [88, 89, 0.0139, 0.0712, 0.01934, 9900, 0, 0, 0, 0, 1, -360, 360], + [89, 90, 0.0518, 0.188, 0.0528, 9900, 0, 0, 0, 0, 1, -360, 360], + [89, 90, 0.0238, 0.0997, 0.106, 9900, 0, 0, 0, 0, 1, -360, 360], + [90, 91, 0.0254, 0.0836, 0.0214, 9900, 0, 0, 0, 0, 1, -360, 360], + [89, 92, 0.0099, 0.0505, 0.0548, 9900, 0, 0, 0, 0, 1, -360, 360], + [89, 92, 0.0393, 0.1581, 0.0414, 9900, 0, 0, 0, 0, 1, -360, 360], + [91, 92, 0.0387, 0.1272, 0.03268, 9900, 0, 0, 0, 0, 1, -360, 360], + [92, 93, 0.0258, 0.0848, 0.0218, 9900, 0, 0, 0, 0, 1, -360, 360], + [92, 94, 0.0481, 0.158, 0.0406, 9900, 0, 0, 0, 0, 1, -360, 360], + [93, 94, 0.0223, 0.0732, 0.01876, 9900, 0, 0, 0, 0, 1, -360, 360], + [94, 95, 0.0132, 0.0434, 0.0111, 9900, 0, 0, 0, 0, 1, -360, 360], + [80, 96, 0.0356, 0.182, 0.0494, 9900, 0, 0, 0, 0, 1, -360, 360], + [82, 96, 0.0162, 0.053, 0.0544, 9900, 0, 0, 0, 0, 1, -360, 360], + [94, 96, 0.0269, 0.0869, 0.023, 9900, 0, 0, 0, 0, 1, -360, 360], + [80, 97, 0.0183, 0.0934, 0.0254, 9900, 0, 0, 0, 0, 1, -360, 360], + [80, 98, 0.0238, 0.108, 0.0286, 9900, 0, 0, 0, 0, 1, -360, 360], + [80, 99, 0.0454, 0.206, 0.0546, 9900, 0, 0, 0, 0, 1, -360, 360], + [92, 100, 0.0648, 0.295, 0.0472, 9900, 0, 0, 0, 0, 1, -360, 360], + [94, 100, 0.0178, 0.058, 0.0604, 9900, 0, 0, 0, 0, 1, -360, 360], + [95, 96, 0.0171, 0.0547, 0.01474, 9900, 0, 0, 0, 0, 1, -360, 360], + [96, 97, 0.0173, 0.0885, 0.024, 9900, 0, 0, 0, 0, 1, -360, 360], + [98, 100, 0.0397, 0.179, 0.0476, 9900, 0, 0, 0, 0, 1, -360, 360], + [99, 100, 0.018, 0.0813, 0.0216, 9900, 0, 0, 0, 0, 1, -360, 360], + [100, 101, 0.0277, 0.1262, 0.0328, 9900, 0, 0, 0, 0, 1, -360, 360], + [92, 102, 0.0123, 0.0559, 0.01464, 9900, 0, 0, 0, 0, 1, -360, 360], + [101, 102, 0.0246, 0.112, 0.0294, 9900, 0, 0, 0, 0, 1, -360, 360], + [100, 103, 0.016, 0.0525, 0.0536, 9900, 0, 0, 0, 0, 1, -360, 360], + [100, 104, 0.0451, 0.204, 0.0541, 9900, 0, 0, 0, 0, 1, -360, 360], + [103, 104, 0.0466, 0.1584, 0.0407, 9900, 0, 0, 0, 0, 1, -360, 360], + [103, 105, 0.0535, 0.1625, 0.0408, 9900, 0, 0, 0, 0, 1, -360, 360], + [100, 106, 0.0605, 0.229, 0.062, 9900, 0, 0, 0, 0, 1, -360, 360], + [104, 105, 0.00994, 0.0378, 0.00986, 9900, 0, 0, 0, 0, 1, -360, 360], + [105, 106, 0.014, 0.0547, 0.01434, 9900, 0, 0, 0, 0, 1, -360, 360], + [105, 107, 0.053, 0.183, 0.0472, 9900, 0, 0, 0, 0, 1, -360, 360], + [105, 108, 0.0261, 0.0703, 0.01844, 9900, 0, 0, 0, 0, 1, -360, 360], + [106, 107, 0.053, 0.183, 0.0472, 9900, 0, 0, 0, 0, 1, -360, 360], + [108, 109, 0.0105, 0.0288, 0.0076, 9900, 0, 0, 0, 0, 1, -360, 360], + [103, 110, 0.03906, 0.1813, 0.0461, 9900, 0, 0, 0, 0, 1, -360, 360], + [109, 110, 0.0278, 0.0762, 0.0202, 9900, 0, 0, 0, 0, 1, -360, 360], + [110, 111, 0.022, 0.0755, 0.02, 9900, 0, 0, 0, 0, 1, -360, 360], + [110, 112, 0.0247, 0.064, 0.062, 9900, 0, 0, 0, 0, 1, -360, 360], + [17, 113, 0.00913, 0.0301, 0.00768, 9900, 0, 0, 0, 0, 1, -360, 360], + [32, 113, 0.0615, 0.203, 0.0518, 9900, 0, 0, 0, 0, 1, -360, 360], + [32, 114, 0.0135, 0.0612, 0.01628, 9900, 0, 0, 0, 0, 1, -360, 360], + [27, 115, 0.0164, 0.0741, 0.01972, 9900, 0, 0, 0, 0, 1, -360, 360], + [114, 115, 0.0023, 0.0104, 0.00276, 9900, 0, 0, 0, 0, 1, -360, 360], + [68, 116, 0.00034, 0.00405, 0.164, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 117, 0.0329, 0.14, 0.0358, 9900, 0, 0, 0, 0, 1, -360, 360], + [75, 118, 0.0145, 0.0481, 0.01198, 9900, 0, 0, 0, 0, 1, -360, 360], + [76, 118, 0.0164, 0.0544, 0.01356, 9900, 0, 0, 0, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0222222, 20, 0], + [2, 0, 0, 3, 0.117647, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0454545, 20, 0], + [2, 0, 0, 3, 0.0318471, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 1.42857, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.526316, 20, 0], + [2, 0, 0, 3, 0.0490196, 20, 0], + [2, 0, 0, 3, 0.208333, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0645161, 20, 0], + [2, 0, 0, 3, 0.0625, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0255754, 20, 0], + [2, 0, 0, 3, 0.0255102, 20, 0], + [2, 0, 0, 3, 0.0193648, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0209644, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 2.5, 20, 0], + [2, 0, 0, 3, 0.0164745, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0396825, 20, 0], + [2, 0, 0, 3, 0.25, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.277778, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0] + ]) + + return ppc diff --git a/module/pypower/autotest/case14.py b/module/pypower/autotest/case14.py new file mode 100644 index 000000000..1ff5573da --- /dev/null +++ b/module/pypower/autotest/case14.py @@ -0,0 +1,97 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for IEEE 14 bus test case. +""" + +from numpy import array + +def case14(): + """Power flow data for IEEE 14 bus test case. + Please see L{caseformat} for details on the case file format. + + This data was converted from IEEE Common Data Format + (ieee14cdf.txt) on 20-Sep-2004 by cdf2matp, rev. 1.11 + + Converted from IEEE CDF file from: + U{http://www.ee.washington.edu/research/pstca/} + + 08/19/93 UW ARCHIVE 100.0 1962 W IEEE 14 Bus Test Case + + @return: Power flow data for IEEE 14 bus test case. + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 3, 0, 0, 0, 0, 1, 1.06, 0, 0, 1, 1.06, 0.94], + [2, 2, 21.7, 12.7, 0, 0, 1, 1.045, -4.98, 0, 1, 1.06, 0.94], + [3, 2, 94.2, 19, 0, 0, 1, 1.01, -12.72, 0, 1, 1.06, 0.94], + [4, 1, 47.8, -3.9, 0, 0, 1, 1.019, -10.33, 0, 1, 1.06, 0.94], + [5, 1, 7.6, 1.6, 0, 0, 1, 1.02, -8.78, 0, 1, 1.06, 0.94], + [6, 2, 11.2, 7.5, 0, 0, 1, 1.07, -14.22, 0, 1, 1.06, 0.94], + [7, 1, 0, 0, 0, 0, 1, 1.062, -13.37, 0, 1, 1.06, 0.94], + [8, 2, 0, 0, 0, 0, 1, 1.09, -13.36, 0, 1, 1.06, 0.94], + [9, 1, 29.5, 16.6, 0, 19, 1, 1.056, -14.94, 0, 1, 1.06, 0.94], + [10, 1, 9, 5.8, 0, 0, 1, 1.051, -15.1, 0, 1, 1.06, 0.94], + [11, 1, 3.5, 1.8, 0, 0, 1, 1.057, -14.79, 0, 1, 1.06, 0.94], + [12, 1, 6.1, 1.6, 0, 0, 1, 1.055, -15.07, 0, 1, 1.06, 0.94], + [13, 1, 13.5, 5.8, 0, 0, 1, 1.05, -15.16, 0, 1, 1.06, 0.94], + [14, 1, 14.9, 5, 0, 0, 1, 1.036, -16.04, 0, 1, 1.06, 0.94] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [1, 232.4, -16.9, 10, 0, 1.06, 100, 1, 332.4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 40, 42.4, 50, -40, 1.045, 100, 1, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 0, 23.4, 40, 0, 1.01, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [6, 0, 12.2, 24, -6, 1.07, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [8, 0, 17.4, 24, -6, 1.09, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.01938, 0.05917, 0.0528, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 5, 0.05403, 0.22304, 0.0492, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 3, 0.04699, 0.19797, 0.0438, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 4, 0.05811, 0.17632, 0.034, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 5, 0.05695, 0.17388, 0.0346, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 4, 0.06701, 0.17103, 0.0128, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 5, 0.01335, 0.04211, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 7, 0, 0.20912, 0, 9900, 0, 0, 0.978, 0, 1, -360, 360], + [4, 9, 0, 0.55618, 0, 9900, 0, 0, 0.969, 0, 1, -360, 360], + [5, 6, 0, 0.25202, 0, 9900, 0, 0, 0.932, 0, 1, -360, 360], + [6, 11, 0.09498, 0.1989, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 12, 0.12291, 0.25581, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 13, 0.06615, 0.13027, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 8, 0, 0.17615, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 9, 0, 0.11001, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 10, 0.03181, 0.0845, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 14, 0.12711, 0.27038, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [10, 11, 0.08205, 0.19207, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 13, 0.22092, 0.19988, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [13, 14, 0.17093, 0.34802, 0, 9900, 0, 0, 0, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.0430293, 20, 0], + [2, 0, 0, 3, 0.25, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0] + ]) + + return ppc diff --git a/module/pypower/autotest/case14e.py b/module/pypower/autotest/case14e.py new file mode 100644 index 000000000..1452ce5a1 --- /dev/null +++ b/module/pypower/autotest/case14e.py @@ -0,0 +1,97 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for IEEE 14 bus test case. +""" + +from numpy import array + +def case14e(): + """Power flow data for IEEE 14 bus test case. + Please see L{caseformat} for details on the case file format. + + This data was converted from IEEE Common Data Format + (ieee14cdf.txt) on 20-Sep-2004 by cdf2matp, rev. 1.11 + + Converted from IEEE CDF file from: + U{http://www.ee.washington.edu/research/pstca/} + + 08/19/93 UW ARCHIVE 100.0 1962 W IEEE 14 Bus Test Case + + @return: Power flow data for IEEE 14 bus test case. + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 3, 0, 0, 0, 0, 1, 1.05, 0, 0, 1, 1.06, 0.94], # error test: changed 1.06 to 1.05 + [2, 2, 21.7, 12.7, 0, 0, 1, 1.045, -4.98, 0, 1, 1.06, 0.94], + [3, 2, 94.2, 19, 0, 0, 1, 1.01, -12.72, 0, 1, 1.06, 0.94], + [4, 1, 47.8, -3.9, 0, 0, 1, 1.019, -10.33, 0, 1, 1.06, 0.94], + [5, 1, 7.6, 1.6, 0, 0, 1, 1.02, -8.78, 0, 1, 1.06, 0.94], + [6, 2, 11.2, 7.5, 0, 0, 1, 1.07, -14.22, 0, 1, 1.06, 0.94], + [7, 1, 0, 0, 0, 0, 1, 1.062, -13.37, 0, 1, 1.06, 0.94], + [8, 2, 0, 0, 0, 0, 1, 1.09, -13.36, 0, 1, 1.06, 0.94], + [9, 1, 29.5, 16.6, 0, 19, 1, 1.056, -14.94, 0, 1, 1.06, 0.94], + [10, 1, 9, 5.8, 0, 0, 1, 1.051, -15.1, 0, 1, 1.06, 0.94], + [11, 1, 3.5, 1.8, 0, 0, 1, 1.057, -14.79, 0, 1, 1.06, 0.94], + [12, 1, 6.1, 1.6, 0, 0, 1, 1.055, -15.07, 0, 1, 1.06, 0.94], + [13, 1, 13.5, 5.8, 0, 0, 1, 1.05, -15.16, 0, 1, 1.06, 0.94], + [14, 1, 14.9, 5, 0, 0, 1, 1.036, -16.04, 0, 1, 1.06, 0.94] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [1, 232.4, -16.9, 10, 0, 1.06, 100, 1, 332.4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 40, 42.4, 50, -40, 1.045, 100, 1, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 0, 23.4, 40, 0, 1.01, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [6, 0, 12.2, 24, -6, 1.07, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [8, 0, 17.4, 24, -6, 1.09, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.01938, 0.05917, 0.0528, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 5, 0.05403, 0.22304, 0.0492, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 3, 0.04699, 0.19797, 0.0438, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 4, 0.05811, 0.17632, 0.034, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 5, 0.05695, 0.17388, 0.0346, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 4, 0.06701, 0.17103, 0.0128, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 5, 0.01335, 0.04211, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 7, 0, 0.20912, 0, 9900, 0, 0, 0.978, 0, 1, -360, 360], + [4, 9, 0, 0.55618, 0, 9900, 0, 0, 0.969, 0, 1, -360, 360], + [5, 6, 0, 0.25202, 0, 9900, 0, 0, 0.932, 0, 1, -360, 360], + [6, 11, 0.09498, 0.1989, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 12, 0.12291, 0.25581, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 13, 0.06615, 0.13027, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 8, 0, 0.17615, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 9, 0, 0.11001, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 10, 0.03181, 0.0845, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 14, 0.12711, 0.27038, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [10, 11, 0.08205, 0.19207, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 13, 0.22092, 0.19988, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [13, 14, 0.17093, 0.34802, 0, 9900, 0, 0, 0, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.0430293, 20, 0], + [2, 0, 0, 3, 0.25, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.01, 40, 0] + ]) + + return ppc diff --git a/module/pypower/autotest/case30.py b/module/pypower/autotest/case30.py new file mode 100644 index 000000000..3a8071cf5 --- /dev/null +++ b/module/pypower/autotest/case30.py @@ -0,0 +1,154 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for 30 bus, 6 generator case. +""" + +from numpy import array + +def case30(): + """Power flow data for 30 bus, 6 generator case. + Please see L{caseformat} for details on the case file format. + + Based on data from ... + + Alsac, O. & Stott, B., I{"Optimal Load Flow with Steady State Security"}, + IEEE Transactions on Power Apparatus and Systems, Vol. PAS 93, No. 3, + 1974, pp. 745-751. + + ... with branch parameters rounded to nearest 0.01, shunt values divided + by 100 and shunt on bus 10 moved to bus 5, load at bus 5 zeroed out. + Generator locations, costs and limits and bus areas were taken from ... + + Ferrero, R.W., Shahidehpour, S.M., Ramesh, V.C., I{"Transaction analysis + in deregulated power systems using game theory"}, IEEE Transactions on + Power Systems, Vol. 12, No. 3, Aug 1997, pp. 1340-1347. + + Generator Q limits were derived from Alsac & Stott, using their Pmax + capacities. V limits and line |S| limits taken from Alsac & Stott. + + @return: Power flow data for 30 bus, 6 generator case. + @see: U{http://www.pserc.cornell.edu/matpower/} + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 3, 0, 0, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [2, 2, 21.7, 12.7, 0, 0, 1, 1, 0, 135, 1, 1.1, 0.95], + [3, 1, 2.4, 1.2, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [4, 1, 7.6, 1.6, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [5, 1, 0, 0, 0, 0.19, 1, 1, 0, 135, 1, 1.05, 0.95], + [6, 1, 0, 0, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [7, 1, 22.8, 10.9, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [8, 1, 30, 30, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [9, 1, 0, 0, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [10, 1, 5.8, 2, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95], + [11, 1, 0, 0, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [12, 1, 11.2, 7.5, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [13, 2, 0, 0, 0, 0, 2, 1, 0, 135, 1, 1.1, 0.95], + [14, 1, 6.2, 1.6, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [15, 1, 8.2, 2.5, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [16, 1, 3.5, 1.8, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [17, 1, 9, 5.8, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [18, 1, 3.2, 0.9, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [19, 1, 9.5, 3.4, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [20, 1, 2.2, 0.7, 0, 0, 2, 1, 0, 135, 1, 1.05, 0.95], + [21, 1, 17.5, 11.2, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95], + [22, 2, 0, 0, 0, 0, 3, 1, 0, 135, 1, 1.1, 0.95], + [23, 2, 3.2, 1.6, 0, 0, 2, 1, 0, 135, 1, 1.1, 0.95], + [24, 1, 8.7, 6.7, 0, 0.04, 3, 1, 0, 135, 1, 1.05, 0.95], + [25, 1, 0, 0, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95], + [26, 1, 3.5, 2.3, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95], + [27, 2, 0, 0, 0, 0, 3, 1, 0, 135, 1, 1.1, 0.95], + [28, 1, 0, 0, 0, 0, 1, 1, 0, 135, 1, 1.05, 0.95], + [29, 1, 2.4, 0.9, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95], + [30, 1, 10.6, 1.9, 0, 0, 3, 1, 0, 135, 1, 1.05, 0.95] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [1, 23.54, 0, 150, -20, 1, 100, 1, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 60.97, 0, 60, -20, 1, 100, 1, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [22, 21.59, 0, 62.5, -15, 1, 100, 1, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [27, 26.91, 0, 48.7, -15, 1, 100, 1, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [23, 19.2, 0, 40, -10, 1, 100, 1, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [13, 37, 0, 44.7, -15, 1, 100, 1, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.02, 0.06, 0.03, 130, 130, 130, 0, 0, 1, -360, 360], + [1, 3, 0.05, 0.19, 0.02, 130, 130, 130, 0, 0, 1, -360, 360], + [2, 4, 0.06, 0.17, 0.02, 65, 65, 65, 0, 0, 1, -360, 360], + [3, 4, 0.01, 0.04, 0, 130, 130, 130, 0, 0, 1, -360, 360], + [2, 5, 0.05, 0.2, 0.02, 130, 130, 130, 0, 0, 1, -360, 360], + [2, 6, 0.06, 0.18, 0.02, 65, 65, 65, 0, 0, 1, -360, 360], + [4, 6, 0.01, 0.04, 0, 90, 90, 90, 0, 0, 1, -360, 360], + [5, 7, 0.05, 0.12, 0.01, 70, 70, 70, 0, 0, 1, -360, 360], + [6, 7, 0.03, 0.08, 0.01, 130, 130, 130, 0, 0, 1, -360, 360], + [6, 8, 0.01, 0.04, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [6, 9, 0, 0.21, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [6, 10, 0, 0.56, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [9, 11, 0, 0.21, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [9, 10, 0, 0.11, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [4, 12, 0, 0.26, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [12, 13, 0, 0.14, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [12, 14, 0.12, 0.26, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [12, 15, 0.07, 0.13, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [12, 16, 0.09, 0.2, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [14, 15, 0.22, 0.2, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [16, 17, 0.08, 0.19, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [15, 18, 0.11, 0.22, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [18, 19, 0.06, 0.13, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [19, 20, 0.03, 0.07, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [10, 20, 0.09, 0.21, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [10, 17, 0.03, 0.08, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [10, 21, 0.03, 0.07, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [10, 22, 0.07, 0.15, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [21, 22, 0.01, 0.02, 0, 32, 32, 32, 0, 0, 1, -360, 360], + [15, 23, 0.1, 0.2, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [22, 24, 0.12, 0.18, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [23, 24, 0.13, 0.27, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [24, 25, 0.19, 0.33, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [25, 26, 0.25, 0.38, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [25, 27, 0.11, 0.21, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [28, 27, 0, 0.4, 0, 65, 65, 65, 0, 0, 1, -360, 360], + [27, 29, 0.22, 0.42, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [27, 30, 0.32, 0.6, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [29, 30, 0.24, 0.45, 0, 16, 16, 16, 0, 0, 1, -360, 360], + [8, 28, 0.06, 0.2, 0.02, 32, 32, 32, 0, 0, 1, -360, 360], + [6, 28, 0.02, 0.06, 0.01, 32, 32, 32, 0, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## area data + # area refbus + ppc["areas"] = array([ + [1, 8], + [2, 23], + [3, 26], + ]) + + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.02, 2, 0], + [2, 0, 0, 3, 0.0175, 1.75, 0], + [2, 0, 0, 3, 0.0625, 1, 0], + [2, 0, 0, 3, 0.00834, 3.25, 0], + [2, 0, 0, 3, 0.025, 3, 0], + [2, 0, 0, 3, 0.025, 3, 0] + ]) + + return ppc diff --git a/module/pypower/autotest/case39.py b/module/pypower/autotest/case39.py new file mode 100644 index 000000000..fb091017a --- /dev/null +++ b/module/pypower/autotest/case39.py @@ -0,0 +1,221 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for 39 bus New England system. +""" + +from numpy import array + +def case39(): + """Power flow data for 39 bus New England system. + Please see L{caseformat} for details on the case file format. + + Data taken from [1] with the following modifications/additions: + + - renumbered gen buses consecutively (as in [2] and [4]) + - added C{Pmin = 0} for all gens + - added C{Qmin}, C{Qmax} for gens at 31 & 39 (copied from gen at 35) + - added C{Vg} based on C{V} in bus data (missing for bus 39) + - added C{Vg, Pg, Pd, Qd} at bus 39 from [2] (same in [4]) + - added C{Pmax} at bus 39: C{Pmax = Pg + 100} + - added line flow limits and area data from [4] + - added voltage limits, C{Vmax = 1.06, Vmin = 0.94} + - added identical quadratic generator costs + - increased C{Pmax} for gen at bus 34 from 308 to 508 + (assumed typo in [1], makes initial solved case feasible) + - re-solved power flow + + Notes: + - Bus 39, its generator and 2 connecting lines were added + (by authors of [1]) to represent the interconnection with + the rest of the eastern interconnect, and did not include + C{Vg, Pg, Qg, Pd, Qd, Pmin, Pmax, Qmin} or C{Qmax}. + - As the swing bus, bus 31 did not include and Q limits. + - The voltages, etc in [1] appear to be quite close to the + power flow solution of the case before adding bus 39 with + it's generator and connecting branches, though the solution + is not exact. + - Explicit voltage setpoints for gen buses are not given, so + they are taken from the bus data, however this results in two + binding Q limits at buses 34 & 37, so the corresponding + voltages have probably deviated from their original setpoints. + - The generator locations and types are as follows: + - 1 30 hydro + - 2 31 nuke01 + - 3 32 nuke02 + - 4 33 fossil02 + - 5 34 fossil01 + - 6 35 nuke03 + - 7 36 fossil04 + - 8 37 nuke04 + - 9 38 nuke05 + - 10 39 interconnection to rest of US/Canada + + This is a solved power flow case, but it includes the following + violations: + - C{Pmax} violated at bus 31: C{Pg = 677.87, Pmax = 646} + - C{Qmin} violated at bus 37: C{Qg = -1.37, Qmin = 0} + + References: + + [1] G. W. Bills, et.al., I{"On-Line Stability Analysis Study"} + RP90-1 Report for the Edison Electric Institute, October 12, 1970, + pp. 1-20 - 1-35. + prepared by + - E. M. Gulachenski - New England Electric System + - J. M. Undrill - General Electric Co. + "...generally representative of the New England 345 KV system, but is + not an exact or complete model of any past, present or projected + configuration of the actual New England 345 KV system." + + [2] M. A. Pai, I{Energy Function Analysis for Power System Stability}, + Kluwer Academic Publishers, Boston, 1989. + (references [3] as source of data) + + [3] Athay, T.; Podmore, R.; Virmani, S., I{"A Practical Method for the + Direct Analysis of Transient Stability,"} IEEE Transactions on Power + Apparatus and Systems , vol.PAS-98, no.2, pp.573-584, March 1979. + U{http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=4113518&isnumber=4113486} + (references [1] as source of data) + + [4] Data included with TC Calculator at + U{http://www.pserc.cornell.edu/tcc/} for 39-bus system. + + @return: Power flow data for 39 bus New England system. + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 1, 97.6, 44.2, 0, 0, 2, 1.0393836, -13.536602, 345, 1, 1.06, 0.94], + [2, 1, 0, 0, 0, 0, 2, 1.0484941, -9.7852666, 345, 1, 1.06, 0.94], + [3, 1, 322, 2.4, 0, 0, 2, 1.0307077, -12.276384, 345, 1, 1.06, 0.94], + [4, 1, 500, 184, 0, 0, 1, 1.00446, -12.626734, 345, 1, 1.06, 0.94], + [5, 1, 0, 0, 0, 0, 1, 1.0060063, -11.192339, 345, 1, 1.06, 0.94], + [6, 1, 0, 0, 0, 0, 1, 1.0082256, -10.40833, 345, 1, 1.06, 0.94], + [7, 1, 233.8, 84, 0, 0, 1, 0.99839728, -12.755626, 345, 1, 1.06, 0.94], + [8, 1, 522, 176.6, 0, 0, 1, 0.99787232, -13.335844, 345, 1, 1.06, 0.94], + [9, 1, 6.5, -66.6, 0, 0, 1, 1.038332, -14.178442, 345, 1, 1.06, 0.94], + [10, 1, 0, 0, 0, 0, 1, 1.0178431, -8.170875, 345, 1, 1.06, 0.94], + [11, 1, 0, 0, 0, 0, 1, 1.0133858, -8.9369663, 345, 1, 1.06, 0.94], + [12, 1, 8.53, 88, 0, 0, 1, 1.000815, -8.9988236, 345, 1, 1.06, 0.94], + [13, 1, 0, 0, 0, 0, 1, 1.014923, -8.9299272, 345, 1, 1.06, 0.94], + [14, 1, 0, 0, 0, 0, 1, 1.012319, -10.715295, 345, 1, 1.06, 0.94], + [15, 1, 320, 153, 0, 0, 3, 1.0161854, -11.345399, 345, 1, 1.06, 0.94], + [16, 1, 329, 32.3, 0, 0, 3, 1.0325203, -10.033348, 345, 1, 1.06, 0.94], + [17, 1, 0, 0, 0, 0, 2, 1.0342365, -11.116436, 345, 1, 1.06, 0.94], + [18, 1, 158, 30, 0, 0, 2, 1.0315726, -11.986168, 345, 1, 1.06, 0.94], + [19, 1, 0, 0, 0, 0, 3, 1.0501068, -5.4100729, 345, 1, 1.06, 0.94], + [20, 1, 680, 103, 0, 0, 3, 0.99101054, -6.8211783, 345, 1, 1.06, 0.94], + [21, 1, 274, 115, 0, 0, 3, 1.0323192, -7.6287461, 345, 1, 1.06, 0.94], + [22, 1, 0, 0, 0, 0, 3, 1.0501427, -3.1831199, 345, 1, 1.06, 0.94], + [23, 1, 247.5, 84.6, 0, 0, 3, 1.0451451, -3.3812763, 345, 1, 1.06, 0.94], + [24, 1, 308.6, -92.2, 0, 0, 3, 1.038001, -9.9137585, 345, 1, 1.06, 0.94], + [25, 1, 224, 47.2, 0, 0, 2, 1.0576827, -8.3692354, 345, 1, 1.06, 0.94], + [26, 1, 139, 17, 0, 0, 2, 1.0525613, -9.4387696, 345, 1, 1.06, 0.94], + [27, 1, 281, 75.5, 0, 0, 2, 1.0383449, -11.362152, 345, 1, 1.06, 0.94], + [28, 1, 206, 27.6, 0, 0, 3, 1.0503737, -5.9283592, 345, 1, 1.06, 0.94], + [29, 1, 283.5, 26.9, 0, 0, 3, 1.0501149, -3.1698741, 345, 1, 1.06, 0.94], + [30, 2, 0, 0, 0, 0, 2, 1.0499, -7.3704746, 345, 1, 1.06, 0.94], + [31, 3, 9.2, 4.6, 0, 0, 1, 0.982, 0, 345, 1, 1.06, 0.94], + [32, 2, 0, 0, 0, 0, 1, 0.9841, -0.1884374, 345, 1, 1.06, 0.94], + [33, 2, 0, 0, 0, 0, 3, 0.9972, -0.19317445, 345, 1, 1.06, 0.94], + [34, 2, 0, 0, 0, 0, 3, 1.0123, -1.631119, 345, 1, 1.06, 0.94], + [35, 2, 0, 0, 0, 0, 3, 1.0494, 1.7765069, 345, 1, 1.06, 0.94], + [36, 2, 0, 0, 0, 0, 3, 1.0636, 4.4684374, 345, 1, 1.06, 0.94], + [37, 2, 0, 0, 0, 0, 2, 1.0275, -1.5828988, 345, 1, 1.06, 0.94], + [38, 2, 0, 0, 0, 0, 3, 1.0265, 3.8928177, 345, 1, 1.06, 0.94], + [39, 2, 1104, 250, 0, 0, 1, 1.03, -14.535256, 345, 1, 1.06, 0.94] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [30, 250, 161.762, 400, 140, 1.0499, 100, 1, 1040, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [31, 677.871, 221.574, 300, -100, 0.982, 100, 1, 646, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [32, 650, 206.965, 300, 150, 0.9841, 100, 1, 725, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [33, 632, 108.293, 250, 0, 0.9972, 100, 1, 652, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [34, 508, 166.688, 167, 0, 1.0123, 100, 1, 508, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [35, 650, 210.661, 300, -100, 1.0494, 100, 1, 687, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [36, 560, 100.165, 240, 0, 1.0636, 100, 1, 580, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [37, 540, -1.36945, 250, 0, 1.0275, 100, 1, 564, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [38, 830, 21.7327, 300, -150, 1.0265, 100, 1, 865, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [39, 1000, 78.4674, 300, -100, 1.03, 100, 1, 1100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.0035, 0.0411, 0.6987, 600, 600, 600, 0, 0, 1, -360, 360], + [1, 39, 0.001, 0.025, 0.75, 1000, 1000, 1000, 0, 0, 1, -360, 360], + [2, 3, 0.0013, 0.0151, 0.2572, 500, 500, 500, 0, 0, 1, -360, 360], + [2, 25, 0.007, 0.0086, 0.146, 500, 500, 500, 0, 0, 1, -360, 360], + [2, 30, 0, 0.0181, 0, 900, 900, 2500, 1.025, 0, 1, -360, 360], + [3, 4, 0.0013, 0.0213, 0.2214, 500, 500, 500, 0, 0, 1, -360, 360], + [3, 18, 0.0011, 0.0133, 0.2138, 500, 500, 500, 0, 0, 1, -360, 360], + [4, 5, 0.0008, 0.0128, 0.1342, 600, 600, 600, 0, 0, 1, -360, 360], + [4, 14, 0.0008, 0.0129, 0.1382, 500, 500, 500, 0, 0, 1, -360, 360], + [5, 6, 0.0002, 0.0026, 0.0434, 1200, 1200, 1200, 0, 0, 1, -360, 360], + [5, 8, 0.0008, 0.0112, 0.1476, 900, 900, 900, 0, 0, 1, -360, 360], + [6, 7, 0.0006, 0.0092, 0.113, 900, 900, 900, 0, 0, 1, -360, 360], + [6, 11, 0.0007, 0.0082, 0.1389, 480, 480, 480, 0, 0, 1, -360, 360], + [6, 31, 0, 0.025, 0, 1800, 1800, 1800, 1.07, 0, 1, -360, 360], + [7, 8, 0.0004, 0.0046, 0.078, 900, 900, 900, 0, 0, 1, -360, 360], + [8, 9, 0.0023, 0.0363, 0.3804, 900, 900, 900, 0, 0, 1, -360, 360], + [9, 39, 0.001, 0.025, 1.2, 900, 900, 900, 0, 0, 1, -360, 360], + [10, 11, 0.0004, 0.0043, 0.0729, 600, 600, 600, 0, 0, 1, -360, 360], + [10, 13, 0.0004, 0.0043, 0.0729, 600, 600, 600, 0, 0, 1, -360, 360], + [10, 32, 0, 0.02, 0, 900, 900, 2500, 1.07, 0, 1, -360, 360], + [12, 11, 0.0016, 0.0435, 0, 500, 500, 500, 1.006, 0, 1, -360, 360], + [12, 13, 0.0016, 0.0435, 0, 500, 500, 500, 1.006, 0, 1, -360, 360], + [13, 14, 0.0009, 0.0101, 0.1723, 600, 600, 600, 0, 0, 1, -360, 360], + [14, 15, 0.0018, 0.0217, 0.366, 600, 600, 600, 0, 0, 1, -360, 360], + [15, 16, 0.0009, 0.0094, 0.171, 600, 600, 600, 0, 0, 1, -360, 360], + [16, 17, 0.0007, 0.0089, 0.1342, 600, 600, 600, 0, 0, 1, -360, 360], + [16, 19, 0.0016, 0.0195, 0.304, 600, 600, 2500, 0, 0, 1, -360, 360], + [16, 21, 0.0008, 0.0135, 0.2548, 600, 600, 600, 0, 0, 1, -360, 360], + [16, 24, 0.0003, 0.0059, 0.068, 600, 600, 600, 0, 0, 1, -360, 360], + [17, 18, 0.0007, 0.0082, 0.1319, 600, 600, 600, 0, 0, 1, -360, 360], + [17, 27, 0.0013, 0.0173, 0.3216, 600, 600, 600, 0, 0, 1, -360, 360], + [19, 20, 0.0007, 0.0138, 0, 900, 900, 2500, 1.06, 0, 1, -360, 360], + [19, 33, 0.0007, 0.0142, 0, 900, 900, 2500, 1.07, 0, 1, -360, 360], + [20, 34, 0.0009, 0.018, 0, 900, 900, 2500, 1.009, 0, 1, -360, 360], + [21, 22, 0.0008, 0.014, 0.2565, 900, 900, 900, 0, 0, 1, -360, 360], + [22, 23, 0.0006, 0.0096, 0.1846, 600, 600, 600, 0, 0, 1, -360, 360], + [22, 35, 0, 0.0143, 0, 900, 900, 2500, 1.025, 0, 1, -360, 360], + [23, 24, 0.0022, 0.035, 0.361, 600, 600, 600, 0, 0, 1, -360, 360], + [23, 36, 0.0005, 0.0272, 0, 900, 900, 2500, 1, 0, 1, -360, 360], + [25, 26, 0.0032, 0.0323, 0.531, 600, 600, 600, 0, 0, 1, -360, 360], + [25, 37, 0.0006, 0.0232, 0, 900, 900, 2500, 1.025, 0, 1, -360, 360], + [26, 27, 0.0014, 0.0147, 0.2396, 600, 600, 600, 0, 0, 1, -360, 360], + [26, 28, 0.0043, 0.0474, 0.7802, 600, 600, 600, 0, 0, 1, -360, 360], + [26, 29, 0.0057, 0.0625, 1.029, 600, 600, 600, 0, 0, 1, -360, 360], + [28, 29, 0.0014, 0.0151, 0.249, 600, 600, 600, 0, 0, 1, -360, 360], + [29, 38, 0.0008, 0.0156, 0, 1200, 1200, 2500, 1.025, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2], + [2, 0, 0, 3, 0.01, 0.3, 0.2] + ]) + + return ppc diff --git a/module/pypower/autotest/case57.py b/module/pypower/autotest/case57.py new file mode 100644 index 000000000..aa6a0ef26 --- /dev/null +++ b/module/pypower/autotest/case57.py @@ -0,0 +1,207 @@ +# Copyright (c) 1996-2015 PSERC. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +"""Power flow data for IEEE 57 bus test case. +""" + +from numpy import array + +def case57(): + """Power flow data for IEEE 57 bus test case. + Please see L{caseformat} for details on the case file format. + + This data was converted from IEEE Common Data Format + (ieee57cdf.txt) on 20-Sep-2004 by cdf2matp, rev. 1.11 + + Converted from IEEE CDF file from: + U{http://www.ee.washington.edu/research/pstca/} + + Manually modified C{Qmax}, C{Qmin} on generator 1 to 200, -140, + respectively. + + 08/25/93 UW ARCHIVE 100.0 1961 W IEEE 57 Bus Test Case + + @return: Power flow data for IEEE 57 bus test case. + """ + ppc = {"version": '2'} + + ##----- Power Flow Data -----## + ## system MVA base + ppc["baseMVA"] = 100.0 + + ## bus data + # bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin + ppc["bus"] = array([ + [1, 3, 55, 17, 0, 0, 1, 1.04, 0, 0, 1, 1.06, 0.94], + [2, 2, 3, 88, 0, 0, 1, 1.01, -1.18, 0, 1, 1.06, 0.94], + [3, 2, 41, 21, 0, 0, 1, 0.985, -5.97, 0, 1, 1.06, 0.94], + [4, 1, 0, 0, 0, 0, 1, 0.981, -7.32, 0, 1, 1.06, 0.94], + [5, 1, 13, 4, 0, 0, 1, 0.976, -8.52, 0, 1, 1.06, 0.94], + [6, 2, 75, 2, 0, 0, 1, 0.98, -8.65, 0, 1, 1.06, 0.94], + [7, 1, 0, 0, 0, 0, 1, 0.984, -7.58, 0, 1, 1.06, 0.94], + [8, 2, 150, 22, 0, 0, 1, 1.005, -4.45, 0, 1, 1.06, 0.94], + [9, 2, 121, 26, 0, 0, 1, 0.98, -9.56, 0, 1, 1.06, 0.94], + [10, 1, 5, 2, 0, 0, 1, 0.986, -11.43, 0, 1, 1.06, 0.94], + [11, 1, 0, 0, 0, 0, 1, 0.974, -10.17, 0, 1, 1.06, 0.94], + [12, 2, 377, 24, 0, 0, 1, 1.015, -10.46, 0, 1, 1.06, 0.94], + [13, 1, 18, 2.3, 0, 0, 1, 0.979, -9.79, 0, 1, 1.06, 0.94], + [14, 1, 10.5, 5.3, 0, 0, 1, 0.97, -9.33, 0, 1, 1.06, 0.94], + [15, 1, 22, 5, 0, 0, 1, 0.988, -7.18, 0, 1, 1.06, 0.94], + [16, 1, 43, 3, 0, 0, 1, 1.013, -8.85, 0, 1, 1.06, 0.94], + [17, 1, 42, 8, 0, 0, 1, 1.017, -5.39, 0, 1, 1.06, 0.94], + [18, 1, 27.2, 9.8, 0, 10, 1, 1.001, -11.71, 0, 1, 1.06, 0.94], + [19, 1, 3.3, 0.6, 0, 0, 1, 0.97, -13.2, 0, 1, 1.06, 0.94], + [20, 1, 2.3, 1, 0, 0, 1, 0.964, -13.41, 0, 1, 1.06, 0.94], + [21, 1, 0, 0, 0, 0, 1, 1.008, -12.89, 0, 1, 1.06, 0.94], + [22, 1, 0, 0, 0, 0, 1, 1.01, -12.84, 0, 1, 1.06, 0.94], + [23, 1, 6.3, 2.1, 0, 0, 1, 1.008, -12.91, 0, 1, 1.06, 0.94], + [24, 1, 0, 0, 0, 0, 1, 0.999, -13.25, 0, 1, 1.06, 0.94], + [25, 1, 6.3, 3.2, 0, 5.9, 1, 0.982, -18.13, 0, 1, 1.06, 0.94], + [26, 1, 0, 0, 0, 0, 1, 0.959, -12.95, 0, 1, 1.06, 0.94], + [27, 1, 9.3, 0.5, 0, 0, 1, 0.982, -11.48, 0, 1, 1.06, 0.94], + [28, 1, 4.6, 2.3, 0, 0, 1, 0.997, -10.45, 0, 1, 1.06, 0.94], + [29, 1, 17, 2.6, 0, 0, 1, 1.01, -9.75, 0, 1, 1.06, 0.94], + [30, 1, 3.6, 1.8, 0, 0, 1, 0.962, -18.68, 0, 1, 1.06, 0.94], + [31, 1, 5.8, 2.9, 0, 0, 1, 0.936, -19.34, 0, 1, 1.06, 0.94], + [32, 1, 1.6, 0.8, 0, 0, 1, 0.949, -18.46, 0, 1, 1.06, 0.94], + [33, 1, 3.8, 1.9, 0, 0, 1, 0.947, -18.5, 0, 1, 1.06, 0.94], + [34, 1, 0, 0, 0, 0, 1, 0.959, -14.1, 0, 1, 1.06, 0.94], + [35, 1, 6, 3, 0, 0, 1, 0.966, -13.86, 0, 1, 1.06, 0.94], + [36, 1, 0, 0, 0, 0, 1, 0.976, -13.59, 0, 1, 1.06, 0.94], + [37, 1, 0, 0, 0, 0, 1, 0.985, -13.41, 0, 1, 1.06, 0.94], + [38, 1, 14, 7, 0, 0, 1, 1.013, -12.71, 0, 1, 1.06, 0.94], + [39, 1, 0, 0, 0, 0, 1, 0.983, -13.46, 0, 1, 1.06, 0.94], + [40, 1, 0, 0, 0, 0, 1, 0.973, -13.62, 0, 1, 1.06, 0.94], + [41, 1, 6.3, 3, 0, 0, 1, 0.996, -14.05, 0, 1, 1.06, 0.94], + [42, 1, 7.1, 4.4, 0, 0, 1, 0.966, -15.5, 0, 1, 1.06, 0.94], + [43, 1, 2, 1, 0, 0, 1, 1.01, -11.33, 0, 1, 1.06, 0.94], + [44, 1, 12, 1.8, 0, 0, 1, 1.017, -11.86, 0, 1, 1.06, 0.94], + [45, 1, 0, 0, 0, 0, 1, 1.036, -9.25, 0, 1, 1.06, 0.94], + [46, 1, 0, 0, 0, 0, 1, 1.05, -11.89, 0, 1, 1.06, 0.94], + [47, 1, 29.7, 11.6, 0, 0, 1, 1.033, -12.49, 0, 1, 1.06, 0.94], + [48, 1, 0, 0, 0, 0, 1, 1.027, -12.59, 0, 1, 1.06, 0.94], + [49, 1, 18, 8.5, 0, 0, 1, 1.036, -12.92, 0, 1, 1.06, 0.94], + [50, 1, 21, 10.5, 0, 0, 1, 1.023, -13.39, 0, 1, 1.06, 0.94], + [51, 1, 18, 5.3, 0, 0, 1, 1.052, -12.52, 0, 1, 1.06, 0.94], + [52, 1, 4.9, 2.2, 0, 0, 1, 0.98, -11.47, 0, 1, 1.06, 0.94], + [53, 1, 20, 10, 0, 6.3, 1, 0.971, -12.23, 0, 1, 1.06, 0.94], + [54, 1, 4.1, 1.4, 0, 0, 1, 0.996, -11.69, 0, 1, 1.06, 0.94], + [55, 1, 6.8, 3.4, 0, 0, 1, 1.031, -10.78, 0, 1, 1.06, 0.94], + [56, 1, 7.6, 2.2, 0, 0, 1, 0.968, -16.04, 0, 1, 1.06, 0.94], + [57, 1, 6.7, 2, 0, 0, 1, 0.965, -16.56, 0, 1, 1.06, 0.94] + ]) + + ## generator data + # bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin, Pc1, Pc2, + # Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf + ppc["gen"] = array([ + [1, 128.9, -16.1, 200, -140, 1.04, 100, 1, 575.88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 0, -0.8, 50, -17, 1.01, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 40, -1, 60, -10, 0.985, 100, 1, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [6, 0, 0.8, 25, -8, 0.98, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [8, 450, 62.1, 200, -140, 1.005, 100, 1, 550, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [9, 0, 2.2, 9, -3, 0.98, 100, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [12, 310, 128.5, 155, -150, 1.015, 100, 1, 410, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ]) + + ## branch data + # fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax + ppc["branch"] = array([ + [1, 2, 0.0083, 0.028, 0.129, 9900, 0, 0, 0, 0, 1, -360, 360], + [2, 3, 0.0298, 0.085, 0.0818, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 4, 0.0112, 0.0366, 0.038, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 5, 0.0625, 0.132, 0.0258, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 6, 0.043, 0.148, 0.0348, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 7, 0.02, 0.102, 0.0276, 9900, 0, 0, 0, 0, 1, -360, 360], + [6, 8, 0.0339, 0.173, 0.047, 9900, 0, 0, 0, 0, 1, -360, 360], + [8, 9, 0.0099, 0.0505, 0.0548, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 10, 0.0369, 0.1679, 0.044, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 11, 0.0258, 0.0848, 0.0218, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 12, 0.0648, 0.295, 0.0772, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 13, 0.0481, 0.158, 0.0406, 9900, 0, 0, 0, 0, 1, -360, 360], + [13, 14, 0.0132, 0.0434, 0.011, 9900, 0, 0, 0, 0, 1, -360, 360], + [13, 15, 0.0269, 0.0869, 0.023, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 15, 0.0178, 0.091, 0.0988, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 16, 0.0454, 0.206, 0.0546, 9900, 0, 0, 0, 0, 1, -360, 360], + [1, 17, 0.0238, 0.108, 0.0286, 9900, 0, 0, 0, 0, 1, -360, 360], + [3, 15, 0.0162, 0.053, 0.0544, 9900, 0, 0, 0, 0, 1, -360, 360], + [4, 18, 0, 0.555, 0, 9900, 0, 0, 0.97, 0, 1, -360, 360], + [4, 18, 0, 0.43, 0, 9900, 0, 0, 0.978, 0, 1, -360, 360], + [5, 6, 0.0302, 0.0641, 0.0124, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 8, 0.0139, 0.0712, 0.0194, 9900, 0, 0, 0, 0, 1, -360, 360], + [10, 12, 0.0277, 0.1262, 0.0328, 9900, 0, 0, 0, 0, 1, -360, 360], + [11, 13, 0.0223, 0.0732, 0.0188, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 13, 0.0178, 0.058, 0.0604, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 16, 0.018, 0.0813, 0.0216, 9900, 0, 0, 0, 0, 1, -360, 360], + [12, 17, 0.0397, 0.179, 0.0476, 9900, 0, 0, 0, 0, 1, -360, 360], + [14, 15, 0.0171, 0.0547, 0.0148, 9900, 0, 0, 0, 0, 1, -360, 360], + [18, 19, 0.461, 0.685, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [19, 20, 0.283, 0.434, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [21, 20, 0, 0.7767, 0, 9900, 0, 0, 1.043, 0, 1, -360, 360], + [21, 22, 0.0736, 0.117, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [22, 23, 0.0099, 0.0152, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [23, 24, 0.166, 0.256, 0.0084, 9900, 0, 0, 0, 0, 1, -360, 360], + [24, 25, 0, 1.182, 0, 9900, 0, 0, 1, 0, 1, -360, 360], + [24, 25, 0, 1.23, 0, 9900, 0, 0, 1, 0, 1, -360, 360], + [24, 26, 0, 0.0473, 0, 9900, 0, 0, 1.043, 0, 1, -360, 360], + [26, 27, 0.165, 0.254, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [27, 28, 0.0618, 0.0954, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [28, 29, 0.0418, 0.0587, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [7, 29, 0, 0.0648, 0, 9900, 0, 0, 0.967, 0, 1, -360, 360], + [25, 30, 0.135, 0.202, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [30, 31, 0.326, 0.497, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [31, 32, 0.507, 0.755, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [32, 33, 0.0392, 0.036, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [34, 32, 0, 0.953, 0, 9900, 0, 0, 0.975, 0, 1, -360, 360], + [34, 35, 0.052, 0.078, 0.0032, 9900, 0, 0, 0, 0, 1, -360, 360], + [35, 36, 0.043, 0.0537, 0.0016, 9900, 0, 0, 0, 0, 1, -360, 360], + [36, 37, 0.029, 0.0366, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [37, 38, 0.0651, 0.1009, 0.002, 9900, 0, 0, 0, 0, 1, -360, 360], + [37, 39, 0.0239, 0.0379, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [36, 40, 0.03, 0.0466, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [22, 38, 0.0192, 0.0295, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [11, 41, 0, 0.749, 0, 9900, 0, 0, 0.955, 0, 1, -360, 360], + [41, 42, 0.207, 0.352, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [41, 43, 0, 0.412, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [38, 44, 0.0289, 0.0585, 0.002, 9900, 0, 0, 0, 0, 1, -360, 360], + [15, 45, 0, 0.1042, 0, 9900, 0, 0, 0.955, 0, 1, -360, 360], + [14, 46, 0, 0.0735, 0, 9900, 0, 0, 0.9, 0, 1, -360, 360], + [46, 47, 0.023, 0.068, 0.0032, 9900, 0, 0, 0, 0, 1, -360, 360], + [47, 48, 0.0182, 0.0233, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [48, 49, 0.0834, 0.129, 0.0048, 9900, 0, 0, 0, 0, 1, -360, 360], + [49, 50, 0.0801, 0.128, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [50, 51, 0.1386, 0.22, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [10, 51, 0, 0.0712, 0, 9900, 0, 0, 0.93, 0, 1, -360, 360], + [13, 49, 0, 0.191, 0, 9900, 0, 0, 0.895, 0, 1, -360, 360], + [29, 52, 0.1442, 0.187, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [52, 53, 0.0762, 0.0984, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [53, 54, 0.1878, 0.232, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [54, 55, 0.1732, 0.2265, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [11, 43, 0, 0.153, 0, 9900, 0, 0, 0.958, 0, 1, -360, 360], + [44, 45, 0.0624, 0.1242, 0.004, 9900, 0, 0, 0, 0, 1, -360, 360], + [40, 56, 0, 1.195, 0, 9900, 0, 0, 0.958, 0, 1, -360, 360], + [56, 41, 0.553, 0.549, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [56, 42, 0.2125, 0.354, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [39, 57, 0, 1.355, 0, 9900, 0, 0, 0.98, 0, 1, -360, 360], + [57, 56, 0.174, 0.26, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [38, 49, 0.115, 0.177, 0.003, 9900, 0, 0, 0, 0, 1, -360, 360], + [38, 48, 0.0312, 0.0482, 0, 9900, 0, 0, 0, 0, 1, -360, 360], + [9, 55, 0, 0.1205, 0, 9900, 0, 0, 0.94, 0, 1, -360, 360] + ]) + + ##----- OPF Data -----## + ## generator cost data + # 1 startup shutdown n x1 y1 ... xn yn + # 2 startup shutdown n c(n-1) ... c0 + ppc["gencost"] = array([ + [2, 0, 0, 3, 0.0775795, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.25, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0222222, 20, 0], + [2, 0, 0, 3, 0.01, 40, 0], + [2, 0, 0, 3, 0.0322581, 20, 0] + ]) + + return ppc diff --git a/module/pypower/autotest/check_case.py b/module/pypower/autotest/check_case.py new file mode 100644 index 000000000..1f4f8bca7 --- /dev/null +++ b/module/pypower/autotest/check_case.py @@ -0,0 +1,46 @@ +import sys, json + +VERBOSE = False + +if '-v' in sys.argv or '--verbose' in sys.argv: + VERBOSE = True + if '-v' in sys.argv: + sys.argv.remove('-v') + if '--verbose' in sys.argv: + sys.argv.remove('--verbose') + +if len(sys.argv) < 4: + print("Syntax: check_case [-v|--verbose] FILE1.json FILE2.json NAME/PROP [...]") + exit(1) + +try: + with open(sys.argv[1],'r') as fh: + ref = json.load(fh) + + with open(sys.argv[2],'r') as fh: + act = json.load(fh) + + assert(ref['application'] == 'gridlabd') + assert(act['application'] == 'gridlabd') + + count = 0 + for spec in sys.argv[3:]: + name,prop = spec.split('/') + if ref['objects'][name][prop] != act['objects'][name][prop]: + if VERBOSE: + print(f"ERROR: {spec} differs --> {ref['objects'][name][prop]} != {act['objects'][name][prop]}", file=sys.stderr) + count += 1 + else: + if VERBOSE: + print(f"{spec} ok --> {ref['objects'][name][prop]} == {act['objects'][name][prop]}", file=sys.stderr) + + if VERBOSE: + print(f"{count} differences found") + + exit(2 if count>0 else 0) + +except Exception as err: + e_type,e_value,e_trace = sys.exc_info() + import traceback + traceback.print_exception(e_type,e_value,e_trace,file=sys.stderr) + exit(-1) \ No newline at end of file diff --git a/module/pypower/autotest/controllers.py b/module/pypower/autotest/controllers.py new file mode 100644 index 000000000..3660847d1 --- /dev/null +++ b/module/pypower/autotest/controllers.py @@ -0,0 +1,17 @@ +import sys + +def on_init(): + # print("controllers init ok",file=sys.stderr) + return True + +def on_sync(data): + # print(f"controllers sync called, data={data}",file=sys.stderr) + return (int(data['t']/3600)+1)*3600 # advance to top of next hour + +def load_control(obj,**kwargs): + # print(obj,": load control update",kwargs,file=sys.stderr) + return dict(t=kwargs['t']+3600, S=(15+2j)) + +def powerplant_control(obj,**kwargs): + # print(obj,": powerplant control update",kwargs,file=sys.stderr) + return dict(t=kwargs['t']+3600, S="15+2j kW") diff --git a/module/pypower/autotest/test_case118.glm b/module/pypower/autotest/test_case118.glm new file mode 100644 index 000000000..1653a47ac --- /dev/null +++ b/module/pypower/autotest/test_case118.glm @@ -0,0 +1,5 @@ +#define CASE=118 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" diff --git a/module/pypower/autotest/test_case14.glm b/module/pypower/autotest/test_case14.glm new file mode 100644 index 000000000..b9697d468 --- /dev/null +++ b/module/pypower/autotest/test_case14.glm @@ -0,0 +1,11 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + solver_method GS; + save_case TRUE; +} diff --git a/module/pypower/autotest/test_case14_controller.glm b/module/pypower/autotest/test_case14_controller.glm new file mode 100644 index 000000000..22fe80f95 --- /dev/null +++ b/module/pypower/autotest/test_case14_controller.glm @@ -0,0 +1,31 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif + +module pypower +{ +#ifdef DIR + controllers_path "${DIR}"; +#endif + controllers "controllers"; +} + +object pypower.load +{ + parent pp_bus_2; + Vn 12.5 kV; + P 10.0; + status ONLINE; + controller "load_control"; +} + +object pypower.powerplant +{ + parent pp_bus_2; + S 10.0; + status ONLINE; + controller "powerplant_control"; +} + +#include "${DIR:-.}/case.glm" diff --git a/module/pypower/autotest/test_case14_err.glm b/module/pypower/autotest/test_case14_err.glm new file mode 100644 index 000000000..bf87a0e56 --- /dev/null +++ b/module/pypower/autotest/test_case14_err.glm @@ -0,0 +1,27 @@ +#define CASE=14e +#ifexists "../case.glm" +#define DIR=.. +#setenv PYTHONPATH=..:${PYTHONPATH} +#endif + +module pypower +{ + solver_method GS; + save_case TRUE; +} + +clock +{ + timezone "PST+8PDT"; + starttime "2020-01-01 00:00:00 PST"; + stoptime "2020-02-01 00:00:00 PST"; +} + +#input "case${CASE}.py" -t pypower // case14e contains an error on line 33 which should detected + +#gridlabd -C "case${CASE}.glm" -D "starttime=${starttime}" -o "case${CASE}_ref.json" + +#set savefile=case${CASE}.json + +#on_exit 0 python3 ${DIR:-.}/check_case.py case${CASE}.json case${CASE}_ref.json pp_bus_1/Vm > gridlabd.diff + diff --git a/module/pypower/autotest/test_case14_load.glm b/module/pypower/autotest/test_case14_load.glm new file mode 100644 index 000000000..0b974da9e --- /dev/null +++ b/module/pypower/autotest/test_case14_load.glm @@ -0,0 +1,34 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif + +// #set debug=TRUE +// #set suppress_repeat_messages=FALSE + +module pypower +{ + maximum_timestep 3600; + save_case TRUE; +} + +object pypower.load +{ + parent pp_bus_2; + Vn 12.5 kV; + P 21+12j; + status ONLINE; +} + +object pypower.load +{ + parent pp_bus_2; + Vn 12.5 kV; + P 0.7+0.7j; + status ONLINE; +} + +#include "${DIR:-.}/case.glm" + +modify pp_bus_2.Pd 0; +modify pp_bus_2.Qd 0; diff --git a/module/pypower/autotest/test_case14_opf.glm b/module/pypower/autotest/test_case14_opf.glm new file mode 100644 index 000000000..244127e12 --- /dev/null +++ b/module/pypower/autotest/test_case14_opf.glm @@ -0,0 +1,10 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + enable_opf TRUE; +} diff --git a/module/pypower/autotest/test_case14_powerline.glm b/module/pypower/autotest/test_case14_powerline.glm new file mode 100644 index 000000000..931f6127d --- /dev/null +++ b/module/pypower/autotest/test_case14_powerline.glm @@ -0,0 +1,21 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + maximum_timestep 3600; + save_case TRUE; +} + +// simple copy into branch +object powerline +{ + parent pp_branch_1; + impedance 0.01938+0.05917j mOhm/mile; + length 1000 mile; +} + +// TODO: add more tests of different line compositions diff --git a/module/pypower/autotest/test_case14_powerplant.glm b/module/pypower/autotest/test_case14_powerplant.glm new file mode 100644 index 000000000..595ad8db0 --- /dev/null +++ b/module/pypower/autotest/test_case14_powerplant.glm @@ -0,0 +1,30 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif + +// #set debug=TRUE +// #set suppress_repeat_messages=FALSE + +module pypower +{ + maximum_timestep 3600; + save_case TRUE; +} + +object pypower.load +{ + parent pp_bus_2; + Vn 12.5 kV; + P 10.0; + status ONLINE; +} + +object pypower.powerplant +{ + parent pp_bus_2; + S 10.0; + status ONLINE; +} + +#include "${DIR:-.}/case.glm" diff --git a/module/pypower/autotest/test_case14_ts.glm b/module/pypower/autotest/test_case14_ts.glm new file mode 100644 index 000000000..cd5597a13 --- /dev/null +++ b/module/pypower/autotest/test_case14_ts.glm @@ -0,0 +1,11 @@ +#define CASE=14 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + solver_method GS; + maximum_timestep 3600; +} diff --git a/module/pypower/autotest/test_case30.glm b/module/pypower/autotest/test_case30.glm new file mode 100644 index 000000000..abdc3553d --- /dev/null +++ b/module/pypower/autotest/test_case30.glm @@ -0,0 +1,10 @@ +#define CASE=30 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + solver_method FD_XB; +} diff --git a/module/pypower/autotest/test_case39.glm b/module/pypower/autotest/test_case39.glm new file mode 100644 index 000000000..2b025c101 --- /dev/null +++ b/module/pypower/autotest/test_case39.glm @@ -0,0 +1,10 @@ +#define CASE=39 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + solver_method FD_BX; +} diff --git a/module/pypower/autotest/test_case57.glm b/module/pypower/autotest/test_case57.glm new file mode 100644 index 000000000..0117e8ff9 --- /dev/null +++ b/module/pypower/autotest/test_case57.glm @@ -0,0 +1,10 @@ +#define CASE=57 +#ifexists "../case.glm" +#define DIR=.. +#endif +#include "${DIR:-.}/case.glm" + +module pypower +{ + solver_method NR; +} diff --git a/module/pypower/branch.cpp b/module/pypower/branch.cpp new file mode 100644 index 000000000..58412db45 --- /dev/null +++ b/module/pypower/branch.cpp @@ -0,0 +1,94 @@ +// module/pypower/branch.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(branch); +EXPORT_INIT(branch); + +CLASS *branch::oclass = NULL; +branch *branch::defaults = NULL; + +branch::branch(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"branch",sizeof(branch),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class branch"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + PT_int32, "fbus", get_fbus_offset(), + PT_DESCRIPTION, "from bus number", + + PT_int32, "tbus", get_tbus_offset(), + PT_DESCRIPTION, "to bus number", + + PT_double, "r[pu*Ohm]", get_r_offset(), + PT_DESCRIPTION, "resistance (p.u.)", + + PT_double, "x[pu*Ohm]", get_x_offset(), + PT_DESCRIPTION, "reactance (p.u.)", + + PT_double, "b[pu/Ohm]", get_b_offset(), + PT_DESCRIPTION, "total line charging susceptance (p.u.)", + + PT_double, "rateA[MVA]", get_rateA_offset(), + PT_DESCRIPTION, "MVA rating A (long term rating)", + + PT_double, "rateB[MVA]", get_rateB_offset(), + PT_DESCRIPTION, "MVA rating B (short term rating)", + + PT_double, "rateC[MVA]", get_rateC_offset(), + PT_DESCRIPTION, "MVA rating C (emergency term rating)", + + PT_double, "ratio[pu]", get_ratio_offset(), + PT_DESCRIPTION, "transformer off nominal turns ratio", + + PT_double, "angle[pu]", get_angle_offset(), + PT_DESCRIPTION, "transformer phase shift angle (degrees)", + + PT_enumeration, "status", get_status_offset(), + PT_KEYWORD,"OUT",(enumeration)BS_OUT, + PT_KEYWORD,"IN",(enumeration)BS_IN, + PT_DESCRIPTION, "initial branch status, 1 - in service, 0 - out of service", + + PT_double, "angmin[deg]", get_angmin_offset(), + PT_DESCRIPTION, "minimum angle difference, angle(Vf) - angle(Vt) (degrees)", + + PT_double, "angmax[deg]", get_angmax_offset(), + PT_DESCRIPTION, "maximum angle difference, angle(Vf) - angle(Vt) (degrees)", + + NULL)<1) + { + throw "unable to publish branch properties"; + } + } +} + +int branch::create(void) +{ + extern branch *branchlist[MAXENT]; + extern size_t nbranch; + if ( nbranch < MAXENT ) + { + branchlist[nbranch++] = this; + } + else + { + throw "maximum branch entities exceeded"; + } + + child_count = 0; + + return 1; /* return 1 on success, 0 on failure */ +} + +int branch::init(OBJECT *parent) +{ + return 1; +} diff --git a/module/pypower/branch.h b/module/pypower/branch.h new file mode 100644 index 000000000..b23c19ffc --- /dev/null +++ b/module/pypower/branch.h @@ -0,0 +1,44 @@ +// module/pypower/branch.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_BRANCH_H +#define _PYPOWER_BRANCH_H + +#include "gridlabd.h" + +class branch : public gld_object +{ + +public: + // published properties + GL_ATOMIC(int32,fbus); + GL_ATOMIC(int32,tbus); + GL_ATOMIC(double,r); + GL_ATOMIC(double,x); + GL_ATOMIC(double,b); + GL_ATOMIC(double,rateA); + GL_ATOMIC(double,rateB); + GL_ATOMIC(double,rateC); + GL_ATOMIC(double,ratio); + GL_ATOMIC(double,angle); + typedef enum {BS_OUT=0,BS_IN=1} BRANCHSTATUS; + GL_ATOMIC(enumeration,status); + GL_ATOMIC(double,angmin); + GL_ATOMIC(double,angmax); + +public: + GL_ATOMIC(int32,child_count); + +public: + // event handlers + branch(MODULE *module); + int create(void); + int init(OBJECT *parent); + +public: + // internal properties + static CLASS *oclass; + static branch *defaults; +}; + +#endif // _branch_H diff --git a/module/pypower/bus.cpp b/module/pypower/bus.cpp new file mode 100644 index 000000000..2899292af --- /dev/null +++ b/module/pypower/bus.cpp @@ -0,0 +1,145 @@ +// module/pypower/bus.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(bus); +EXPORT_INIT(bus); +EXPORT_SYNC(bus); + +CLASS *bus::oclass = NULL; +bus *bus::defaults = NULL; + +bus::bus(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"bus",sizeof(bus),PC_PRETOPDOWN|PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class bus"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + PT_int32, "bus_i", get_bus_i_offset(), + PT_DESCRIPTION, "bus number (1 to 29997)", + + PT_complex, "S[MVA]", get_Pd_offset(), + PT_DESCRIPTION, "base load demand not counting child objects, copied from Pd,Qd by default (MVA)", + + PT_enumeration, "type", get_type_offset(), + PT_DESCRIPTION, "bus type (1 = PQ, 2 = PV, 3 = ref, 4 = isolated)", + PT_KEYWORD, "UNKNOWN", (enumeration)0, + PT_KEYWORD, "PQ", (enumeration)1, + PT_KEYWORD, "PV", (enumeration)2, + PT_KEYWORD, "REF", (enumeration)3, + PT_KEYWORD, "NONE", (enumeration)4, + PT_KEYWORD, "PQREF", (enumeration)1, + + PT_double, "Pd[MW]", get_Pd_offset(), + PT_DESCRIPTION, "real power demand (MW)", + + PT_double, "Qd[MVAr]", get_Qd_offset(), + PT_DESCRIPTION, "reactive power demand (MVAr)", + + PT_double, "Gs[MW]", get_Gs_offset(), + PT_DESCRIPTION, "shunt conductance (MW at V = 1.0 p.u.)", + + PT_double, "Bs[MVAr]", get_Bs_offset(), + PT_DESCRIPTION, "shunt susceptance (MVAr at V = 1.0 p.u.)", + + PT_int32, "area", get_area_offset(), + PT_DESCRIPTION, "area number, 1-100", + + PT_double, "baseKV[kV]", get_baseKV_offset(), + PT_DESCRIPTION, "voltage magnitude (p.u.)", + + PT_double, "Vm[pu*V]", get_Vm_offset(), + PT_DESCRIPTION, "voltage angle (degrees)", + + PT_double, "Va[deg]", get_Va_offset(), + PT_DESCRIPTION, "base voltage (kV)", + + PT_int32, "zone", get_zone_offset(), + PT_DESCRIPTION, "loss zone (1-999)", + + PT_double, "Vmax[pu*V]", get_Vmax_offset(), + PT_DEFAULT,"1.2 pu*V", + PT_DESCRIPTION, "maximum voltage magnitude (p.u.)", + + PT_double, "Vmin[pu*V]", get_Vmin_offset(), + PT_DEFAULT,"0.8 pu*V", + PT_DESCRIPTION, "minimum voltage magnitude (p.u.)", + + PT_double, "lam_P", get_lam_P_offset(), + PT_DESCRIPTION, "Lagrange multiplier on real power mismatch (u/MW)", + PT_ACCESS, PA_REFERENCE, + + PT_double, "lam_Q", get_lam_Q_offset(), + PT_DESCRIPTION, "Lagrange multiplier on reactive power mismatch (u/MVAr)", + PT_ACCESS, PA_REFERENCE, + + PT_double, "mu_Vmax", get_mu_Vmax_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on upper voltage limit (u/p.u.)", + PT_ACCESS, PA_REFERENCE, + + PT_double, "mu_Vmin", get_mu_Vmin_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on lower voltage limit (u/p.u.)", + PT_ACCESS, PA_REFERENCE, + + NULL)<1) + { + throw "unable to publish bus properties"; + } + } +} + +int bus::create(void) +{ + extern bus *buslist[MAXENT]; + extern size_t nbus; + if ( nbus < MAXENT ) + { + buslist[nbus++] = this; + } + else + { + throw "maximum bus entities exceeded"; + } + + return 1; // return 1 on success, 0 on failure +} + +int bus::init(OBJECT *parent) +{ + // copy demand to base load if baseload not set + if ( S.Re() == 0.0 && S.Im() == 0.0 ) + { + S.Re() = Pd; + S.Im() = Qd; + } + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP bus::presync(TIMESTAMP t0) +{ + // reset to base load + Pd = S.Re(); + Qd = S.Im(); + return TS_NEVER; +} + +TIMESTAMP bus::sync(TIMESTAMP t0) +{ + exception("invalid sync call"); + return TS_NEVER; +} + +TIMESTAMP bus::postsync(TIMESTAMP t0) +{ + exception("invalid postsync call"); + return TS_NEVER; +} diff --git a/module/pypower/bus.h b/module/pypower/bus.h new file mode 100644 index 000000000..c3f45b580 --- /dev/null +++ b/module/pypower/bus.h @@ -0,0 +1,49 @@ +// module/pypower/bus.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_BUS_H +#define _PYPOWER_BUS_H + +#include "gridlabd.h" + +class bus : public gld_object +{ + +public: + // published properties + GL_ATOMIC(int32,bus_i); + GL_ATOMIC(enumeration,type); + GL_ATOMIC(double,Pd); + GL_ATOMIC(double,Qd); + GL_ATOMIC(double,Gs); + GL_ATOMIC(double,Bs); + GL_ATOMIC(int32,area); + GL_ATOMIC(double,baseKV); + GL_ATOMIC(double,Vm); + GL_ATOMIC(double,Va); + GL_ATOMIC(int32,zone); + GL_ATOMIC(double,Vmax); + GL_ATOMIC(double,Vmin); + GL_ATOMIC(double,lam_P); + GL_ATOMIC(double,lam_Q); + GL_ATOMIC(double,mu_Vmax); + GL_ATOMIC(double,mu_Vmin); + GL_ATOMIC(complex,S); + +public: + + // event handlers + bus(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP presync(TIMESTAMP t0); + TIMESTAMP sync(TIMESTAMP t0); + TIMESTAMP postsync(TIMESTAMP t0); + +public: + // internal properties + static CLASS *oclass; + static bus *defaults; +}; + +#endif // _BUS_H diff --git a/module/pypower/gen.cpp b/module/pypower/gen.cpp new file mode 100644 index 000000000..0941d3396 --- /dev/null +++ b/module/pypower/gen.cpp @@ -0,0 +1,132 @@ +// module/pypower/gen.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(gen); +EXPORT_INIT(gen); + +CLASS *gen::oclass = NULL; +gen *gen::defaults = NULL; + +gen::gen(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"gen",sizeof(gen),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class gen"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_int32, "bus", get_bus_offset(), + PT_DESCRIPTION, "bus number", + + PT_double, "Pg[MW]", get_Pg_offset(), + PT_DESCRIPTION, "real power output (MW)", + + PT_double, "Qg[MVAr]", get_Qg_offset(), + PT_DESCRIPTION, "reactive power output (MVAr)", + + PT_double, "Qmax[MVAr]", get_Qmax_offset(), + PT_DESCRIPTION, "maximum reactive power output (MVAr)", + + PT_double, "Qmin[MVAr]", get_Qmin_offset(), + PT_DESCRIPTION, "minimum reactive power output (MVAr)", + + PT_double, "Vg[pu*V]", get_Vg_offset(), + PT_DESCRIPTION, "voltage magnitude setpoint (p.u.)", + + PT_double, "mBase[MVA]", get_mBase_offset(), + PT_DESCRIPTION, "total MVA base of machine, defaults to baseMVA", + + PT_enumeration, "status", get_status_offset(), + PT_DESCRIPTION, "1 - in service, 0 - out of service", + PT_KEYWORD, "IN_SERVICE", (enumeration)1, + PT_KEYWORD, "OUT_OF_SERVICE", (enumeration)0, + + PT_double, "Pmax[MW]", get_Pmax_offset(), + PT_DESCRIPTION, "maximum real power output (MW)", + + PT_double, "Pmin[MW]", get_Pmin_offset(), + PT_DESCRIPTION, "minimum real power output (MW)", + + PT_double, "Pc1[MW]", get_Pc1_offset(), + PT_DESCRIPTION, "lower real power output of PQ capability curve (MW)", + + PT_double, "Pc2[MW]", get_Pc2_offset(), + PT_DESCRIPTION, "upper real power output of PQ capability curve (MW)", + + PT_double, "Qc1min[MVAr]", get_Qc1min_offset(), + PT_DESCRIPTION, "minimum reactive power output at Pc1 (MVAr)", + + PT_double, "Qc1max[MVAr]", get_Qc1max_offset(), + PT_DESCRIPTION, "maximum reactive power output at Pc1 (MVAr)", + + PT_double, "Qc2min[MVAr]", get_Qc2min_offset(), + PT_DESCRIPTION, "minimum reactive power output at Pc2 (MVAr)", + + PT_double, "Qc2max[MVAr]", get_Qc2max_offset(), + PT_DESCRIPTION, "maximum reactive power output at Pc2 (MVAr)", + + PT_double, "ramp_agc[MW/min]", get_ramp_agc_offset(), + PT_DESCRIPTION, "ramp rate for load following/AGC (MW/min)", + + PT_double, "ramp_10[MW]", get_ramp_10_offset(), + PT_DESCRIPTION, "ramp rate for 10 minute reserves (MW)", + + PT_double, "ramp_30[MW]", get_ramp_30_offset(), + PT_DESCRIPTION, "ramp rate for 30 minute reserves (MW)", + + PT_double, "ramp_q[MVAr/min]", get_ramp_q_offset(), + PT_DESCRIPTION, "ramp rate for reactive power (2 sec timescale) (MVAr/min)", + + PT_double, "apf", get_apf_offset(), + PT_DESCRIPTION, "area participation factor", + + PT_double, "mu_Pmax[pu/MW]", get_mu_Pmax_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on upper Pg limit (p.u./MW)", + + PT_double, "mu_Pmin[pu/MW]", get_mu_Pmin_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on lower Pg limit (p.u./MW)", + + PT_double, "mu_Qmax[pu/MVAr]", get_mu_Qmax_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on upper Qg limit (p.u./MVAr)", + + PT_double, "mu_Qmin[pu/MVAr]", get_mu_Qmin_offset(), + PT_DESCRIPTION, "Kuhn-Tucker multiplier on lower Qg limit (p.u./MVAr)", + + NULL)<1) + { + throw "unable to publish gen properties"; + } + } +} + +int gen::create(void) +{ + extern gen *genlist[MAXENT]; + extern size_t ngen; + if ( ngen < MAXENT ) + { + genlist[ngen++] = this; + } + else + { + throw "maximum gen entities exceeded"; + } + + extern double base_MVA; + mBase = base_MVA; + + return 1; /* return 1 on success, 0 on failure */ +} + +int gen::init(OBJECT *parent) +{ + return 1; +} diff --git a/module/pypower/gen.h b/module/pypower/gen.h new file mode 100644 index 000000000..0e10af72c --- /dev/null +++ b/module/pypower/gen.h @@ -0,0 +1,52 @@ +// module/pypower/gen.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_GEN_H +#define _PYPOWER_GEN_H + +#include "gridlabd.h" + +class gen : public gld_object +{ + +public: + // published properties + GL_ATOMIC(int32,bus); + GL_ATOMIC(double,Pg); + GL_ATOMIC(double,Qg); + GL_ATOMIC(double,Qmax); + GL_ATOMIC(double,Qmin); + GL_ATOMIC(double,Vg); + GL_ATOMIC(double,mBase); + GL_ATOMIC(enumeration,status); + GL_ATOMIC(double,Pmax); + GL_ATOMIC(double,Pmin); + GL_ATOMIC(double,Pc1); + GL_ATOMIC(double,Pc2); + GL_ATOMIC(double,Qc1min); + GL_ATOMIC(double,Qc1max); + GL_ATOMIC(double,Qc2min); + GL_ATOMIC(double,Qc2max); + GL_ATOMIC(double,ramp_agc); + GL_ATOMIC(double,ramp_10); + GL_ATOMIC(double,ramp_30); + GL_ATOMIC(double,ramp_q); + GL_ATOMIC(double,apf); + GL_ATOMIC(double,mu_Pmax); + GL_ATOMIC(double,mu_Pmin); + GL_ATOMIC(double,mu_Qmax); + GL_ATOMIC(double,mu_Qmin); + +public: + // event handlers + gen(MODULE *module); + int create(void); + int init(OBJECT *parent); + +public: + // internal properties + static CLASS *oclass; + static gen *defaults; +}; + +#endif // _gen_H diff --git a/module/pypower/gencost.cpp b/module/pypower/gencost.cpp new file mode 100644 index 000000000..84afeabcf --- /dev/null +++ b/module/pypower/gencost.cpp @@ -0,0 +1,84 @@ +// module/pypower/gencost.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(gencost); +EXPORT_INIT(gencost); + +CLASS *gencost::oclass = NULL; +gencost *gencost::defaults = NULL; + +gencost::gencost(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"gencost",sizeof(gencost),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class gencost"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + PT_enumeration, "model", get_model_offset(), + PT_DESCRIPTION, "cost model (1=piecewise linear, 2=polynomial)", + PT_KEYWORD, "UNKNOWN", (enumeration)CM_UNKNOWN, + PT_KEYWORD, "PIECEWISE", (enumeration)CM_PIECEWISE, + PT_KEYWORD, "POLYNOMIAL", (enumeration)CM_POLYNOMIAL, + + PT_double, "startup[$]", get_startup_offset(), + PT_DESCRIPTION, "startup cost ($)", + + PT_double, "shutdown[$]", get_shutdown_offset(), + PT_DESCRIPTION, "shutdown cost($)", + + PT_char1024, "costs", get_costs_offset(), + PT_DESCRIPTION, "cost model (comma-separate values)", + + NULL)<1) + { + throw "unable to publish properties in pypower gencost"; + } + } +} + +int gencost::create(void) +{ + extern gencost *gencostlist[MAXENT]; + extern size_t ngencost; + if ( ngencost < MAXENT ) + { + gencostlist[ngencost++] = this; + } + else + { + throw "maximum gencost entities exceeded"; + } + + return 1; /* return 1 on success, 0 on failure */ +} + +int gencost::init(OBJECT *parent) +{ + if ( model == CM_UNKNOWN ) + { + error("cost model must be PIECEWISE or POLYNOMIAL"); + return 0; + } + + if ( startup < 0 ) + { + error("startup cost must be non-negative"); + return 0; + } + + if ( shutdown < 0 ) + { + error("shutdown cost must be non-negative"); + return 0; + } + + return 1; +} diff --git a/module/pypower/gencost.h b/module/pypower/gencost.h new file mode 100644 index 000000000..fabdcb930 --- /dev/null +++ b/module/pypower/gencost.h @@ -0,0 +1,33 @@ +// module/pypower/gencost.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_GENCOST_H +#define _PYPOWER_GENCOST_H + +#include "gridlabd.h" + +class gencost : public gld_object +{ +private: + typedef enum {CM_UNKNOWN=0,CM_PIECEWISE=1,CM_POLYNOMIAL} COSTMODEL; + +public: + // published properties + GL_ATOMIC(enumeration,model); + GL_ATOMIC(double,startup); + GL_ATOMIC(double,shutdown); + GL_ATOMIC(char1024,costs); + +public: + // event handlers + gencost(MODULE *module); + int create(void); + int init(OBJECT *parent); + +public: + // internal properties + static CLASS *oclass; + static gencost *defaults; +}; + +#endif // _gencost_H diff --git a/module/pypower/load.cpp b/module/pypower/load.cpp new file mode 100644 index 000000000..d99e53c41 --- /dev/null +++ b/module/pypower/load.cpp @@ -0,0 +1,261 @@ +// module/pypower/load.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(load); +EXPORT_INIT(load); +EXPORT_SYNC(load); + +CLASS *load::oclass = NULL; +load *load::defaults = NULL; + +load::load(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"load",sizeof(load),PC_PRETOPDOWN|PC_BOTTOMUP|PC_POSTTOPDOWN|PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class load"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_complex, "S[VA]", get_S_offset(), + PT_DESCRIPTION, "power demand (VA)", + + PT_complex, "Z[Ohm]", get_Z_offset(), + PT_DESCRIPTION, "constant impedance load (Ohm)", + + PT_complex, "I[A]", get_I_offset(), + PT_DESCRIPTION, "constant current load (A)", + + PT_complex, "P[VA]", get_P_offset(), + PT_DESCRIPTION, "constant power load (VA)", + + PT_complex, "V[V]", get_V_offset(), + PT_DESCRIPTION, "bus voltage (V)", + + PT_double, "Vn[V]", get_Vn_offset(), + PT_DESCRIPTION, "nominal voltage (V)", + + PT_enumeration, "status", get_status_offset(), + PT_DESCRIPTION, "load status", + PT_KEYWORD, "OFFLINE", (enumeration)0, + PT_KEYWORD, "ONLINE", (enumeration)1, + PT_KEYWORD, "CURTAILED", (enumeration)2, + + PT_double, "response[pu]", get_response_offset(), + PT_DESCRIPTION, "curtailment response as fractional load reduction", + + PT_char256, "controller", get_controller_offset(), + PT_DESCRIPTION,"controller python function name", + + NULL) < 1 ) + { + throw "unable to publish load properties"; + } + } +} + +int load::create(void) +{ + py_controller = NULL; + py_args = PyTuple_New(1); + py_kwargs = PyDict_New(); + + return 1; // return 1 on success, 0 on failure +} + +int load::init(OBJECT *parent_hdr) +{ + bus *parent = (bus*)get_parent(); + if ( parent && ! parent->isa("bus","pypower") ) + { + error("parent '%s' is not a pypower bus object",get_parent()->get_name()); + return 0; + } + + if ( Vn <= 0.0 ) + { + Vn = parent->get_baseKV(); + } + else if ( fabs(Vn - parent->get_baseKV()) > Vn*1e-3 ) + { + warning("nominal voltage Vn differs from bus baseKV by more than 0.1%"); + } + + extern PyObject *py_globals; + if ( py_globals != NULL && controller[0] != '\0' ) + { + py_controller = PyDict_GetItemString(py_globals,controller); + if ( py_controller == NULL ) + { + error("pypower controller '%s' is not found",(const char *)controller); + return 0; + } + if ( ! PyCallable_Check(py_controller) ) + { + Py_DECREF(py_controller); + error("pypower controller '%s' is not callable",(const char *)controller); + return 0; + } + } + + if ( get_name() ) + { + PyTuple_SET_ITEM(py_args,0,PyUnicode_FromString(get_name())); + } + else + { + char buffer[80]; + snprintf(buffer,sizeof(buffer)-1,"%64s:%ld",get_oclass()->get_name(),(long)get_id()); + PyTuple_SET_ITEM(py_args,0,PyUnicode_FromString(buffer)); + } + Py_complex z = {get_S().Re(), get_S().Im()}; + PyDict_SetItemString(py_kwargs,"S",PyComplex_FromCComplex(z)); + z.real = get_Z().Re(); z.imag = get_Z().Im(); + PyDict_SetItemString(py_kwargs,"Z",PyComplex_FromCComplex(z)); + z.real = get_I().Re(); z.imag = get_I().Im(); + PyDict_SetItemString(py_kwargs,"I",PyComplex_FromCComplex(z)); + z.real = get_P().Re(); z.imag = get_P().Im(); + PyDict_SetItemString(py_kwargs,"P",PyComplex_FromCComplex(z)); + z.real = get_V().Re(); z.imag = get_V().Im(); + PyDict_SetItemString(py_kwargs,"V",PyComplex_FromCComplex(z)); + PyDict_SetItemString(py_kwargs,"status",PyLong_FromLong(get_status())); + PyDict_SetItemString(py_kwargs,"response",PyFloat_FromDouble(get_response())); + PyDict_SetItemString(py_kwargs,"t",PyFloat_FromDouble((double)gl_globalclock)); + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP load::presync(TIMESTAMP t0) +{ + // calculate load based on voltage and ZIP values + complex Vpu = V / Vn; + S = complex(0,0); + enumeration status = get_status(); + if ( status != LS_OFFLINE ) + { + S = P + Vpu * ~I; + if ( Z.Re() != 0 && Z.Im() != 0 ) + { + S += (~Vpu*Vpu) / ~Z; + } + if ( status == LS_CURTAILED) + { + S *= 1.0 - get_response(); + } + } + + // copy load data to parent + if ( S.Re() != 0.0 || S.Im() != 0.0 ) + { + bus *parent = (bus*)get_parent(); + if ( parent ) + { + parent->set_Pd(parent->get_Pd()+S.Re()); + parent->set_Qd(parent->get_Qd()+S.Im()); + } + } + return TS_NEVER; +} + +TIMESTAMP load::sync(TIMESTAMP t0) +{ + TIMESTAMP t1 = TS_NEVER; + if ( py_controller ) + { + Py_complex z = {get_S().Re(), get_S().Im()}; + PyDict_SetItemString(py_kwargs,"S",PyComplex_FromCComplex(z)); + z.real = get_Z().Re(); z.imag = get_Z().Im(); + PyDict_SetItemString(py_kwargs,"Z",PyComplex_FromCComplex(z)); + z.real = get_I().Re(); z.imag = get_I().Im(); + PyDict_SetItemString(py_kwargs,"I",PyComplex_FromCComplex(z)); + z.real = get_P().Re(); z.imag = get_P().Im(); + PyDict_SetItemString(py_kwargs,"P",PyComplex_FromCComplex(z)); + z.real = get_V().Re(); z.imag = get_V().Im(); + PyDict_SetItemString(py_kwargs,"V",PyComplex_FromCComplex(z)); + PyDict_SetItemString(py_kwargs,"status",PyLong_FromLong(get_status())); + PyDict_SetItemString(py_kwargs,"response",PyFloat_FromDouble(get_response())); + PyDict_SetItemString(py_kwargs,"t",PyFloat_FromDouble((double)gl_globalclock)); + + PyObject *result = PyObject_Call(py_controller,py_args,py_kwargs); + if ( result == NULL ) + { + if ( PyErr_Occurred() ) + { + PyErr_Print(); + } + else + { + error("controller call failed (no info)"); + } + + extern bool stop_on_failure; + if ( stop_on_failure ) + { + return TS_INVALID; + } + } + else if ( PyDict_Check(result) ) + { + PyObject *key, *value; + Py_ssize_t pos = 0; + while ( PyDict_Next(result,&pos,&key,&value) ) + { + PyObject *repr = PyObject_Str(key); + const char *name = PyUnicode_AsUTF8(repr); + if ( strcmp(name,"t") == 0 ) + { + t1 = (TIMESTAMP)PyFloat_AsDouble(value); + } + else + { + gld_property prop(my(),name); + Py_DECREF(repr); + + if ( prop.is_valid() ) + { + repr = PyObject_Str(value); + const char *data = PyUnicode_AsUTF8(repr); + prop.from_string(data); + Py_DECREF(repr); + } + else + { + error("controller return property '%s' is not valid",name); + } + } + } + Py_DECREF(result); + } + else + { + error("controller return value not a property update dictionary"); + Py_DECREF(result); + } + } + return t1; +} + +TIMESTAMP load::postsync(TIMESTAMP t0) +{ + // copy voltage data from parent + if ( get_status() != LS_OFFLINE ) + { + bus *parent = (bus*)get_parent(); + if ( parent ) + { + V.SetPolar(parent->get_Vm()*Vn,parent->get_Va()); + } + } + else + { + V = complex(0,0); + } + return TS_NEVER; +} diff --git a/module/pypower/load.h b/module/pypower/load.h new file mode 100644 index 000000000..a4173ff47 --- /dev/null +++ b/module/pypower/load.h @@ -0,0 +1,45 @@ +// module/pypower/load.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_LOAD_H +#define _PYPOWER_LOAD_H + +#include "gridlabd.h" + +class load : public gld_object +{ + +public: + // published properties + GL_ATOMIC(complex,S); + GL_ATOMIC(complex,Z) + GL_ATOMIC(complex,I); + GL_ATOMIC(complex,P); + GL_ATOMIC(complex,V); + GL_ATOMIC(double,Vn); + typedef enum {LS_OFFLINE=0, LS_ONLINE=1, LS_CURTAILED=2,} LOADSTATUS; + GL_ATOMIC(enumeration,status); + GL_ATOMIC(double,response); + GL_ATOMIC(char256,controller); + +private: + PyObject *py_controller; + PyObject *py_args; + PyObject *py_kwargs; + +public: + // event handlers + load(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP presync(TIMESTAMP t1); + TIMESTAMP sync(TIMESTAMP t1); + TIMESTAMP postsync(TIMESTAMP t1); + +public: + // internal properties + static CLASS *oclass; + static load *defaults; +}; + +#endif // _LOAD_H diff --git a/module/pypower/powerline.cpp b/module/pypower/powerline.cpp new file mode 100644 index 000000000..dd6f90d42 --- /dev/null +++ b/module/pypower/powerline.cpp @@ -0,0 +1,166 @@ +// module/pypower/powerline.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(powerline); +EXPORT_INIT(powerline); +EXPORT_PRECOMMIT(powerline); + +CLASS *powerline::oclass = NULL; +powerline *powerline::defaults = NULL; + +powerline::powerline(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"powerline",sizeof(powerline),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class powerline"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_double, "length[mile]", get_length_offset(), + PT_REQUIRED, + PT_DESCRIPTION, "length (miles)", + + PT_complex, "impedance[Ohm/mile]", get_impedance_offset(), + PT_REQUIRED, + PT_DESCRIPTION, "line impedance (Ohm/mile)", + + PT_enumeration, "status", get_status_offset(), + PT_DEFAULT, "IN", + PT_KEYWORD, "IN", (enumeration)PLS_IN, + PT_KEYWORD, "OUT", (enumeration)PLS_OUT, + PT_DESCRIPTION, "line status (IN or OUT)", + + PT_enumeration, "composition", get_composition_offset(), + PT_KEYWORD, "SERIES", (enumeration)PLC_SERIES, + PT_KEYWORD, "PARALLEL", (enumeration)PLC_PARALLEL, + PT_DESCRIPTION, "parent line composition (SERIES or PARALLEL)", + + NULL) < 1 ) + { + throw "unable to publish powerline properties"; + } + } +} + +int powerline::create(void) +{ + parent_is_branch = false; + Z = complex(0,0); + Y = complex(0,0); + ratio = 1.0; + angle = 0.0; + rating = 0.0; + + return 1; // return 1 on success, 0 on failure +} + +int powerline::init(OBJECT *parent_hdr) +{ + powerline *parent = (powerline*)get_parent(); + if ( parent ) + { + if ( parent->isa("branch","pypower") ) + { + branch *parent = (branch*)get_parent(); + parent_is_branch = true; + int32 n_children = parent->get_child_count(); + if ( n_children > 0 ) + { + error("parent '%s' cannot accept more than one child component",get_parent()->get_name()); + return 0; + } + parent->set_child_count(n_children+1); + } + else if ( parent->isa("powerline","pypower") ) + { + if ( ( parent->get_impedance().Re() != 0 || parent->get_impedance().Im() != 0 ) + && ( parent->get_length() > 0 ) ) + { + error("parent '%s' non-zero impedance will be overwritten",get_parent()->get_name()); + return 0; + } + } + else + { + error("parent '%s' is not a pypower branch or powerline",get_parent()->get_name()); + return 0; + } + + } + else + { + warning("powerline without parent does nothing"); + } + + // check impedance + if ( impedance.Re() != 0 || impedance.Im() != 0 ) + { + if ( length <= 0 ) + { + error("line length must be positive to calculate impedance and admittance"); + return 0; + } + Z = impedance * length; + Y = Z.Inv(); + } + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP powerline::precommit(TIMESTAMP t0) +{ + if ( get_parent() != NULL ) + { + if ( parent_is_branch ) + { + branch *parent = (branch*)get_parent(); + + if ( get_status() == PLS_IN ) + { + // copy status/impedance/admittance values to branch + parent->set_r(get_Z().Re()); + parent->set_x(get_Z().Im()); + parent->set_b(get_Y().Im()); + parent->set_rateA(get_rateA()); + parent->set_status(branch::BS_IN); + } + else + { + parent->set_status(branch::BS_OUT); + } + } + else if ( get_status() == PLS_IN ) + { + powerline *parent = (powerline*)get_parent(); + if ( parent->get_composition() == PLC_SERIES ) + { + // add impedance + complex Z = parent->get_Z() + get_Z(); + parent->set_Z(Z); + parent->set_Y(Z.Inv()); + parent->set_rateA(min(parent->get_rateA(),get_rateA())); + } + else if ( parent->get_composition() == PLC_PARALLEL ) + { + complex Y = parent->get_Y() + get_Y(); + parent->set_Y(Y); + parent->set_Z(Y.Inv()); + parent->set_rateA(parent->get_rateA()+get_rateA()); + } + else + { + exception("invalid powerline composition value '%d' encountered",get_composition()); + } + } + } + return TS_NEVER; +} + diff --git a/module/pypower/powerline.h b/module/pypower/powerline.h new file mode 100644 index 000000000..3f4c1e968 --- /dev/null +++ b/module/pypower/powerline.h @@ -0,0 +1,45 @@ +// module/pypower/powerline.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_POWERLINE_H +#define _PYPOWER_POWERLINE_H + +#include "gridlabd.h" + +class powerline : public gld_object +{ + +public: + // published properties + GL_ATOMIC(double,length); + GL_ATOMIC(complex,impedance); + GL_ATOMIC(double,rating); + typedef enum {PLS_OUT=0,PLS_IN=1} POWERLINESTATUS; + GL_ATOMIC(enumeration,status); + typedef enum {PLC_SERIES=1,PLC_PARALLEL=2} POWERLINECOMPOSITION; + GL_ATOMIC(enumeration,composition); + +public: + GL_ATOMIC(double,ratio); + GL_ATOMIC(double,angle); + GL_ATOMIC(double,rateA); + GL_ATOMIC(complex,Z); + GL_ATOMIC(complex,Y); + +public: + bool parent_is_branch; + +public: + // event handlers + powerline(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP precommit(TIMESTAMP t1); + +public: + // internal properties + static CLASS *oclass; + static powerline *defaults; +}; + +#endif // _LOAD_H diff --git a/module/pypower/powerplant.cpp b/module/pypower/powerplant.cpp new file mode 100644 index 000000000..73555b0b3 --- /dev/null +++ b/module/pypower/powerplant.cpp @@ -0,0 +1,306 @@ +// module/pypower/powerplant.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(powerplant); +EXPORT_INIT(powerplant); +EXPORT_SYNC(powerplant); + +CLASS *powerplant::oclass = NULL; +powerplant *powerplant::defaults = NULL; + +powerplant::powerplant(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"powerplant",sizeof(powerplant),PC_PRETOPDOWN|PC_BOTTOMUP|PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class powerplant"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_char32, "city", get_city_offset(), + PT_DESCRIPTION, "City in which powerplant is located", + + PT_char32, "state", get_state_offset(), + PT_DESCRIPTION, "State in which powerplant is located", + + PT_char32, "zipcode", get_zipcode_offset(), + PT_DESCRIPTION, "Zipcode in which powerplant is located", + + PT_char32, "country", get_country_offset(), + PT_DESCRIPTION, "Country in which powerplant is located", + + PT_char32, "naics_code", get_naics_code_offset(), + PT_DESCRIPTION, "Powerplant NAICS code", + + PT_char256, "naics_description", get_naics_description_offset(), + PT_DESCRIPTION, "Powerplant NAICS description", + + PT_int16, "plant_code", get_plant_code(), + PT_DESCRIPTION, "Generator plant code number", + + PT_set, "generator", get_generator_offset(), + PT_KEYWORD, "UNKNOWN", (set)0x00000001, + PT_KEYWORD, "HT", (set)0x00000002, // hydro turbine + PT_KEYWORD, "ST", (set)0x00000004, // steam turbine + PT_KEYWORD, "AT", (set)0x00000008, // compressed air turbine + PT_KEYWORD, "IC", (set)0x00000010, // internal combustion + PT_KEYWORD, "FW", (set)0x00000020, // flywheel + PT_KEYWORD, "WT", (set)0x00000040, // wind turbine + PT_KEYWORD, "ES", (set)0x00000080, // energy storage inverter + PT_KEYWORD, "CT", (set)0x00000100, // combustion turbine + PT_KEYWORD, "PV", (set)0x00000200, // photovoltaic inverter + PT_KEYWORD, "CC", (set)0x00000400, // combined cycle turbine + PT_DESCRIPTION, "Generator type", + + PT_set, "fuel", get_fuel_offset(), + PT_KEYWORD, "ELEC", (set)0x00000001, + PT_KEYWORD, "WIND", (set)0x00000002, + PT_KEYWORD, "SUN", (set)0x00000004, + PT_KEYWORD, "GEO", (set)0x00000008, + PT_KEYWORD, "COKE", (set)0x00000010, + PT_KEYWORD, "WASTE", (set)0x00000020, + PT_KEYWORD, "BIO", (set)0x00000040, + PT_KEYWORD, "OIL", (set)0x00000080, + PT_KEYWORD, "UNKNOWN", (set)0x00000100, + PT_KEYWORD, "WOOD", (set)0x00000200, + PT_KEYWORD, "OTHER", (set)0x00000400, + PT_KEYWORD, "GAS", (set)0x00000800, + PT_KEYWORD, "NUC", (set)0x00001000, + PT_KEYWORD, "WATER", (set)0x00002000, + PT_KEYWORD, "COAL", (set)0x00004000, + PT_KEYWORD, "NG", (set)0x00008000, + PT_DESCRIPTION, "Generator fuel type", + + PT_enumeration, "status", get_status_offset(), + PT_KEYWORD, "OFFLINE", (enumeration)0x00, + PT_KEYWORD, "ONLINE", (enumeration)0x01, + PT_DESCRIPTION, "Generator status", + + PT_double, "operating_capacity[MW]", get_operating_capacity_offset(), + PT_DESCRIPTION, "Generator normal operating capacity (MW)", + + PT_double, "summer_capacity[MW]", get_summer_capacity_offset(), + PT_DESCRIPTION, "Generator summer operating capacity (MW)", + + PT_double, "winter_capacity[MW]", get_winter_capacity_offset(), + PT_DESCRIPTION, "Generator winter operating capacity (MW)", + + PT_double, "capacity_factor[pu]", get_capacity_factor_offset(), + PT_DESCRIPTION, "Generator capacity factor (pu)", + + PT_char256, "substation_1", get_substation_1_offset(), + PT_DESCRIPTION, "Substation 1 id", + + PT_char256, "substation_2", get_substation_2_offset(), + PT_DESCRIPTION, "Substation 2 id", + + PT_complex, "S[VA]", get_S_offset(), + PT_DESCRIPTION, "power generation (VA)", + + PT_char256, "controller", get_controller_offset(), + PT_DESCRIPTION,"controller python function name", + + NULL) < 1 ) + { + throw "unable to publish powerplant properties"; + } + } +} + +int powerplant::create(void) +{ + py_controller = NULL; + py_args = PyTuple_New(1); + py_kwargs = PyDict_New(); + + return 1; // return 1 on success, 0 on failure +} + +int powerplant::init(OBJECT *parent_hdr) +{ + gen *parent = (gen*)get_parent(); + if ( parent ) + { + if ( parent->isa("gen","pypower") ) + { + is_dynamic = TRUE; + } + else if ( parent->isa("bus","pypower") ) + { + is_dynamic = FALSE; + } + else + { + error("parent '%s' is not a pypower bus or gen object",get_parent()->get_name()); + return 0; + } + } + + extern PyObject *py_globals; + if ( py_globals != NULL && controller[0] != '\0' ) + { + py_controller = PyDict_GetItemString(py_globals,controller); + if ( py_controller == NULL ) + { + error("pypower controller '%s' is not found",(const char *)controller); + return 0; + } + if ( ! PyCallable_Check(py_controller) ) + { + Py_DECREF(py_controller); + error("pypower controller '%s' is not callable",(const char *)controller); + return 0; + } + } + + if ( get_name() ) + { + PyTuple_SET_ITEM(py_args,0,PyUnicode_FromString(get_name())); + } + else + { + char buffer[80]; + snprintf(buffer,sizeof(buffer)-1,"%64s:%ld",get_oclass()->get_name(),(long)get_id()); + PyTuple_SET_ITEM(py_args,0,PyUnicode_FromString(buffer)); + } + PyDict_SetItemString(py_kwargs,"city",PyUnicode_FromString(get_city())); + PyDict_SetItemString(py_kwargs,"state",PyUnicode_FromString(get_state())); + PyDict_SetItemString(py_kwargs,"zipcode",PyUnicode_FromString(get_zipcode())); + PyDict_SetItemString(py_kwargs,"country",PyUnicode_FromString(get_country())); + PyDict_SetItemString(py_kwargs,"naics_code",PyUnicode_FromString(get_naics_code())); + PyDict_SetItemString(py_kwargs,"naics_description",PyUnicode_FromString(get_naics_description())); + PyDict_SetItemString(py_kwargs,"generator",PyLong_FromLong(get_generator())); + PyDict_SetItemString(py_kwargs,"fuel",PyLong_FromLong(get_fuel())); + PyDict_SetItemString(py_kwargs,"operating_capacity",PyFloat_FromDouble(get_operating_capacity())); + PyDict_SetItemString(py_kwargs,"summer_capacity",PyFloat_FromDouble(get_summer_capacity())); + PyDict_SetItemString(py_kwargs,"winter_capacity",PyFloat_FromDouble(get_winter_capacity())); + PyDict_SetItemString(py_kwargs,"capacity_factor",PyFloat_FromDouble(get_capacity_factor())); + PyDict_SetItemString(py_kwargs,"substation_1",PyUnicode_FromString(get_substation_1())); + PyDict_SetItemString(py_kwargs,"substation_2",PyUnicode_FromString(get_substation_2())); + Py_complex z = {get_S().Re(), get_S().Im()}; + PyDict_SetItemString(py_kwargs,"S",PyComplex_FromCComplex(z)); + if ( get_parent() && get_parent()->isa("bus","pypower") ) + { + bus *parent = (bus*)get_parent(); + PyDict_SetItemString(py_kwargs,"Vm",PyFloat_FromDouble(parent->get_Vm())); + PyDict_SetItemString(py_kwargs,"Va",PyFloat_FromDouble(parent->get_Va())); + } + PyDict_SetItemString(py_kwargs,"controller",PyUnicode_FromString(get_controller())); + PyDict_SetItemString(py_kwargs,"t",PyFloat_FromDouble((double)gl_globalclock)); + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP powerplant::presync(TIMESTAMP t0) +{ + // copy data to parent + if ( is_dynamic ) // gen parent + { + if ( S.Re() != 0 || S.Im() != 0 ) + { + gen *parent = (gen*)get_parent(); + parent->set_Pg(parent->get_Pg()-S.Re()); + parent->set_Qg(parent->get_Qg()-S.Im()); + } + } + else // bus parent + { + if ( S.Re() != 0 || S.Im() != 0 ) + { + bus *parent = (bus*)get_parent(); + parent->set_Pd(parent->get_Pd()-S.Re()); + parent->set_Qd(parent->get_Qd()-S.Im()); + } + } + return TS_NEVER; +} + +TIMESTAMP powerplant::sync(TIMESTAMP t0) +{ + TIMESTAMP t1 = TS_NEVER; + + if ( py_controller ) + { + Py_complex z = {get_S().Re(), get_S().Im()}; + PyDict_SetItemString(py_kwargs,"S",PyComplex_FromCComplex(z)); + if ( get_parent() && get_parent()->isa("bus","pypower") ) + { + bus *parent = (bus*)get_parent(); + PyDict_SetItemString(py_kwargs,"Vm",PyFloat_FromDouble(parent->get_Vm())); + PyDict_SetItemString(py_kwargs,"Va",PyFloat_FromDouble(parent->get_Va())); + } + PyDict_SetItemString(py_kwargs,"controller",PyUnicode_FromString(get_controller())); + PyDict_SetItemString(py_kwargs,"t",PyFloat_FromDouble((double)gl_globalclock)); + + PyObject *result = PyObject_Call(py_controller,py_args,py_kwargs); + if ( result == NULL ) + { + if ( PyErr_Occurred() ) + { + PyErr_Print(); + } + else + { + error("controller call failed (no info)"); + } + + extern bool stop_on_failure; + if ( stop_on_failure ) + { + return TS_INVALID; + } + } + else if ( PyDict_Check(result) ) + { + PyObject *key, *value; + Py_ssize_t pos = 0; + while ( PyDict_Next(result,&pos,&key,&value) ) + { + PyObject *repr = PyObject_Str(key); + const char *name = PyUnicode_AsUTF8(repr); + if ( strcmp(name,"t") == 0 ) + { + t1 = (TIMESTAMP)PyFloat_AsDouble(value); + } + else + { + gld_property prop(my(),name); + Py_DECREF(repr); + + if ( prop.is_valid() ) + { + repr = PyObject_Str(value); + const char *data = PyUnicode_AsUTF8(repr); + prop.from_string(data); + Py_DECREF(repr); + } + else + { + error("controller return property '%s' is not valid",name); + } + } + } + Py_DECREF(result); + } + else + { + error("controller return value not a property update dictionary"); + Py_DECREF(result); + } + } + return t1; +} + +TIMESTAMP powerplant::postsync(TIMESTAMP t0) +{ + // cannot separate contribution of this powerplant to total gen Pg,Qg + exception("invalid postsync event requrest"); + return TS_NEVER; +} diff --git a/module/pypower/powerplant.h b/module/pypower/powerplant.h new file mode 100644 index 000000000..adc2b5dc0 --- /dev/null +++ b/module/pypower/powerplant.h @@ -0,0 +1,56 @@ +// module/pypower/powerplant.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_POWERPLANT_H +#define _PYPOWER_POWERPLANT_H + +#include "gridlabd.h" + +class powerplant : public gld_object +{ + +public: + // published properties + GL_STRING(char32,city); + GL_STRING(char32,state); + GL_STRING(char32,zipcode); + GL_STRING(char32,country); + GL_STRING(char32,naics_code); + GL_STRING(char256,naics_description); + GL_ATOMIC(int16,plant_code); + GL_ATOMIC(set,generator); + GL_ATOMIC(set,fuel); + GL_ATOMIC(enumeration,status) + GL_ATOMIC(double,operating_capacity); + GL_ATOMIC(double,summer_capacity); + GL_ATOMIC(double,winter_capacity); + GL_ATOMIC(double,capacity_factor); + GL_ATOMIC(char256,substation_1); + GL_ATOMIC(char256,substation_2); + GL_ATOMIC(complex,S); + GL_ATOMIC(char256,controller); + +private: + PyObject *py_controller; + PyObject *py_args; + PyObject *py_kwargs; + +private: + bool is_dynamic; // true if parent is a gen otherwise false + +public: + // event handlers + powerplant(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP presync(TIMESTAMP t1); + TIMESTAMP sync(TIMESTAMP t1); + TIMESTAMP postsync(TIMESTAMP t1); + +public: + // internal properties + static CLASS *oclass; + static powerplant *defaults; +}; + +#endif // _LOAD_H diff --git a/module/pypower/pypower.cpp b/module/pypower/pypower.cpp new file mode 100644 index 000000000..d4e593447 --- /dev/null +++ b/module/pypower/pypower.cpp @@ -0,0 +1,583 @@ +// module/pypower/pypower.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#define DLMAIN + +#include "pypower.h" + +#include "Python.h" + +bool enable_opf = false; +double base_MVA = 100.0; +int32 pypower_version = 2; +bool stop_on_failure = false; +int32 maximum_timestep = 0; // seconds; 0 = no max ts +enumeration solver_method = 1; +double solver_update_resolution = 1e-8; +bool save_case = false; + +enum { + SS_INIT = 0, + SS_SUCCESS = 1, + SS_FAILED = 2, +} solver_status; + +char1024 controllers; +char1024 controllers_path; +PyObject *py_controllers; +PyObject *py_globals; +PyObject *py_sync; + +EXPORT CLASS *init(CALLBACKS *fntable, MODULE *module, int argc, char *argv[]) +{ + if (set_callback(fntable)==NULL) + { + errno = EINVAL; + return NULL; + } + + INIT_MMF(pypower); + + new bus(module); + new branch(module); + new gen(module); + new gencost(module); + new load(module); + new powerplant(module); + new powerline(module); + + gl_global_create("pypower::version", + PT_int32, &pypower_version, + PT_DESCRIPTION, "Version of pypower used", + NULL); + + gl_global_create("pypower::solver_method", + PT_enumeration, &solver_method, + PT_KEYWORD, "GS", (enumeration)4, + PT_KEYWORD, "FD_BX", (enumeration)3, + PT_KEYWORD, "FD_XB", (enumeration)2, + PT_KEYWORD, "NR", (enumeration)1, + PT_DESCRIPTION, "PyPower solver method to use", + NULL + ); + + gl_global_create("pypower::maximum_timestep", + PT_int32, &maximum_timestep, + PT_DESCRIPTION, "Maximum timestep allowed between solutions", + NULL); + + gl_global_create("pypower::baseMVA", + PT_double, &base_MVA, + PT_UNITS, "MVA", + PT_DESCRIPTION, "Base MVA value", + NULL); + + gl_global_create("pypower::enable_opf", + PT_bool, &enable_opf, + PT_DESCRIPTION, "Flag to enable optimal powerflow (OPF) solver", + NULL); + + gl_global_create("pypower::stop_on_failure", + PT_bool, &stop_on_failure, + PT_DESCRIPTION, "Flag to stop simulation on solver failure", + NULL); + + gl_global_create("pypower::save_case", + PT_bool, &save_case, + PT_DESCRIPTION, "Flag to save pypower case data and results", + NULL); + + gl_global_create("pypower::controllers_path", + PT_char1024, &controllers_path, + PT_DESCRIPTION, "Path to find module containing controller functions", + NULL); + + gl_global_create("pypower::controllers", + PT_char1024, &controllers, + PT_DESCRIPTION, "Python module containing controller functions", + NULL); + + gl_global_create("pypower::solver_update_resolution", + PT_double, &solver_update_resolution, + PT_DESCRIPTION, "Minimum difference before a value is considered changed", + NULL); + + gl_global_create("pypower::solver_status", + PT_enumeration, &solver_status, + PT_KEYWORD, "INIT", (enumeration)SS_INIT, + PT_KEYWORD, "SUCCESS", (enumeration)SS_SUCCESS, + PT_KEYWORD, "FAILED", (enumeration)SS_FAILED, + PT_DESCRIPTION, "Result of the last pypower solver run", + NULL); + + + // always return the first class registered + return bus::oclass; +} + +PyObject *solver = NULL; +PyObject *data = NULL; + +size_t nbus = 0; +bus *buslist[MAXENT]; +PyObject *busdata = NULL; + +size_t nbranch = 0; +branch *branchlist[MAXENT]; +PyObject *branchdata = NULL; + +size_t ngen = 0; +gen *genlist[MAXENT]; +PyObject *gendata = NULL; + +size_t ngencost = 0; +gencost *gencostlist[MAXENT]; +PyObject *gencostdata = NULL; + +EXPORT bool on_init(void) +{ + // import controllers, if any + if ( controllers[0] != '\0' ) + { + if ( controllers_path[0] != '\0' ) + { + char buffer[2000]; + snprintf(buffer,2000,"import sys\n" + "sys.path.append('%s')\n", (const char *)controllers_path); + PyRun_SimpleString(buffer); + } + py_controllers = PyImport_ImportModule(controllers); + if ( py_controllers == NULL ) + { + if ( PyErr_Occurred() ) + { + PyErr_Print(); + } + else + { + gl_error("unable to load controllers module '%s'",(const char*)controllers); + } + return false; + } + + py_globals = PyModule_GetDict(py_controllers); + if ( py_globals == NULL ) + { + gl_error("unable to get globals in '%s'",(const char*)controllers); + return false; + } + Py_INCREF(py_globals); + + PyObject *on_init = PyDict_GetItemString(py_globals,"on_init"); + if ( on_init ) + { + if ( ! PyCallable_Check(on_init) ) + { + gl_error("%s.on_init() is not callable",(const char*)controllers); + Py_DECREF(on_init); + return false; + } + PyObject *result = PyObject_CallNoArgs(on_init); + if ( result == NULL ) + { + if ( PyErr_Occurred() ) + { + PyErr_Print(); + } + else + { + gl_error("%s.on_init() return None (expected bool)",(const char*)controllers); + } + return false; + } + if ( PyBool_Check(result) ) + { + if ( ! PyObject_IsTrue(result) ) + { + gl_error("%s.on_init() failed (returned False)",(const char*)controllers); + Py_DECREF(result); + return false; + } + } + else + { + gl_error("%s.on_init() returned non-boolean value (expected True or False)",(const char*)controllers); + Py_DECREF(result); + return false; + } + Py_DECREF(result); + } + + py_sync = PyDict_GetItemString(py_globals,"on_sync"); + if ( py_sync ) + { + if ( ! PyCallable_Check(py_sync) ) + { + gl_error("%s.on_sync() is not callable",(const char*)controllers); + return false; + } + Py_INCREF(py_sync); + } + } + + // import pypower solver + PyObject *module = PyImport_ImportModule("pypower_solver"); + if ( module == NULL ) + { + if ( PyErr_Occurred() ) + { + PyErr_Print(); + } + else + { + gl_error("unable to load pypower solver module (no info)"); + } + return false; + } + solver = PyObject_GetAttrString(module,"solver"); + if ( solver == NULL ) + { + gl_error("unable to find pypower solver call"); + return false; + } + + // first time setup of arrays + data = PyDict_New(); + PyDict_SetItemString(data,"version",PyLong_FromLong((long)pypower_version)); + PyDict_SetItemString(data,"baseMVA",PyFloat_FromDouble((double)base_MVA)); + + busdata = PyList_New(nbus); + for ( size_t n = 0 ; n < nbus ; n++ ) + { + PyList_SET_ITEM(busdata,n,PyList_New(enable_opf?17:13)); + } + PyDict_SetItemString(data,"bus",busdata); + + branchdata = PyList_New(nbranch); + PyDict_SetItemString(data,"branch",branchdata); + for ( size_t n = 0 ; n < nbranch ; n++ ) + { + PyList_SET_ITEM(branchdata,n,PyList_New(13)); + } + + gendata = PyList_New(ngen); + PyDict_SetItemString(data,"gen",gendata); + for ( size_t n = 0 ; n < ngen ; n++ ) + { + PyList_SET_ITEM(gendata,n,PyList_New(enable_opf?25:21)); + } + + if ( enable_opf ) + { + gencostdata = PyList_New(ngencost); + PyDict_SetItemString(data,"gencost",gencostdata); + for ( size_t n = 0; n < ngencost ; n++ ) + { + PyList_SET_ITEM(gencostdata,n,PyList_New(4)); + } + } + + // set options + gld_global global_verbose("verbose"); + PyDict_SetItemString(data,"verbose",global_verbose=="TRUE"?Py_True:Py_False); + + gld_global global_debug("debug"); + PyDict_SetItemString(data,"debug",global_debug=="TRUE"?Py_True:Py_False); + + PyDict_SetItemString(data,"solver_method",PyLong_FromLong(solver_method)); + PyDict_SetItemString(data,"save_case",save_case?Py_True:Py_False); + + return true; +} + +// conditional solver send/receive (only if value differs or is not set yet) +#define SEND(INDEX,NAME,FROM,TO) { PyObject *py = PyList_GetItem(pyobj,INDEX); \ + if ( py == NULL || fabs(obj->get_##NAME()-Py##TO##_As##FROM(py)) > solver_update_resolution ) { \ + PyObject *value = Py##TO##_From##FROM(obj->get_##NAME()); \ + if ( value == NULL ) { \ + gl_warning("pypower:on_sync(t0=%lld): unable to create value " #NAME " for data item %d",t0,INDEX); \ + } \ + else { \ + PyList_SET_ITEM(pyobj,INDEX,value); \ + Py_XDECREF(py); \ + n_changes++; \ +}}} + +#define RECV(NAME,INDEX,FROM,TO) { PyObject *py = PyList_GET_ITEM(pyobj,INDEX);\ + if ( fabs(obj->get_##NAME()-Py##FROM##_As##TO(py)) > solver_update_resolution ) { \ + n_changes++; \ + obj->set_##NAME(Py##FROM##_As##TO(py)); \ + }} + +EXPORT TIMESTAMP on_sync(TIMESTAMP t0) +{ + // not a pypower model + if ( nbus == 0 || nbranch == 0 ) + { + return TS_NEVER; + } + + int n_changes = 0; + + // send values out to solver + for ( size_t n = 0 ; n < nbus ; n++ ) + { + bus *obj = buslist[n]; + PyObject *pyobj = PyList_GetItem(busdata,n); + SEND(0,bus_i,Double,Float) + SEND(1,type,Long,Long) + SEND(2,Pd,Double,Float) + SEND(3,Qd,Double,Float) + SEND(4,Gs,Double,Float) + SEND(5,Bs,Double,Float) + SEND(6,area,Long,Long) + SEND(7,Vm,Double,Float) + SEND(8,Va,Double,Float) + SEND(9,baseKV,Double,Float) + SEND(10,zone,Long,Long) + SEND(11,Vmax,Double,Float) + SEND(12,Vmin,Double,Float) + if ( enable_opf ) + { + SEND(13,lam_P,Double,Float) + SEND(14,lam_Q,Double,Float) + SEND(15,mu_Vmax,Double,Float) + SEND(16,mu_Vmin,Double,Float) + } + } + for ( size_t n = 0 ; n < nbranch ; n++ ) + { + branch *obj = branchlist[n]; + PyObject *pyobj = PyList_GetItem(branchdata,n); + SEND(0,fbus,Long,Long) + SEND(1,tbus,Long,Long) + SEND(2,r,Double,Float) + SEND(3,x,Double,Float) + SEND(4,b,Double,Float) + SEND(5,rateA,Double,Float) + SEND(6,rateB,Double,Float) + SEND(7,rateC,Double,Float) + SEND(8,ratio,Double,Float) + SEND(9,angle,Double,Float) + SEND(10,status,Long,Long) + SEND(11,angmin,Double,Float) + SEND(12,angmax,Double,Float) + + } + for ( size_t n = 0 ; n < ngen ; n++ ) + { + gen *obj = genlist[n]; + PyObject *pyobj = PyList_GetItem(gendata,n); + SEND(0,bus,Long,Long) + SEND(1,Pg,Double,Float) + SEND(2,Qg,Double,Float) + SEND(3,Qmax,Double,Float) + SEND(4,Qmin,Double,Float) + SEND(5,Vg,Double,Float) + SEND(6,mBase,Double,Float) + SEND(7,status,Long,Long) + SEND(8,Pmax,Double,Float) + SEND(9,Pmin,Double,Float) + SEND(10,Pc1,Double,Float) + SEND(11,Pc2,Double,Float) + SEND(12,Qc1min,Double,Float) + SEND(13,Qc1max,Double,Float) + SEND(14,Qc2min,Double,Float) + SEND(15,Qc2max,Double,Float) + SEND(16,ramp_agc,Double,Float) + SEND(17,ramp_10,Double,Float) + SEND(18,ramp_30,Double,Float) + SEND(19,ramp_q,Double,Float) + SEND(20,apf,Double,Float) + if ( enable_opf ) + { + SEND(21,mu_Pmax,Double,Float) + SEND(22,mu_Pmin,Double,Float) + SEND(23,mu_Qmax,Double,Float) + SEND(24,mu_Qmin,Double,Float) + } + } + if ( gencostdata ) + { + for ( size_t n = 0 ; n < ngencost ; n++ ) + { + gencost *obj = gencostlist[n]; + PyObject *pyobj = PyList_GetItem(gencostdata,n); + SEND(0,model,Long,Long) + SEND(1,startup,Double,Float) + SEND(2,shutdown,Double,Float) + PyObject *py = PyList_GetItem(pyobj,3); + if ( py == NULL || strcmp((const char*)PyUnicode_DATA(py),obj->get_costs())!=0 ) + { + Py_XDECREF(py); + PyList_SET_ITEM(pyobj,3,PyUnicode_FromString(obj->get_costs())); + } + } + } + + // run controller on_sync, if any + TIMESTAMP t1 = TS_NEVER; + if ( py_sync ) + { + PyDict_SetItemString(data,"t",PyLong_FromLong(t0)); + PyErr_Clear(); + PyObject *ts = PyObject_CallOneArg(py_sync,data); + if ( PyErr_Occurred() ) + { + PyErr_Print(); + return TS_INVALID; + } + if ( ts == NULL || ! PyLong_Check(ts) ) + { + gl_error("%s.on_sync(data) returned value that is not a valid timestamp",(const char*)controllers); + Py_XDECREF(ts); + return TS_INVALID; + } + t1 = PyLong_AsLong(ts); + Py_DECREF(ts); + if ( t1 < 0 ) + { + t1 = TS_NEVER; + } + else if ( t1 == 0 && stop_on_failure ) + { + gl_error("%s.on_sync(data) halted the simulation",(const char*)controllers); + return TS_INVALID; + } + else if ( t1 < t0 ) + { + gl_error("%s.on_sync(data) returned a timestamp earlier than sync time t0=%lld",(const char*)controllers,t0); + return TS_INVALID; + } + } + + // run solver + static PyObject *result = NULL; + if ( result == NULL || n_changes > 0 ) + { + // run pypower solver + if ( result != data ) + { + Py_XDECREF(result); + } + PyErr_Clear(); + result = PyObject_CallOneArg(solver,data); + + // receive results (if new) + if ( result != NULL ) + { + if ( result == Py_False ) + { + if ( stop_on_failure ) + { + gl_error("pypower solver failed"); + solver_status = SS_FAILED; + return TS_INVALID; + } + else + { + gl_warning("pypower solver failed"); + solver_status = SS_FAILED; + return TS_NEVER; + } + } + else if ( ! PyDict_Check(result) ) + { + gl_error("pypower solver returned invalid result type (not a dict)"); + solver_status = SS_FAILED; + return TS_INVALID; + } + + // copy values back from solver + n_changes = 0; + PyObject *busdata = PyDict_GetItemString(result,"bus"); + if ( nbus > 0 && busdata == NULL ) + { + gl_error("pypower solver did not return any bus data"); + solver_status = SS_FAILED; + return TS_INVALID; + } + for ( size_t n = 0 ; n < nbus ; n++ ) + { + bus *obj = buslist[n]; + PyObject *pyobj = PyList_GetItem(busdata,n); + RECV(Vm,7,Float,Double) + RECV(Va,8,Float,Double) + + if ( enable_opf ) + { + RECV(lam_P,13,Float,Double) + RECV(lam_Q,14,Float,Double) + RECV(mu_Vmax,15,Float,Double) + RECV(mu_Vmin,16,Float,Double) + } + } + + PyObject *gendata = PyDict_GetItemString(result,"gen"); + if ( ngencost > 0 && gendata == NULL ) + { + gl_error("pypower solver did not return any gen data"); + solver_status = SS_FAILED; + return TS_INVALID; + } + for ( size_t n = 0 ; n < ngen ; n++ ) + { + gen *obj = genlist[n]; + PyObject *pyobj = PyList_GetItem(gendata,n); + RECV(Pg,1,Float,Double) + RECV(Qg,2,Float,Double) + RECV(apf,20,Float,Double) + if ( enable_opf ) + { + RECV(mu_Pmax,21,Float,Double) + RECV(mu_Pmin,22,Float,Double) + RECV(mu_Qmax,23,Float,Double) + RECV(mu_Qmin,24,Float,Double) + } + } + } + } + + PyErr_Clear(); + if ( result == NULL && stop_on_failure ) + { + gl_warning("pypower solver failed"); + solver_status = SS_FAILED; + return TS_INVALID; + } + else + { + if ( ! result ) + { + gl_warning("pypower solver failed"); + solver_status = SS_FAILED; + } + else + { + solver_status = SS_SUCCESS; + } + if ( n_changes > 0 ) + { + return t0; + } + TIMESTAMP t2 = maximum_timestep > 0 ? t0+maximum_timestep : TS_NEVER; + return (TIMESTAMP)min((unsigned long long)t1,(unsigned long long)t2); + + } +} + +EXPORT int do_kill(void*) +{ + // if global memory needs to be released, this is a good time to do it + Py_XDECREF(busdata); + Py_XDECREF(branchdata); + Py_XDECREF(gendata); + Py_XDECREF(gencostdata); + + return 0; +} + +EXPORT int check(){ + // if any assert objects have bad filenames, they'll fail on init() + return 0; +} diff --git a/module/pypower/pypower.h b/module/pypower/pypower.h new file mode 100644 index 000000000..4bcfd3096 --- /dev/null +++ b/module/pypower/pypower.h @@ -0,0 +1,27 @@ +// module/pypower/pypower.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_H +#define _PYPOWER_H + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <math.h> + +#include "gridlabd.h" + +#include "bus.h" +#include "branch.h" +#include "gen.h" +#include "gencost.h" +#include "load.h" +#include "powerplant.h" +#include "powerline.h" +#include "relay.h" +#include "scada.h" +#include "transformer.h" + +#define MAXENT 30000 // maximum number of bus/branch/gen/gencost entities supported + +#endif diff --git a/module/pypower/pypower_solver.py b/module/pypower/pypower_solver.py new file mode 100644 index 000000000..53538be97 --- /dev/null +++ b/module/pypower/pypower_solver.py @@ -0,0 +1,109 @@ +# module/pypower/pypower_solver.py +# Copyright (C) 2024 Regents of the Leland Stanford Junior University + +import os, sys +from pypower.api import case14, ppoption, runpf, runopf +from numpy import array, set_printoptions, inf + +set_printoptions(threshold=inf,linewidth=inf,formatter={'float':lambda x:f"{x:.6g}"}) + +# TODO: read these values from the pf_case argument +save_case = False +debug = False +verbose = False +solver_method = 1 # 1=NR, 2=FD-XB, 3=FD-BX, 4=GS +solution_tolerance = 1e-08 +maximum_iterations_nr = 10 +maximum_iterations_fd = 30 +maximum_iterations_gs = 1000 +enforce_q_limits = False +use_dc_powerflow = False + +def solver(pf_case): + + try: + + # read options from case + for key in globals(): + if key in pf_case: + globals()[key] = pf_case[key] + options = ppoption( + PF_ALG = solver_method, + PF_TOL = solution_tolerance, + PF_MAX_IT = maximum_iterations_nr, + PF_MAX_IT_FD = maximum_iterations_fd, + PF_MAX_IT_GS = maximum_iterations_gs, + ENFORCE_Q_LIMS = enforce_q_limits, + PF_DC = use_dc_powerflow, + OUT_ALL = 1 if debug else 0, + VERBOSE = 3 if verbose else 0, + OUT_SYS_SUM = verbose, + OUT_AREA_SUM = verbose, + OUT_BUS = verbose, + OUT_BRTANCH = verbose, + OUT_GEN = verbose, + OUT_ALL_LIM = verbose, + OUT_V_LIM = verbose, + OUT_LINE_LIM = verbose, + OUT_PG_LIM = verbose, + OUT_QG_LIM = verbose, + ) + for key in options: + if key in pf_case: + options[key] = pf_case[key] + + # setup casedata + casedata = dict(version=str(pf_case['version']),baseMVA=pf_case['baseMVA']) + + # copy from model + for name in ['bus','gen','branch']: + if name in pf_case: + casedata[name] = array(pf_case[name]) + if 'gencost' in pf_case: + costdata = [] + for cost in pf_case['gencost']: + costs = [float(x) for x in cost[3].split(',')] + costdata.append([int(cost[0]),cost[1],cost[2],len(costs)]) + costdata[-1].extend(costs) + casedata['gencost'] = array(costdata) + + # save casedata to file + if save_case: + with open("pypower_casedata.py","w") as fh: + fh.write(str(casedata)) + + # run OPF solver if gencost data is found + if 'gencost' in casedata: + results = runopf(casedata,options) + success = results['success'] + else: + results,success = runpf(casedata,options) + + # save results to file + if save_case: + with open("pypower_results.py","w") as fh: + fh.write(str(results)) + + # copy back to model + if success: + + # print(" --> SUCCESS:",results,file=sys.stderr,flush=True) + for name in ['bus','gen','branch']: + pf_case[name] = results[name].tolist() + return pf_case + + else: + + print(" --> FAILED:",results,file=sys.stderr,flush=True) + return False + + except Exception: + + e_type,e_value,e_trace = sys.exc_info() + + print("EXCEPTION [pypower_solver.py]:",e_type,e_value,file=sys.stderr,flush=True) + import traceback + traceback.print_exception(e_type,e_value,e_trace,file=sys.stderr) + + return False + diff --git a/module/pypower/relay.cpp b/module/pypower/relay.cpp new file mode 100644 index 000000000..d9b7599f4 --- /dev/null +++ b/module/pypower/relay.cpp @@ -0,0 +1,140 @@ +// module/pypower/relay.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(relay); +EXPORT_INIT(relay); +EXPORT_PRECOMMIT(relay); + +CLASS *relay::oclass = NULL; +relay *relay::defaults = NULL; + +relay::relay(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"relay",sizeof(relay),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class relay"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_enumeration, "status", get_status_offset(), + PT_DEFAULT, "IN", + PT_KEYWORD, "IN", (enumeration)RS_IN, + PT_KEYWORD, "OUT", (enumeration)RS_OUT, + PT_DESCRIPTION, "relay status (IN or OUT)", + + NULL) < 1 ) + { + throw "unable to publish relay properties"; + } + } +} + +int relay::create(void) +{ + parent_is_branch = false; + return 1; // return 1 on success, 0 on failure +} + +int relay::init(OBJECT *parent_hdr) +{ + powerline *parent = (powerline*)get_parent(); + if ( parent ) + { + if ( parent->isa("branch","pypower") ) + { + branch *parent = (branch*)get_parent(); + parent_is_branch = true; + int32 n_children = parent->get_child_count(); + if ( n_children > 0 ) + { + error("parent '%s' cannot accept more than one child component",get_parent()->get_name()); + return 0; + } + parent->set_child_count(n_children+1); + } + else if ( parent->isa("powerline","pypower") ) + { + if ( ( parent->get_impedance().Re() != 0 || parent->get_impedance().Im() != 0 ) + && ( parent->get_length() > 0 ) ) + { + error("parent '%s' non-zero impedance will be overwritten",get_parent()->get_name()); + return 0; + } + if ( parent->get_composition() == powerline::PLC_PARALLEL ) + { + error("parent '%s' must have series composition",get_parent()->get_name()); + return 0; + } + } + else + { + error("parent '%s' is not a pypower branch or powerline",get_parent()->get_name()); + return 0; + } + } + else + { + warning("relay without parent does nothing"); + } + + // check impedance + if ( impedance.Re() == 0 || impedance.Im() == 0 ) + { + error("relay impedance must be positive"); + return 0; + } + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP relay::precommit(TIMESTAMP t0) +{ + if ( get_parent() != NULL ) + { + if ( parent_is_branch ) + { + branch *parent = (branch*)get_parent(); + + if ( get_status() == RS_IN ) + { + // copy status/impedance/admittance values to branch + parent->set_r(get_impedance().Re()); + parent->set_x(get_impedance().Im()); + parent->set_b(get_impedance().Inv().Im()); + parent->set_rateA(get_rating()); + parent->set_status(branch::BS_IN); + } + else + { + parent->set_status(branch::BS_OUT); + } + } + else + { + powerline *parent = (powerline*)get_parent(); + if ( get_status() == RS_IN ) + { + // add impedance + complex Z = parent->get_Z() + get_impedance(); + parent->set_Z(Z); + parent->set_Y(Z.Inv()); + parent->set_rateA(min(parent->get_rateA(),get_rating())); + } + else + { + parent->set_status(powerline::PLS_IN); + } + } + } + + return TS_NEVER; +} + diff --git a/module/pypower/relay.h b/module/pypower/relay.h new file mode 100644 index 000000000..0486f023e --- /dev/null +++ b/module/pypower/relay.h @@ -0,0 +1,39 @@ +// module/pypower/relay.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_RELAY_H +#define _PYPOWER_RELAY_H + +#include "gridlabd.h" + +class relay : public gld_object +{ + +public: + // published properties + GL_ATOMIC(complex,impedance); + GL_ATOMIC(double,rating); + typedef enum {RS_OUT=0,RS_IN=1} RELAYSTATUS; + GL_ATOMIC(enumeration,status); + +public: + GL_ATOMIC(complex,Z); + GL_ATOMIC(complex,Y); + +public: + bool parent_is_branch; + +public: + // event handlers + relay(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP precommit(TIMESTAMP t1); + +public: + // internal properties + static CLASS *oclass; + static relay *defaults; +}; + +#endif // _LOAD_H diff --git a/module/pypower/scada.cpp b/module/pypower/scada.cpp new file mode 100644 index 000000000..879be582c --- /dev/null +++ b/module/pypower/scada.cpp @@ -0,0 +1,108 @@ +// module/pypower/scada.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(scada); +EXPORT_INIT(scada); +EXPORT_SYNC(scada); + +CLASS *scada::oclass = NULL; +scada *scada::defaults = NULL; + +scada::scada(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"scada",sizeof(scada),PC_POSTTOPDOWN|PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class scada"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_complex,"V[V]",get_V_offset(), + + PT_double,"Vm[V]",get_Vm_offset(), + + PT_double,"Va[deg]",get_Va_offset(), + + PT_complex,"I",get_I_offset(), + + PT_complex,"S",get_S_offset(), + + PT_double,"P",PADDR(get_S().Re()), + + PT_double,"Q",PADDR(get_S().Re()), + + NULL) < 1 ) + { + throw "unable to publish scada properties"; + } + } +} + +int scada::create(void) +{ + parent_is_branch = false; + return 1; // return 1 on success, 0 on failure +} + +int scada::init(OBJECT *parent) +{ + if ( get_parent() == NULL ) + { + warning("scada object without parent is ignored"); + } + else if ( get_parent()->isa("branch","pypower") ) + { + parent_is_branch = true; + } + else if ( get_parent()->isa("bus","pypower") ) + { + parent_is_branch = false; + } + else + { + error("parent '%s' is not a pypower bus or branch",get_parent()->get_name()); + return 0; + } + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP scada::presync(TIMESTAMP t0) +{ + exception("invalid presync event call"); + return TS_NEVER; +} + +TIMESTAMP scada::sync(TIMESTAMP t0) +{ + exception("invalid sync event call"); + return TS_NEVER; +} + +TIMESTAMP scada::postsync(TIMESTAMP t0) +{ + if ( parent_is_branch ) + { + branch *parent = (branch*)get_parent(); + complex Z(parent->get_r(),parent->get_x()); + // bus *from = NULL + // bus *to = NULL; + // complex DeltaV(to->get_V()-from->get_V()); + } + else + { + bus *parent = (bus*)get_parent(); + set_Vm(parent->get_Vm()); + set_Va(parent->get_Va()); + // get_S().SetPolar(Vm,Va,'d'); + } + return TS_NEVER; +} + diff --git a/module/pypower/scada.h b/module/pypower/scada.h new file mode 100644 index 000000000..b1398352b --- /dev/null +++ b/module/pypower/scada.h @@ -0,0 +1,39 @@ +// module/pypower/scada.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_SCADA_H +#define _PYPOWER_SCADA_H + +#include "gridlabd.h" + +class scada : public gld_object +{ + +public: + // published properties + GL_ATOMIC(complex,V); + GL_ATOMIC(double,Vm); + GL_ATOMIC(double,Va); + GL_ATOMIC(complex,I); + GL_ATOMIC(complex,S); + +private: + bool parent_is_branch; + +public: + + // event handlers + scada(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP presync(TIMESTAMP t0); + TIMESTAMP sync(TIMESTAMP t0); + TIMESTAMP postsync(TIMESTAMP t0); + +public: + // internal properties + static CLASS *oclass; + static scada *defaults; +}; + +#endif // _BUS_H diff --git a/module/pypower/transformer.cpp b/module/pypower/transformer.cpp new file mode 100644 index 000000000..beea590fc --- /dev/null +++ b/module/pypower/transformer.cpp @@ -0,0 +1,148 @@ +// module/pypower/transformer.cpp +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#include "pypower.h" + +EXPORT_CREATE(transformer); +EXPORT_INIT(transformer); +EXPORT_PRECOMMIT(transformer); + +CLASS *transformer::oclass = NULL; +transformer *transformer::defaults = NULL; + +transformer::transformer(MODULE *module) +{ + if (oclass==NULL) + { + // register to receive notice for first top down. bottom up, and second top down synchronizations + oclass = gld_class::create(module,"transformer",sizeof(transformer),PC_AUTOLOCK|PC_OBSERVER); + if (oclass==NULL) + throw "unable to register class transformer"; + else + oclass->trl = TRL_PROVEN; + + defaults = this; + if (gl_publish_variable(oclass, + + PT_complex, "impedance[Ohm]", get_impedance_offset(), + PT_REQUIRED, + PT_DESCRIPTION, "transformer impedance (Ohm)", + + PT_enumeration, "status", get_status_offset(), + PT_DEFAULT, "IN", + PT_KEYWORD, "IN", (enumeration)TS_IN, + PT_KEYWORD, "OUT", (enumeration)TS_OUT, + PT_DESCRIPTION, "transformer status (IN or OUT)", + + NULL) < 1 ) + { + throw "unable to publish transformer properties"; + } + } +} + +int transformer::create(void) +{ + parent_is_branch = false; + return 1; // return 1 on success, 0 on failure +} + +int transformer::init(OBJECT *parent_hdr) +{ + powerline *parent = (powerline*)get_parent(); + if ( parent ) + { + if ( parent->isa("branch","pypower") ) + { + branch *parent = (branch*)get_parent(); + parent_is_branch = true; + int32 n_children = parent->get_child_count(); + if ( n_children > 0 ) + { + error("parent '%s' cannot accept more than one child component",get_parent()->get_name()); + return 0; + } + parent->set_child_count(n_children+1); + } + else if ( parent->isa("powerline","pypower") ) + { + if ( ( parent->get_impedance().Re() != 0 || parent->get_impedance().Im() != 0 ) + && ( parent->get_length() > 0 ) ) + { + error("parent '%s' non-zero impedance will be overwritten",get_parent()->get_name()); + return 0; + } + if ( parent->get_composition() == powerline::PLC_PARALLEL ) + { + error("parent '%s' must have series composition",get_parent()->get_name()); + return 0; + } + } + else + { + error("parent '%s' is not a pypower branch or powerline",get_parent()->get_name()); + return 0; + } + + } + else + { + warning("transformer without parent does nothing"); + } + + // check impedance + if ( impedance.Re() == 0 && impedance.Im() == 0 ) + { + error("transformer impedance must be positive"); + return 0; + } + + return 1; // return 1 on success, 0 on failure, 2 on retry later +} + +TIMESTAMP transformer::precommit(TIMESTAMP t0) +{ + if ( get_parent() != NULL ) + { + if ( parent_is_branch ) + { + branch *parent = (branch*)get_parent(); + + if ( get_status() == TS_IN ) + { + // copy status/impedance/admittance values to branch + parent->set_r(get_impedance().Re()); + parent->set_x(get_impedance().Im()); + parent->set_b(get_impedance().Inv().Im()); + parent->set_ratio(get_turns_ratio()); + parent->set_angle(get_phase_shift()); + parent->set_rateA(get_rating()); + parent->set_status(branch::BS_IN); + } + else + { + parent->set_status(branch::BS_OUT); + } + } + else + { + powerline *parent = (powerline*)get_parent(); + if ( get_status() == TS_IN ) + { + // add impedance + complex Z = parent->get_Z() + get_impedance(); + parent->set_Z(Z); + parent->set_Y(Z.Inv()); + parent->set_ratio(parent->get_ratio()*get_turns_ratio()); + parent->set_angle(parent->get_angle()+get_phase_shift()); + parent->set_rateA(min(parent->get_rateA(),get_rating())); + } + else + { + parent->set_status(powerline::PLS_IN); + } + } + } + return TS_NEVER; +} + diff --git a/module/pypower/transformer.h b/module/pypower/transformer.h new file mode 100644 index 000000000..85796afaa --- /dev/null +++ b/module/pypower/transformer.h @@ -0,0 +1,37 @@ +// module/pypower/transformer.h +// Copyright (C) 2024 Regents of the Leland Stanford Junior University + +#ifndef _PYPOWER_TRANSFORMER_H +#define _PYPOWER_TRANSFORMER_H + +#include "gridlabd.h" + +class transformer : public gld_object +{ + +public: + // published properties + GL_ATOMIC(double,turns_ratio); + GL_ATOMIC(double,phase_shift); + GL_ATOMIC(complex,impedance); + GL_ATOMIC(double,rating); + typedef enum {TS_OUT=0,TS_IN=1} TRANSFORMERSTATUS; + GL_ATOMIC(enumeration,status); + +public: + bool parent_is_branch; + +public: + // event handlers + transformer(MODULE *module); + int create(void); + int init(OBJECT *parent); + TIMESTAMP precommit(TIMESTAMP t1); + +public: + // internal properties + static CLASS *oclass; + static transformer *defaults; +}; + +#endif // _LOAD_H diff --git a/python/requirements.csv b/python/requirements.csv index 222fe06b7..73eac64b5 100644 --- a/python/requirements.csv +++ b/python/requirements.csv @@ -34,6 +34,7 @@ Pillow,2,,9.3.0 Pillow,,,9.3.0 PyGithub,2,,1.54.1 pymysql,,,1.0.2 +PYPOWER,,,5.1.16 pyproj,,,3.4.0 pysolar,,,0.9 pytz,2,, diff --git a/runtime/gridlabd.h b/runtime/gridlabd.h index bc1039519..2e33cb126 100644 --- a/runtime/gridlabd.h +++ b/runtime/gridlabd.h @@ -166,7 +166,11 @@ class complex { // return r; //}; #endif - + inline complex Inv(void) + { + double a = r*r + i*i; + return complex(r/a,-i/a); + }; inline complex operator - (void) /**< change sign */ { return complex(-r,-i,f); diff --git a/source/complex.h b/source/complex.h index 37ad2b652..fd4e55afb 100644 --- a/source/complex.h +++ b/source/complex.h @@ -248,6 +248,13 @@ inline DEPRECATED double complex_set_ang(C,D) {}; // TODO return f; }; + // Method: Inv + inline complex Inv(void) + { + double a = r*r + i*i; + return complex(r/a,-i/a); + }; + // Method: Mag inline double Mag(void) const /**< compute magnitude */ { diff --git a/source/convert.cpp b/source/convert.cpp index fc25caf60..02dbd0be2 100644 --- a/source/convert.cpp +++ b/source/convert.cpp @@ -337,7 +337,14 @@ int convert_to_complex(const char *buffer, /**< a pointer to the string buffer * v->SetRect(0.0, 0.0,v->Notation()); return 1; } - n = sscanf(buffer,"%lg%lg%1[ijdr]%s",&a,&b,notation,unit); + if ( buffer[0] == '(' ) // python notation + { + n = sscanf(buffer,"(%lg%lg%1[ijdr])",&a,&b,notation); + } + else + { + n = sscanf(buffer,"%lg%lg%1[ijdr]%s",&a,&b,notation,unit); + } if (n==1) { /* only real part */ diff --git a/source/gridlabd.h b/source/gridlabd.h index 35d057117..583a310e9 100644 --- a/source/gridlabd.h +++ b/source/gridlabd.h @@ -3487,6 +3487,13 @@ class gld_global { // Method: get_next inline GLOBALVAR* get_next(void) { if (!var) return NULL; else return var->next; }; + + bool operator ==(const char *str) { return strcmp((const char*)var->prop->addr,str)==0;}; + bool operator !=(const char *str) { return strcmp((const char*)var->prop->addr,str)!=0;}; + bool operator <(const char *str) { return strcmp((const char*)var->prop->addr,str)<0;}; + bool operator >(const char *str) { return strcmp((const char*)var->prop->addr,str)>0;}; + bool operator <=(const char *str) { return strcmp((const char*)var->prop->addr,str)<=0;}; + bool operator >=(const char *str) { return strcmp((const char*)var->prop->addr,str)>=0;}; }; /* Class: gld_aggregate diff --git a/source/gridlabd.in b/source/gridlabd.in index 086d7bb57..4f6c89765 100644 --- a/source/gridlabd.in +++ b/source/gridlabd.in @@ -387,7 +387,17 @@ fi if test -f "${GLD_ETC}/gridlabd.rc" then : - . ${GLD_ETC}/gridlabd.rc + . "${GLD_ETC}/gridlabd.rc" +fi + +if test -f "${HOME}/.gridlabd/gridlabd.rc" +then : + . "${HOME}/.gridlabd/gridlabd.rc" +fi + +if test -f "./gridlabd.rc" +then : + . "./gridlabd.rc" fi if test "x$1" = "xshell" @@ -397,7 +407,7 @@ fi if test -f "${GLD_ETC}/$1.py" then : - export PYTHONPATH=$GLD_ETC; python3 -m "$@" ; exit $? + export PYTHONPATH="$GLD_ETC"; python3 -m "$@" ; exit $? fi if test -x "${GLD_BIN}/gridlabd-$1" diff --git a/source/gridlabd.m4sh b/source/gridlabd.m4sh index d634b763c..4deabf512 100644 --- a/source/gridlabd.m4sh +++ b/source/gridlabd.m4sh @@ -119,7 +119,15 @@ elif test "x$1" = "xvalgrind" ; then : fi AS_IF([test -f "${GLD_ETC}/gridlabd.rc"], - [. ${GLD_ETC}/gridlabd.rc], + [. "${GLD_ETC}/gridlabd.rc"], + []) + +AS_IF([test -f "${HOME}/.gridlabd/gridlabd.rc"], + [. "${HOME}/.gridlabd/gridlabd.rc"], + []) + +AS_IF([test -f "./gridlabd.rc"], + [. "./gridlabd.rc"], []) AS_IF([test "x$1" = "xshell"], @@ -127,7 +135,7 @@ AS_IF([test "x$1" = "xshell"], []) AS_IF([test -f "${GLD_ETC}/$1.py"], - [export PYTHONPATH=$GLD_ETC; python3 -m "$@" ; exit $?], + [export PYTHONPATH="$GLD_ETC"; python3 -m "$@" ; exit $?], []) AS_IF([test -x "${GLD_BIN}/gridlabd-$1"], diff --git a/source/version.h b/source/version.h index ea676946d..91f248c57 100644 --- a/source/version.h +++ b/source/version.h @@ -11,7 +11,7 @@ #define REV_MAJOR 4 #define REV_MINOR 3 -#define REV_PATCH 6 +#define REV_PATCH 7 #ifdef HAVE_CONFIG_H #include "config.h" diff --git a/tools/Makefile.mk b/tools/Makefile.mk index ce50f2058..534c7b891 100644 --- a/tools/Makefile.mk +++ b/tools/Makefile.mk @@ -1,6 +1,7 @@ dist_pkgdata_DATA += tools/create_childs.py dist_pkgdata_DATA += tools/create_ductbank.py dist_pkgdata_DATA += tools/create_filter.py +dist_pkgdata_DATA += tools/create_metered_loads.py dist_pkgdata_DATA += tools/create_meters.py dist_pkgdata_DATA += tools/create_player.py dist_pkgdata_DATA += tools/create_poles.py diff --git a/tools/create_metered_loads.py b/tools/create_metered_loads.py new file mode 100644 index 000000000..01c01d868 --- /dev/null +++ b/tools/create_metered_loads.py @@ -0,0 +1,236 @@ +# Syntax: create_metered_loads [-i|--input=INPUTFILE] [-o|--output=GLMFILE] [OPTIONS ...] +"""Syntax: create_metered_loads [-i|--input=INPUTFILE] [-o|--output=GLMFILE] [OPTIONS ...] + +Options +------- + -P|--parents=NAME:VALUE,... specify parent property pattern to match (required) + -C|--childs=NAME:VALUE,... specify child property list to assign (required) + -N|--names=STRING specify object naming convention (default is '{class}_{name}') + -M|--modules=NAME,... specify module names to use (defaults to those found) + -L|--link=NAME:VALUE,.... specify link types (required) + +Description +----------- + +The `create_metered_loads` tool adds loads with meter objects to all objects that match the +parent object pattern specified. + +Parent patterns and child properties as specified as a comma-separate list of +`NAME:VALUE` strings, e.g., `class:node` or `nominal_voltage:2.4kV`. Parent +patterns use `regex` pattern matching. Child properties may include `{NAME}` +format strings where `NAME` is a property of the parent object. This +allows copying of values from the parent object. This formatting also can be +applied to the naming string, e.g., `-N='{name}_L' to append '_L' to the +parent object name. + +Example +------- + +The following creates a GLM file containing a `triplex_load` objects attached +to `triplex_node` objects with names starting as `N_` in the file `my-network.json`: + +~~~ +$ gridlabd create_metered_loads -i=my-network.json -o=loads.glm -P='class:triplex_node,name:^N_' -C='class:triplex_load,nominal_voltage:{nominal_voltage},phases:{phases},constant_power_B:1.2+0.1jkVA' +~~~ + + +The following creates a GLM file containing a `triplex_load` objects attached +to `triplex_node` objects with names starting as `N_` in the file `my-network.json` without specifying the power phase. The power is captured from the upstream node/triplex_node: + +~~~ +$ gridlabd create_metered_loads -i=my-network.json -o=loads.glm -P='class:triplex_node,name:^N_' -C='class:triplex_load,nominal_voltage:{nominal_voltage},phases:{phases},constant_power_B:1.2+0.1jkVA' +~~~ + +""" + +import sys, os +import json +import re +import datetime +import subprocess +import random + +EXENAME = os.path.splitext(os.path.basename(sys.argv[0]))[0] + +DEBUG = False +WARNING = True +QUIET = False +VERBOSE = False + +E_OK = 0 +E_INVALID = 1 +E_FAILED = 2 +E_SYNTAX = 8 +E_EXCEPTION = 9 +EXITCODE = E_OK + +class GldException(Exception): + pass + +def error(msg,code=None): + if type(code) is int: + global EXITCODE + EXITCODE = code + if DEBUG: + raise GldException(msg) + print("ERROR [create_metered_loads]:",msg,file=sys.stderr) + exit(code) + + +def load(): + + if not INPUTFILE.endswith(".json"): + tmpfile = "." + while os.path.exists(tmpfile): + tmpfile = f"tmp{hex(random.randint(1e30,1e31))[2:]}.json" + try: + result = subprocess.run(["gridlabd","-C",INPUTFILE,"-o",tmpfile]) + assert(result.returncode==0) + with open(tmpfile,"r") as fh: + model = json.load(fh) + except: + raise + finally: + os.remove(tmpfile) + pass + else: + with open(INPUTFILE,"r") as fh: + model = json.load(fh) + return model + +def save(fh): + print(f"// generated by '{' '.join(sys.argv)}' at {datetime.datetime.now()}",file=fh) + for name in MODULES: + print(f"module {name};",file=fh) + classname = CHILDS["class"] + link_class = LINK["class"] + if classname == "load" : + meter_class = "meter" + elif classname == "triplex_load" : + meter_class = "triplex_meter" + for obj,data in OBJECTS.items(): + if "meter" in obj : + print(f"object {meter_class} {{",file=fh) + elif "link" in obj : + print(f"object {link_class} {{",file=fh) + else : + print(f"object {classname} {{",file=fh) + for prop,value in data.items(): + print(f" {prop} \"{value}\";",file=fh) + print("}",file=fh) + +def main(): + + PATTERN = {} + for name,pattern in PARENTS.items(): + PATTERN[name] = re.compile(pattern) + + if "class" not in CHILDS: + error("you must include a class name in the child properties",E_INVALID) + classname = CHILDS["class"] + model = load() + assert(model['application']=='gridlabd') + global MODULES + if not MODULES: + MODULES = list(model['modules']) + + for obj,data in model['objects'].items(): + data['name'] = obj + ok = True + for name,pattern in PATTERN.items(): + if not pattern.match(data[name]): + ok = False + break + if ok: + name = f"{classname}_{obj}" if NAMING is None else NAMING.format(**data) + name_meter = f"meter_{obj}" + name_link = f"link_{obj}" + OBJECTS[name_meter] = dict(name=name_meter) + OBJECTS[name] = dict(parent=name_meter,name=name) + OBJECTS[name_link] = dict(name=name_link, to=name_meter) + OBJECTS[name_link]["from"] = obj + + for prop,value in CHILDS.items(): + if prop in ["phases"]: + load_phase = ''.join([x for x in 'ABC' if x in value.format(**data)]) + + for prop,value in CHILDS.items(): + if not prop in ["class"] and "constant_" not in prop: + OBJECTS[name][prop] = value.format(**data) + if prop in ["phases"]: + OBJECTS[name_meter][prop] = value.format(**data) + OBJECTS[name_link][prop] = value.format(**data) + if prop in ["nominal_voltage"]: + OBJECTS[name_meter][prop] = value.format(**data) + if "constant_" in prop: + if "_A" in prop or "_B" in prop or "_C" in prop: + OBJECTS[name][prop] = value.format(**data) + + else : + for i in load_phase : + OBJECTS[name][prop+'_'+i] = value.format(**data) + + for prop,value in LINK.items(): + if not prop in ["class"]: + OBJECTS[name_link][prop] = value.format(**data) + + + + if OUTPUTFILE.endswith(".glm"): + with open(OUTPUTFILE,"w") as fh: + save(fh) + else: + error("invalid output file format") + + return E_OK + +INPUTFILE = "/dev/stdin" +OUTPUTFILE = "/dev/stdout" +PARENTS = None +CHILDS = None +NAMING = None +OBJECTS = {} +MODULES = [] + +if __name__ == "__main__": + + if len(sys.argv) == 1: + print(__doc__.split('\n')[0],file=sys.stderr) + exit(E_SYNTAX) + + for arg in sys.argv[1:]: + spec = arg.split("=") + if len(spec) == 1: + tag = arg + value = None + else: + tag = spec[0] + value = '='.join(spec[1:]) + + if tag in ["-h","--help","help"]: + print(__doc__) + exit(E_OK) + if tag in ["-i","--input"]: + INPUTFILE = value if value else "/dev/stdin" + elif tag in ["-o","--output"]: + OUTPUTFILE = value if value else "/dev/stdout" + elif tag in ["-P","--parent"]: + PARENTS = dict([x.split(":") for x in value.split(",")]) + elif tag in ["-C","--childs"]: + CHILDS = dict([x.split(":") for x in value.split(",")]) + elif tag in ["-L","--link"]: + LINK = dict([x.split(":") for x in value.split(",")]) + elif tag in ["-N","--names"]: + NAMING = value + elif tag in ["-M","--modules"]: + MODULES = value.split(",") + else: + error(f"option '{arg}' is invalid",E_INVALID) + + if PARENTS is None: + error("you must specify the parent patterns to match") + if CHILDS is None: + error("you must specify the child properties to define") + + EXITCODE = main() + exit(EXITCODE)