2. Renewable PPAs#

This example demonstrates a the renewable Generator class of NEMGLO and the PPA features within. It shows how to extract historical AEMO data of the NEM, define load characteristics (as per Example 1) and further PPA structures, then running the optimiser to find the operational load behaviour.

This example uses plotly == 5.6.0 to plot results. Install with… pip install plotly==5.6.0

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. Additionally we specify here one or two generators we wish to extract dispatch traces for.

start = "02/01/2020 00:00"
end = "09/01/2020 00:00"
region = 'VIC1'
duid_1 = 'NUMURSF1'
duid_2 = 'ARWF1'

Here we can optionally check the AEMO defined information about these generators, namely their registered capacity (MW). Although in the load optimisation we can set any desired sizings for these plants.

inputdata._download_geninfo()
inputdata._info[(inputdata._info['DUID'].isin([duid_1, duid_2]))]
Retrieving static table Generators and Scheduled Loads.
Station Name Region Fuel Source - Descriptor DUID Reg Cap (MW)
11 Ararat Wind Farm VIC1 Wind ARWF1 241.59
364 Numurkah Solar Farm VIC1 Solar NUMURSF1 112.00

The defined parameters are set by using functions set_ of the nemosis_data class.

inputdata.set_dates(start, end)
inputdata.set_region(region)
inputdata.set_unit(duid_1, duid_2)

Price data can now be loaded as per Example 1. Additionally we now have VRE generator trace data, which is the historical dispatch data (MW) scaled by the noted AEMO registered capacities (MW) to get a percentage capacity factor trace as below.

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

vre = inputdata.get_vre_traces()
vre
Compiling data for table DISPATCHLOAD.
Returning DISPATCHLOAD.
Compiling data for table DISPATCH_UNIT_SCADA.
Returning DISPATCH_UNIT_SCADA.
ERROR DISCREPENCY between SCADAVALUE and INITIALMW. Assuming INITIALMW
Retrieving static table Generators and Scheduled Loads.
Time ARWF1 NUMURSF1
0 2020-01-02 00:30:00 0.424686 0.0
1 2020-01-02 01:00:00 0.310995 0.0
2 2020-01-02 01:30:00 0.264429 0.0
3 2020-01-02 02:00:00 0.234626 0.0
4 2020-01-02 02:30:00 0.198132 0.0
... ... ... ...
331 2020-01-08 22:00:00 0.265119 0.0
332 2020-01-08 22:30:00 0.299888 0.0
333 2020-01-08 23:00:00 0.307753 0.0
334 2020-01-08 23:30:00 0.348731 0.0
335 2020-01-09 00:00:00 0.329484 0.0

336 rows × 3 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 “P2G”. 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.

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

Create an Electrolyser object + define its 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. Similarly to Plan, all components must have a unique identifer which has been called H2E here for the Electrolyser. Following this we defined the operating characteristics as per Example 1, and add the electrolyser operation to the model.

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

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 = 5.0)

h2e.add_electrolyser_operation()

Create an Generator object + define the RE trace data & PPA structure#

The Generator component is created just like the Electrolyser, parsing the Plan object and using a unique identifier for this Generator.

solar = Generator(P2G, identifier="SF")

To define the parameters of our new Generator SF, we call the load_vre_parameters function specifying the ‘duid’, desired ‘capacity’, ‘trace’ using the vre dataframe we extracted from the nemosis_data module earlier, ‘ppa_strike’ price and optionally a ‘ppa_floor’ price. Note we only parse the ‘Time’ and duid columns of the vre dataframe, we do not want to use the other generator data here for the wind farm!

solar.load_vre_parameters(duid = 'NUMURSF1',
                          capacity = 100.0,
                          trace = vre[['Time', 'NUMURSF1']],
                          ppa_strike = 30.0,
                          ppa_floor=None)

Finally, we can implement this component in the model by using add_ppa_contract

solar.add_ppa_contract()

Running the optimisation#

As per Example 1…

P2G.optimise()
OPTIMISATION COMPLETE, Obj Value: -610452.4214854483
<mip.model.Model at 0x1e583630a00>

View planner results#

The results can also be extracted within python through a number of functions. Specific to the VRE Generator component, we find:

  • The timeseries data for the vre availability in MW with get_vre_availability. Note this is simply the VRE input trace scaled by the defined load_vre_parameters capacity value.

result_solar = P2G.get_vre_availability(identifier="SF")
print(result_solar.head())
print("The maximum capacity factor was: {:.1f} %".format(result_solar['value'].max()))
print("The average capacity factor was: {:.1f} %".format(result_solar['value'].mean()))
  interval  value                time
0        0    0.0 2020-01-02 00:30:00
1        1    0.0 2020-01-02 01:00:00
2        2    0.0 2020-01-02 01:30:00
3        3    0.0 2020-01-02 02:00:00
4        4    0.0 2020-01-02 02:30:00
The maximum capacity factor was: 89.3 %
The average capacity factor was: 29.7 %
  • A breakdown of cost components and the total cost per dispatch interval using the get_costs function of the Plan.

Tip

The column names returned in get_costs are standardized across the pacakge based on the identifiers given for each components and default variable names which can be found in the Naming Convention

result_costs = P2G.get_costs()
result_costs
time interval total_cost H2E-h2_produced H2E-mw_load SF-ppa_cfd
0 2020-01-02 00:30:00 0 -433.161955 -3787.878788 3354.716833 0.0
1 2020-01-02 01:00:00 1 -511.533455 -3787.878788 3276.345333 0.0
2 2020-01-02 01:30:00 2 -1286.453705 -3787.878788 2501.425083 0.0
3 2020-01-02 02:00:00 3 -1754.642955 -3787.878788 2033.235833 0.0
4 2020-01-02 02:30:00 4 -1607.427371 -3787.878788 2180.451417 0.0
... ... ... ... ... ... ...
331 2020-01-08 22:00:00 331 -1559.270871 -3787.878788 2228.607917 0.0
332 2020-01-08 22:30:00 332 -1218.517371 -3787.878788 2569.361417 0.0
333 2020-01-08 23:00:00 333 -1546.757371 -3787.878788 2241.121417 0.0
334 2020-01-08 23:30:00 334 -1581.066871 -3787.878788 2206.811917 0.0
335 2020-01-09 00:00:00 335 -1313.582205 -3787.878788 2474.296583 0.0

336 rows × 6 columns

Again, calling the earlier results from Example 1 so we can plot a combination of output results.

result_load = P2G.get_load()

Plotting results#

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

fig = go.Figure()
fig.update_layout(title='<b>NEMGLO Electrolyser with PPA Example 2 - Operation Timeseries<br><sup>VIC: Jan-2020</sup></b>', titlefont=dict(size=24),
                  margin=dict(l=20, r=20, t=60, b=0),
                  xaxis=dict(title="Time", showgrid=False, mirror=True, titlefont=dict(size=18), \
                    tickfont=dict(size=18), tickangle=-45, tickformat="%d-%b",  domain=[0, 1]),
                  yaxis=dict(title="Generator / Load Dispatch (MW)", showgrid=False, range=[-10,140], mirror=True, titlefont=dict(size=18),\
                    tickfont=dict(size=18), tickvals=[i for i in range(-20, 140, 20)]),
                  yaxis2=dict(title="Price ($/MWh)", showgrid=False, gridcolor='slategrey', range=[-250,150], mirror=True, \
                    titlefont=dict(size=18),tickfont=dict(size=18), anchor="x", overlaying="y", side="right", color="FireBrick"),
                  legend=dict(xanchor='center',x=0.55, y=-0.25, orientation='h', font=dict(size=18)),
                  template="simple_white",
                  font_family="Times New Roman",
                  xaxis_showgrid=True,
                  yaxis_showgrid=True,
                  width=1000,
                  height=600)
fmt_timestamps = [dt.strftime(prices['Time'][i], "%d-%b %H:%M") for i in range(len(prices))]
fig.add_trace(go.Scatter(x=prices['Time'], y=prices['Prices'],name="Price ($/MWh)", \
    line={'color':'FireBrick'}, yaxis="y2"))
fig.add_trace(go.Scatter(x=result_load['time'], y=result_load['value'],name='Load (MW)', \
    line={'color':'Purple'}))
fig.add_trace(go.Scatter(x=result_solar['time'], y=result_solar['value'], name=f'{duid_1} (MW)', \
    line={'color':'darkorange'}, yaxis="y"))

for ser in fig['data']:
  ser['text'] = [dt.strftime(prices['Time'][i], "%d-%b %H:%M") for i in range(len(prices))]
  ser['hovertemplate'] = 'Time: %{text}<br>Value: %{y}'

fig.show()
time = result_costs['time']
energy = result_costs['H2E-mw_load'].mul(-1)
h2 = result_costs['H2E-h2_produced'].mul(-1)
ppa = result_costs['SF-ppa_cfd'].mul(-1)
total = result_costs['total_cost'].mul(-1)

fig = go.Figure()
fig.update_layout(title='<b>NEMGLO Electrolyser with PPA Example 2 - Revenue Timeseries<br><sup>VIC: Jan-2020</sup></b>', titlefont=dict(size=24),
                  margin=dict(l=20, r=20, t=60, b=0),
                  xaxis=dict(title="Time", showgrid=True, mirror=True, titlefont=dict(size=18), \
                    tickfont=dict(size=18), tickangle=-45, tickformat="%d-%b",  domain=[0, 1]),
                  yaxis=dict(title="Revenue ($)", showgrid=True, mirror=True, titlefont=dict(size=18),\
                    tickfont=dict(size=18),),
                  legend=dict(xanchor='center',x=0.55, y=-0.25, orientation='h', font=dict(size=18)),
                  template="simple_white",
                  font_family="Times New Roman",
                  width=1000,
                  height=600)
fmt_timestamps = [dt.strftime(prices['Time'][i], "%d-%b %H:%M") for i in range(len(prices))]

fig.add_trace(go.Scatter(x=time, y=energy, name="Energy Spot Revenue", line={'color':'firebrick'}, yaxis="y"))
fig.add_trace(go.Scatter(x=time, y=[ppa[i] + energy[i] for i in range(min(len(ppa),len(energy)))], \
    name=f'Net Energy Revenue ($)', line={'color':'firebrick', 'dash': 'dash'}, yaxis="y"))
fig.add_trace(go.Scatter(x=time, y=ppa, name=f'{duid_1} PPA Revenue', line={'color':'darkorange'}, yaxis="y"))
fig.add_trace(go.Scatter(x=time, y=h2, name="H2 Production Revenue", line={'color':'purple'}, yaxis="y"))
fig.add_trace(go.Scatter(x=time, y=total, name="Total Revenue", line={'color':'black'}, yaxis="y"))

for ser in fig['data']:
  ser['text'] = [dt.strftime(prices['Time'][i], "%d-%b %H:%M") for i in range(len(prices))]
  ser['hovertemplate'] = 'Time: %{text}<br>Value: %{y}'

fig.show()
fig = go.Figure(go.Waterfall(
    orientation = "v",
    measure = ["relative", "relative", "relative", "total"],
    x = ["Hydrogen Benefit", "Energy", "PPA", "Total"],
    textposition = "outside",
    textfont = dict(family="Times New Roman", size=18),
    text = ["{}k".format(int(np.ceil(sum(h2)/1000))), 
            "{}k".format(int(np.ceil(sum(energy)/1000))),
            "{}k".format(int(np.ceil(sum(ppa)/1000))),
            "{}k".format(int(np.ceil(sum(total)/1000))),],
    y = [sum(h2), sum(energy), sum(ppa), sum(total), -20, 0],
    connector = {"line":{"color":"slategrey"}},
    increasing=dict(marker=dict(color="ForestGreen")),
    decreasing = dict(marker = dict(color="firebrick")),
))

fig.update_layout(
    title='<b>NEMGLO Electrolyser with PPA Example 2<br>Operational Profit<br><sup>VIC: Jan-2020</sup></b>', titlefont=dict(size=24),
    margin=dict(l=20, r=20, t=120, b=0),
    xaxis=dict(title="Cost Component", showgrid=False, mirror=True, titlefont=dict(size=18), \
        tickfont=dict(size=18), tickangle=0, tickformat="%d-%b",  domain=[0, 1]),
    yaxis=dict(title="Revenue / Cost ($)", showgrid=True, mirror=True, titlefont=dict(size=18), \
        tickfont=dict(size=18), range=[0,1.5*10**6]),
    template="simple_white",
    font_family="Times New Roman",
    width=600,
    height=600)

fig.show()