1. Simple Electrolyser Load Operation#

This example demonstrates a very simple application of the NEMGLO package; extracting historical NEM price data from AEMO (via NEMOSIS), defining load characteristics, then running the optimiser to find the operational behaviour of the load.

Install Packages#

For standard use of NEMGLO we can use from nemglo import * to import nemglo functionality. This example also uses plotly to generate charts, with the optional setting defining where to render charts.

# NEMGLO Packages
from nemglo import *

# Generic Packages
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Display plotly chart in a browser (optional)
import plotly.io as pio
pio.renderers.default = "browser"

Load Historical AEMO data#

Create a nemosis_data object to retrieve historical data for the simulation. nemosis_data class requires a defined interval length and cache folder.

inputdata = nemosis_data(intlength=30, local_cache=r'E:\TEMPCACHE')

Now we define the simulation period by start and end dispatch intervals, as well as the region for which we are modelling the load in. These parameters are set by using functions set_ of the nemosis_data class.

start = "02/01/2020 00:00"
end = "09/01/2020 00:00"
region = 'VIC1'
inputdata.set_dates(start, end)
inputdata.set_region(region)

Price data can now be loaded from the data object by using the get_prices function. Notice the data has been aggregated to 30-minute intervals as initially defined (the original downloaded AEMO data is in 5-minute dispatch interval resolution)

prices = inputdata.get_prices()
prices
Compiling data for table DISPATCHPRICE.
Returning DISPATCHPRICE.
Time Prices
0 2020-01-02 00:30:00 67.094337
1 2020-01-02 01:00:00 65.526907
2 2020-01-02 01:30:00 50.028502
3 2020-01-02 02:00:00 40.664717
4 2020-01-02 02:30:00 43.609028
... ... ...
331 2020-01-08 22:00:00 44.572158
332 2020-01-08 22:30:00 51.387228
333 2020-01-08 23:00:00 44.822428
334 2020-01-08 23:30:00 44.136238
335 2020-01-09 00:00:00 49.485932

336 rows × 2 columns

Create a planner object for this modelling session#

For every use case of NEMGLO, a nemglo.planner.Plan object is required. This must be created with a unique identifier, in this case called “H2_VIC”. The first step is then to load market prices for the simulation period for which the load will be optimised for. The function load_market_prices stores these required values.

Tip

Simplify your model by naming the python variable and the identifier parameter of the Plan class as the same.

H2_VIC = Plan(identifier = "H2_VIC")
H2_VIC.load_market_prices(prices)

This data is now stored as attributes in the Plan class. You can check these values, which are now lists, if you wish by _timeseries and _prices. For example, the first interval is:

H2_VIC._timeseries[0]
Timestamp('2020-01-02 00:30:00')
H2_VIC._prices[0]
67.09433666666666

Create an Electrolyser object + defining operating characteristics#

A load must be defined and linked to the Plan object in order to model it’s behaviour. For creating components in NEMGLO, they must be defined as belonging to a Plan class. This is done by parsing the variable of the Plan object which we created in this python session, here H@_VIC (conveniently the same as the identifier name). Similarly to Plan, all components must have a unique identifer which has been called H2E here for the Electrolyser.

h2e = Electrolyser(H2_VIC, identifier='H2E')

There are numerous parameters we can now defined for the Electrolyser using load_h2_parameters_preset. At a minimum the following must be specified of the electrolyser: capacity: rated capacity [MW], maxload: maximum load [MW], minload: minimum stable loading (MSL) [MW], offload: off state [MW], electrolyser_type: either ‘PEM’ or ‘AE’, sec_profile: specific energy consumption as either ‘fixed’ or ‘variable’.

For this scenario we will also define h2_price_kg: the production benefit price of hydrogen [$/kg]. Although this is optional, if a price incentive is not defined, a production target must be set for the amount of hydrogen produced, otherwise the electrolyser will do nothing.

h2e.load_h2_parameters_preset(capacity = 100.0,
                              maxload = 100.0,
                              minload = 10.0,
                              offload = 0.0,
                              electrolyser_type = 'PEM',
                              sec_profile = 'fixed',
                              h2_price_kg = 6.0)

The parameters are now stored in our Electrolyser object. We can check this by:

h2e.__dict__
{'_system_plan': <nemglo.planner.Plan at 0x21f882f6f10>,
 '_id': 'H2E',
 '_capacity': 100.0,
 '_maxload': 100.0,
 '_minload': 10.0,
 '_offload': 0.0,
 '_type': 'PEM',
 '_profile': 'fixed',
 '_sec_nominal': 66,
 '_sec_conversion': 1.0,
 '_sec_system': 66.0,
 '_sec_variable_points':    h2e_load_pct  nominal_sec_pct
 0           0.0           0.8383
 1           0.2           0.8383
 2           0.4           0.8598
 3           0.6           0.8982
 4           0.8           0.9315
 5           1.0           1.0101,
 '_h2_price_kg': 6.0,
 '_storage_max': None,
 '_storage_initial': None,
 '_storage_final': None,
 '_storage_drain': None}

To create necessary variables and constraints in the optimisation model we call the function add_electrolyser_operation. The specific variables and constraints created will depend on the input parameters as defined by the previous function.

h2e.add_electrolyser_operation()

We can check the variables created using the view_variable function of the Plan object, and parse the name (or part of the name) for the variables we want to check. For example, specifying the variable “H2E-mw_load_sum” yields the following.

Alternatively, if we parse the identifier of the component Electrolyser which is “H2E” we will see all optimisation variables belonging to that component.

H2_VIC.view_variable('H2E-mw_load_sum')
variable_name variable_id interval lower_bound upper_bound type
0 H2E-mw_load_sum 336 None 0.0 inf continuous
H2_VIC.view_variable('H2E')
variable_name variable_id interval lower_bound upper_bound type
0 H2E-mw_load 0 0 0.0 100.0 continuous
1 H2E-mw_load 1 1 0.0 100.0 continuous
2 H2E-mw_load 2 2 0.0 100.0 continuous
3 H2E-mw_load 3 3 0.0 100.0 continuous
4 H2E-mw_load 4 4 0.0 100.0 continuous
... ... ... ... ... ... ...
1677 H2E-msl_relieve 1677 331 0.0 1.0 binary
1678 H2E-msl_relieve 1678 332 0.0 1.0 binary
1679 H2E-msl_relieve 1679 333 0.0 1.0 binary
1680 H2E-msl_relieve 1680 334 0.0 1.0 binary
1681 H2E-msl_relieve 1681 335 0.0 1.0 binary

1682 rows × 6 columns

Running the optimisation#

Since we have loaded a price trace into the model and added operating characteristics of the Electrolyser, we can now run the optimisation model via the Plan object function optimise. By default this is set to use the CBC solver. If you wish to save the optimisation results you can set save_results as True, likewise for debug files which are the low-level variable and constraints tables which formulate the optimisation problem, set save_debug as True. If you define these you can also specific the root filepath where you wish to save the results as results_dir. If this is left unchanged, a new results folder will be created in your current working directory.

H2_VIC.optimise(solver_name="CBC", save_debug=False, save_results=False, results_dir=None)

View planner results#

The results can also be extracted within python through a number of functions.

  • The timeseries data for the load dispatch each interval is found with get_load.

  • The total energy consumption of the load in MW is found with get_total_consumption.

  • The load energy capacity factor is found with get_load_capacity_factor.

result_load = H2_VIC.get_load()
result_load
H2_VIC.get_total_consumption()
H2_VIC.get_load_capacity_factor()

Plotting results#

Constructing a chart in plotly of result_load and price produces the below.

# A visual representation of the results can be displayed using plotly to
# compare the load result and input market price traces.
fig = make_subplots(specs=[[{"secondary_y":True}]])
fig.update_layout(title='NEMGLO Dispatch & Pricing Example 1<br><sup>VIC: Jan-2020</sup>', titlefont=dict(size=24),
                  xaxis=dict(title="Time", showgrid=False, mirror=True, titlefont=dict(size=24), \
                    tickfont=dict(size=24), tickangle=-45, tickformat="%d-%b"),
                  yaxis=dict(title="Dispatch (MW)", showgrid=False, range=[-10,140], mirror=True, titlefont=dict(size=24),\
                    tickfont=dict(size=24)),
                  legend=dict(xanchor='center',x=0.5, y=-0.35, orientation='h', font=dict(size=20)),
                  template="simple_white",
                  font_family="Times New Roman",
                  xaxis_showgrid=True,
                  yaxis_showgrid=True,
                  #width=800,
                  height=600)
fig.update_yaxes(title="Price ($/MWh)", showgrid=False, gridcolor='slategrey', range=[-250,150], mirror=True, titlefont=dict(size=24),\
                    tickfont=dict(size=24), secondary_y=True)
fig.add_trace(go.Scatter(x=prices['Time'], y=prices['Prices'],name="Price ($/MWh)", \
    line={'color':'#972f42'}),secondary_y=True)
fig.add_trace(go.Scatter(x=result_load['time'], y=result_load['value'],name='Load (MW)', \
    line={'color':'#7F22A6'}),secondary_y=False)


fig.show()