Skip to content

Commit

Permalink
Enhance pypower module (#198)
Browse files Browse the repository at this point in the history
Signed-off-by: David P. Chassin <dchassin@slac.stanford.edu>
  • Loading branch information
dchassin committed Apr 20, 2024
1 parent 313468b commit 0ac4612
Show file tree
Hide file tree
Showing 16 changed files with 2,193 additions and 69 deletions.
71 changes: 58 additions & 13 deletions docs/Module/Pypower.md
Expand Up @@ -5,19 +5,28 @@
GLM:

~~~
module pypower
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
set {QUIET=65536, WARNING=131072, DEBUG=262144, VERBOSE=524288} message_flags; // module message control flags
char256 timestamp_format; // Format for weather file timestamps ('' is RFC822/ISO8601)
int32 version; // Version of pypower used
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
double baseMVA[MVA]; // Base MVA value
bool enable_opf; // Flag to enable optimal powerflow (OPF) solver
bool stop_on_failure; // Flag to stop simulation on solver failure
bool save_case; // Flag to save pypower case data and results
char1024 controllers_path; // Path to find module containing controller functions
char1024 controllers; // Python module containing controller functions
double solver_update_resolution; // Minimum difference before a value is considered changed
int32 maximum_iterations; // Maximum iterations (0 defaults to pypower default for solver_method)
double solution_tolerance; // Solver convergence error tolerante (0 defaults to pypower default)
enumeration {FAILED=2, SUCCESS=1, INIT=0} solver_status; // Result of the last pypower solver run
bool enforce_q_limits; // Enable enforcement of reactive power limits
bool use_dc_powerflow; // Enable use of DC powerflow solution
enumeration {PY=2, JSON=1, CSV=0} save_format; // Save case format
double total_loss[MW]; // System-wide line losses
double generation_shortfall[MW]; // System-wide generation shortfall
}
~~~

Expand Down Expand Up @@ -206,9 +215,24 @@ 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.

In addition, the following event handlers are supported:

* `on_precommit(dict:data) --> int`: Called when the clock advances before
the main solver is called.

* `on_sync(dict:data) --> int`: Called each time the main solver is called.

* `on_commit(dict:data) --> int`: Called after the last time the main solver
is called.

* `on_term() --> None`: Called when the simulation is done.

Any `load`, `powerplant`, and `relay` 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.
function will be called if it is defined in the `controllers` module. The
return value is a unix timestamp in seconds of epoch indicating the time of
the next event, if any. Otherwise, it should return `gridlabd.NEVER` or
`gridlabd.INVALID` to indicate an error.

Controller functions use the following call/return prototype

Expand All @@ -222,6 +246,13 @@ 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.

When the controller module is loaded, the built-in `gridlabd` module is added
to the globals to provide access to the main instance of `gridlabd` and its
support methods. You do not need to import `gridlabd`.

For more information on the `gridlabd` main module, see [[/Module/Python]] and
[[/Module/Python/Property]].

## SCADA

The `scada` object is used to access properties of objects in the
Expand All @@ -232,6 +263,17 @@ will copy back values that have changed. If the `record` property is `TRUE`,
the `controllers` module will have a global `historian` which records are
past values of the `scada` global.

## Geodata

The `geodata` object is used to map a panel of location-based data to
variables across a number of objects or classes based on the objects'
locations. Each `geodata` CSV file corresponds to a single object property,
and is organized a locations in columns and time-series in rows.

## Weather

The `weather` object is used to read weather data from a weather file.

# See also

* [PyPower documentation](https://pypi.org/project/PYPOWER/)
Expand All @@ -241,6 +283,9 @@ past values of the `scada` global.
* [[/Module/Pypower/Powerplant]]
* [[/Module/Pypower/Relay]]
* [[/Module/Pypower/Scada]]
* [[/Module/Pypower/Weather]]
* [[/Module/Pypower/Transformer]]
* [[/Module/Python]]
* [[/Module/Python/Property]]
* [[/Converters/Import/Pypower_cases]]
* [[/Converters/Import/Psse_models]]
11 changes: 11 additions & 0 deletions docs/Module/Pypower/Powerplant.md
Expand Up @@ -40,6 +40,17 @@ are applied to the object, and the next update is schedule at the time `t`
returned by the controller. Note that unlike the `load` object, `powerplant`
objects do force iteration if no value of `t` is returned.

Note the following:

1. `gen` objects are dispatchable -- the values of `Pg` and `Qg` may be
updated after the powerflow solution is updated. As a result, only
dispatchable resources should use a `gen` parent object.

2. `bus` objects are non-dispatchable -- the values of `Pd` and `Qd`
will not be changed following the powerflow solution. As a result, only
non-dispatchable resources should use a `bus` parent object. The applies
particularly to wind, solar, and batteries.

# Example

The following example implements a 10 kW generator turn turns on only in the
Expand Down
47 changes: 47 additions & 0 deletions docs/Module/Pypower/Weather.md
@@ -0,0 +1,47 @@
[[/Module/Pypower/Weather]] -- PyPower weather object

# Synopsis

~~~
class weather {
char1024 file; // Source object for weather data
char1024 variables; // Weather variable column names (col1,col2,...)
double resolution[s]; // Weather time downsampling resolution (s)
double Sn[W/m^2]; // Solar direct normal irradiance (W/m^2)
double Sh[W/m^2]; // Solar horizontal irradiance (W/m^2)
double Sg[W/m^2]; // Solar global irradiance (W/m^2)
double Wd[deg]; // Wind direction (deg)
double Ws[m/2]; // Wind speed (m/2)
double Td[degC]; // Dry-bulb air temperature (degC)
double Tw[degC]; // Wet-bulb air temperature (degC)
double RH[%]; // Relative humidity (%)
double PW[in]; // Precipitable_water (in)
double HI[degF]; // Heat index (degF)
}
~~~

# Description

The `weather` object reads data from a weather CSV file. The columns
in the CSV file are mapped according to the `variables` property.

The `bus` object includes the `weather` file properties, which allows
individual busses to read weather data directly.

# Example

The following example reads weather data in bus 1 of the IEEE 14 bus model.

~~~
module pypower
{
solver_method NR;
}
#input "case14.py" -t pypower
modify pp_bus_1.weather_file ${DIR:-.}/case14_bus1_weather.csv;
modify pp_bus_1.weather_variables Sh,Sn,Sg,Wd,Ws,Td,Tw,RH,PW;
~~~

# See also

- [[/Module/Pypower]]
25 changes: 17 additions & 8 deletions module/pypower/autotest/controllers.py
Expand Up @@ -12,26 +12,35 @@ def on_sync(data):
def load_control(obj,**kwargs):
# print(f"load_control({obj})",kwargs,file=sys.stderr)
if kwargs['t']%3600 < 1800 and kwargs['P'] != 0: # turn off load in first half-hour
return dict(P=0)
return dict(t=(int(kwargs['t']/1800)+1)*1800,P=0)
elif kwargs['t']%3600 >= 1800 and kwargs['P'] == 0: # turn on load in second half-hour
return dict(P=10)
return dict(t=(int(kwargs['t']/1800)+1)*1800,P=10)
else: # no change -- advance to next 1/2 hour when a change is anticipated
return dict(t=(int(kwargs['t']/1800)+1)*1800)

def powerplant_control(obj,**kwargs):
# print(f"powerplant_control({obj})",kwargs,file=sys.stderr)
if kwargs['t']%3600 < 1800 and kwargs['S'].real != 0: # turn off plant in first half-hour
return dict(S=(0j))
elif kwargs['t']%3600 >= 1800 and kwargs['S'].real == 0: # turn on plant in second half-hour
return dict(S=(10+0j))
if kwargs['t']%3600 < 1800: # turn off plant in first half-hour
return dict(t=(int(kwargs['t']/1800)+1)*1800,S=(0j))
elif kwargs['t']%3600 >= 1800: # turn on plant in second half-hour
return dict(t=(int(kwargs['t']/1800)+1)*1800,S=(10+0j))
else: # no change -- advance to next 1/2 hour when a change is anticipated
return dict(t=(int(kwargs['t']/1800)+1)*1800)

def storage_control(obj,**kwargs):
# print(f"powerplant_control({obj})",kwargs,file=sys.stderr)
if kwargs['t']%3600 < 1800: # discharge in first half-hour
return dict(t=(int(kwargs['t']/1800)+1)*1800,S=(-0.001+0j))
elif kwargs['t']%3600 >= 1800: # charge in second half-hour
return dict(t=(int(kwargs['t']/1800)+1)*1800,S=(0.01+0j))
else: # no change -- advance to next 1/2 hour when a change is anticipated
return dict(t=(int(kwargs['t']/1800)+1)*1800)

def relay_control(obj,**kwargs):
# print(f"relay_control({obj})",kwargs,file=sys.stderr)
if kwargs['t']%3600 < 1800 and kwargs['status'] != 0: # open the relay in first half-hour
return dict(status=0)
return dict(t=(int(kwargs['t']/1800)+1)*1800,status=0)
elif kwargs['t']%3600 >= 1800 and kwargs['status'] == 0: # close the relay in second half-hour
return dict(status=1)
return dict(t=(int(kwargs['t']/1800)+1)*1800,status=1)
else: # no change -- advance to next 1/2 hour when a change is anticipated
return dict(t=(int(kwargs['t']/1800)+1)*1800)
35 changes: 35 additions & 0 deletions module/pypower/autotest/test_case14_storage.glm
@@ -0,0 +1,35 @@
#define CASE=14
#ifexists "../case.glm"
#define DIR=..
#endif

//#option debug

module pypower
{
#ifdef DIR
controllers_path "${DIR}";
#endif
controllers "controllers";
}

module tape
{
csv_header_type NAME;
}
object pypower.powerplant
{
parent pp_bus_2;
status ONLINE;
controller "storage_control";
charging_capacity 15 kW;
storage_capacity 10 MWh;
storage_efficiency 0.95;
object recorder
{
file "${modelname/.glm/_record.csv}";
property S,state_of_charge;
};
}

#include "${DIR:-.}/case.glm"

0 comments on commit 0ac4612

Please sign in to comment.