Module nemo.demand
Demand side scenarios.
Expand source code
# Copyright (C) 2021 Ben Elliston
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
"""Demand side scenarios."""
import numpy as np
import pandas as pd
def roll(label):
"""roll:X rolls the load by X timesteps.
>>> roll("roll:3") # doctest: +ELLIPSIS
<function roll.<locals>.<lambda> at ...>
>>> roll("junk string")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('roll:')
# label form: "roll:X" rolls the load by X timesteps
_, posns = label.split(':')
posns = int(posns)
return lambda context: _roll_demand(context, posns)
def scale(label):
"""scale:X scales all of the load uniformly by X%.
>>> scale("scale:10") # doctest: +ELLIPSIS
<function scale.<locals>.<lambda> at ...>
>>> scale("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('scale:')
_, factor = label.split(':')
factor = 1 + float(factor) / 100
return lambda context: _scale_demand_by(context, factor)
def scalex(label):
"""scalex:H1:H2:X scales hours H1 to H2 by X%.
>>> scalex("scalex:8:12:10") # doctest: +ELLIPSIS
<function scalex.<locals>.<lambda> at ...>
>>> scalex("scalex:10:30:5")
Traceback (most recent call last):
ValueError: hour > 24
>>> scalex("scalex:12:8:5")
Traceback (most recent call last):
ValueError: to_hour comes before from_hour
>>> scalex("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('scalex:')
_, hour1, hour2, factor = label.split(':')
from_hour = int(hour1)
to_hour = int(hour2)
if from_hour < 0 or to_hour < 0:
raise ValueError("hour < 0")
if from_hour > 24 or to_hour > 24:
raise ValueError("hour > 24")
if to_hour <= from_hour:
raise ValueError("to_hour comes before from_hour")
factor = 1 + float(factor) / 100
return lambda context: _scale_range_demand(context,
from_hour, to_hour, factor)
def scaletwh(label):
"""scaletwh:N scales demand to N TWh.
>>> scaletwh("scaletwh:100") # doctest: +ELLIPSIS
<function scaletwh.<locals>.<lambda> at ...>
>>> scaletwh("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('scaletwh:')
_, val = label.split(':')
new_demand = float(val)
return lambda context: _scale_demand_twh(context, new_demand)
def shift(label):
"""shift:N:H1:H2 shifts N MW of daily load from H1 to H2.
>>> shift("shift:3:10:14") # doctest: +ELLIPSIS
<function shift.<locals>.<lambda> at ...>
>>> shift("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('shift:')
_, demand, hour1, hour2 = label.split(':')
demand = int(demand)
from_hour = int(hour1)
to_hour = int(hour2)
if from_hour < 0 or to_hour < 0:
raise ValueError("hour < 0")
if from_hour > 24 or to_hour > 24:
raise ValueError("hour > 24")
return lambda context: _shift_demand(context, demand, from_hour, to_hour)
def peaks(label):
"""peaks:N:X reduces demand peaks over N MW by X%.
>>> peaks("peaks:5:10") # doctest: +ELLIPSIS
<function peaks.<locals>.<lambda> at ...>
>>> peaks("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('peaks:')
_, power, factor = label.split(':')
power = int(power)
factor = 1 + float(factor) / 100
return lambda context: _scale_peaks(context, power, factor)
def npeaks(label):
"""npeaks:N:X adjusts top N demand peaks by X%.
>>> npeaks("npeaks:5:10") # doctest: +ELLIPSIS
<function npeaks.<locals>.<lambda> at ...>
>>> npeaks("junkstring")
Traceback (most recent call last):
AssertionError
"""
assert label.startswith('npeaks:')
_, topn, factor = label.split(':')
topn = int(topn)
factor = 1 + float(factor) / 100
return lambda context: _scale_npeaks(context, topn, factor)
def unchanged(_):
"""No demand modification.
>>> unchanged("any") # doctest: +ELLIPSIS
<function unchanged.<locals>.<lambda> at ...>
"""
return lambda context: context
switch_table = [
(lambda s: s == 'unchanged', unchanged),
(lambda s: s.startswith('roll:'), roll),
(lambda s: s.startswith('scale:'), scale),
(lambda s: s.startswith('scalex:'), scalex),
(lambda s: s.startswith('scaletwh:'), scaletwh),
(lambda s: s.startswith('shift:'), shift),
(lambda s: s.startswith('peaks:'), peaks),
(lambda s: s.startswith('npeaks:'), npeaks),
]
# Demand modifiers
def switch(label):
"""Return a callback function to modify the demand.
>>> switch('unchanged') # doctest: +ELLIPSIS
<function ...>
>>> switch('roll:10') # doctest: +ELLIPSIS
<function ...>
>>> switch('scale:5') # doctest: +ELLIPSIS
<function ...>
>>> switch('scalex:0:10:5') # doctest: +ELLIPSIS
<function ...>
>>> switch('shift:100:10:12') # doctest: +ELLIPSIS
<function ...>
>>> switch('shift:100:-2:12') # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: hour < 0
>>> switch('shift:100:12:25') # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: hour > 24
>>> switch('scalex:-1:12:20') # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: hour < 0
>>> switch('scalex:12:25:20') # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: hour > 24
>>> switch('scalex:20:8:20') # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: to_hour comes before from_hour
>>> switch('peaks:10:34000') # doctest: +ELLIPSIS
<function ...>
>>> switch('npeaks:10:5') # doctest: +ELLIPSIS
<function ...>
>>> switch('foo')
Traceback (most recent call last):
ValueError: invalid scenario: foo
"""
for (predicate, callback) in switch_table:
if predicate(label):
return callback(label)
raise ValueError(f'invalid scenario: {label}')
def _roll_demand(context, posns):
"""
Roll demand by posns timesteps.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame(list(range(10)))
>>> _roll_demand(c, 1)
>>> print(c.demand)
0
0 9
1 0
2 1
3 2
4 3
5 4
6 5
7 6
8 7
9 8
"""
idx = context.demand.index
values = np.roll(context.demand.values, posns)
context.demand = pd.DataFrame(data=values, index=idx)
def _scale_range_demand(context, from_hour, to_hour, factor):
"""
Scale demand between from_hour and to_hour by factor%.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame(list(range(10)))
>>> _scale_range_demand(c, 0, 4, 1.2)
>>> print(c.demand)
0
0 0.0
1 1.2
2 2.4
3 3.6
4 4.0
5 5.0
6 6.0
7 7.0
8 8.0
9 9.0
"""
for hour in range(from_hour, to_hour):
context.demand[hour::24] *= factor
def _scale_demand_twh(context, new_demand):
"""
Scale demand to new_demand TWh.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame([100]*10)
>>> _scale_demand_twh(c, 0.0002)
>>> print(c.demand.loc[0])
0 20.0
Name: 0, dtype: float64
"""
total_demand = context.demand.values.sum()
new_demand *= 10 ** 6
context.demand *= new_demand / total_demand
def _scale_demand_by(context, factor):
"""
Scale demand by factor%.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame([0, 1, 2])
>>> _scale_demand_by(c, 1.2)
>>> print(c.demand)
0
0 0.0
1 1.2
2 2.4
"""
context.demand *= factor
def _shift_demand(context, demand, from_hour, to_hour):
"""Move n MW of demand from from_hour to to_hour.
>>> from nemo import context, regions
>>> ctx = context.Context()
>>> ctx.regions = [regions.sa]
>>> ctx.demand = np.zeros((43, 24 * 7))
>>> ctx.demand[26][0::24] = 100 # polygon 27
>>> ctx.demand[31][0::24] = 100 # polygon 32
>>> saved_sum = ctx.demand.sum()
>>> _shift_demand(ctx, 50, 0, 12) # shift 50MW from midnight to noon
>>> assert ctx.demand.sum() == saved_sum # verify no change
>>> ctx.demand[26][0], ctx.demand[26][12] # 5MW -> noon
(95.0, 5.0)
>>> ctx.demand[31][0], ctx.demand[31][12] # 45MW -> noon
(55.0, 45.0)
"""
# Shift demand within in each polygon
for poly in range(43):
for regn in context.regions:
if poly + 1 in regn.polygons:
weight = regn.polygons[poly + 1]
if context.demand[poly].sum() > 0:
context.demand[poly, from_hour::24] -= demand * weight
context.demand[poly, to_hour::24] += demand * weight
assert np.all(context.demand >= 0), \
f"negative load in hour {from_hour}"
def _scale_peaks(context, power, factor):
"""
Adjust demand peaks over N megawatts by factor%.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame([[0.0]*5]*5)
>>> c.demand.loc[3] = 5000
>>> _scale_peaks(c, 3000, 0.5)
>>> c.demand.loc[3]
0 2500.0
1 2500.0
2 2500.0
3 2500.0
4 2500.0
Name: 3, dtype: float64
"""
agg_demand = context.demand.sum(axis=1)
context.demand[agg_demand > power] *= factor
def _scale_npeaks(context, topn, factor):
"""
Adjust top N demand peaks by X%.
>>> c = type('context', (), {})
>>> c.demand = pd.DataFrame([[0.0]*5]*5)
>>> c.demand.loc[3] = 5000
>>> c.demand.loc[4] = 3000
>>> _scale_npeaks(c, 1, 0.5)
>>> c.demand.loc[4]
0 3000.0
1 3000.0
2 3000.0
3 3000.0
4 3000.0
Name: 4, dtype: float64
>>> c.demand.loc[3]
0 2500.0
1 2500.0
2 2500.0
3 2500.0
4 2500.0
Name: 3, dtype: float64
"""
agg_demand = context.demand.sum(axis=1).sort_values(ascending=False)
rng = agg_demand.head(topn).index
context.demand.loc[rng] *= factor
Functions
def npeaks(label)
-
npeaks:N:X adjusts top N demand peaks by X%.
>>> npeaks("npeaks:5:10") # doctest: +ELLIPSIS <function npeaks.<locals>.<lambda> at ...> >>> npeaks("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def npeaks(label): """npeaks:N:X adjusts top N demand peaks by X%. >>> npeaks("npeaks:5:10") # doctest: +ELLIPSIS <function npeaks.<locals>.<lambda> at ...> >>> npeaks("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('npeaks:') _, topn, factor = label.split(':') topn = int(topn) factor = 1 + float(factor) / 100 return lambda context: _scale_npeaks(context, topn, factor)
def peaks(label)
-
peaks:N:X reduces demand peaks over N MW by X%.
>>> peaks("peaks:5:10") # doctest: +ELLIPSIS <function peaks.<locals>.<lambda> at ...> >>> peaks("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def peaks(label): """peaks:N:X reduces demand peaks over N MW by X%. >>> peaks("peaks:5:10") # doctest: +ELLIPSIS <function peaks.<locals>.<lambda> at ...> >>> peaks("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('peaks:') _, power, factor = label.split(':') power = int(power) factor = 1 + float(factor) / 100 return lambda context: _scale_peaks(context, power, factor)
def roll(label)
-
roll:X rolls the load by X timesteps.
>>> roll("roll:3") # doctest: +ELLIPSIS <function roll.<locals>.<lambda> at ...> >>> roll("junk string") Traceback (most recent call last): AssertionError
Expand source code
def roll(label): """roll:X rolls the load by X timesteps. >>> roll("roll:3") # doctest: +ELLIPSIS <function roll.<locals>.<lambda> at ...> >>> roll("junk string") Traceback (most recent call last): AssertionError """ assert label.startswith('roll:') # label form: "roll:X" rolls the load by X timesteps _, posns = label.split(':') posns = int(posns) return lambda context: _roll_demand(context, posns)
def scale(label)
-
scale:X scales all of the load uniformly by X%.
>>> scale("scale:10") # doctest: +ELLIPSIS <function scale.<locals>.<lambda> at ...> >>> scale("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def scale(label): """scale:X scales all of the load uniformly by X%. >>> scale("scale:10") # doctest: +ELLIPSIS <function scale.<locals>.<lambda> at ...> >>> scale("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('scale:') _, factor = label.split(':') factor = 1 + float(factor) / 100 return lambda context: _scale_demand_by(context, factor)
def scaletwh(label)
-
scaletwh:N scales demand to N TWh.
>>> scaletwh("scaletwh:100") # doctest: +ELLIPSIS <function scaletwh.<locals>.<lambda> at ...> >>> scaletwh("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def scaletwh(label): """scaletwh:N scales demand to N TWh. >>> scaletwh("scaletwh:100") # doctest: +ELLIPSIS <function scaletwh.<locals>.<lambda> at ...> >>> scaletwh("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('scaletwh:') _, val = label.split(':') new_demand = float(val) return lambda context: _scale_demand_twh(context, new_demand)
def scalex(label)
-
scalex:H1:H2:X scales hours H1 to H2 by X%.
>>> scalex("scalex:8:12:10") # doctest: +ELLIPSIS <function scalex.<locals>.<lambda> at ...> >>> scalex("scalex:10:30:5") Traceback (most recent call last): ValueError: hour > 24 >>> scalex("scalex:12:8:5") Traceback (most recent call last): ValueError: to_hour comes before from_hour >>> scalex("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def scalex(label): """scalex:H1:H2:X scales hours H1 to H2 by X%. >>> scalex("scalex:8:12:10") # doctest: +ELLIPSIS <function scalex.<locals>.<lambda> at ...> >>> scalex("scalex:10:30:5") Traceback (most recent call last): ValueError: hour > 24 >>> scalex("scalex:12:8:5") Traceback (most recent call last): ValueError: to_hour comes before from_hour >>> scalex("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('scalex:') _, hour1, hour2, factor = label.split(':') from_hour = int(hour1) to_hour = int(hour2) if from_hour < 0 or to_hour < 0: raise ValueError("hour < 0") if from_hour > 24 or to_hour > 24: raise ValueError("hour > 24") if to_hour <= from_hour: raise ValueError("to_hour comes before from_hour") factor = 1 + float(factor) / 100 return lambda context: _scale_range_demand(context, from_hour, to_hour, factor)
def shift(label)
-
shift:N:H1:H2 shifts N MW of daily load from H1 to H2.
>>> shift("shift:3:10:14") # doctest: +ELLIPSIS <function shift.<locals>.<lambda> at ...> >>> shift("junkstring") Traceback (most recent call last): AssertionError
Expand source code
def shift(label): """shift:N:H1:H2 shifts N MW of daily load from H1 to H2. >>> shift("shift:3:10:14") # doctest: +ELLIPSIS <function shift.<locals>.<lambda> at ...> >>> shift("junkstring") Traceback (most recent call last): AssertionError """ assert label.startswith('shift:') _, demand, hour1, hour2 = label.split(':') demand = int(demand) from_hour = int(hour1) to_hour = int(hour2) if from_hour < 0 or to_hour < 0: raise ValueError("hour < 0") if from_hour > 24 or to_hour > 24: raise ValueError("hour > 24") return lambda context: _shift_demand(context, demand, from_hour, to_hour)
def switch(label)
-
Return a callback function to modify the demand.
>>> switch('unchanged') # doctest: +ELLIPSIS <function ...>
>>> switch('roll:10') # doctest: +ELLIPSIS <function ...>
>>> switch('scale:5') # doctest: +ELLIPSIS <function ...>
>>> switch('scalex:0:10:5') # doctest: +ELLIPSIS <function ...>
>>> switch('shift:100:10:12') # doctest: +ELLIPSIS <function ...>
>>> switch('shift:100:-2:12') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour < 0
>>> switch('shift:100:12:25') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour > 24
>>> switch('scalex:-1:12:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour < 0
>>> switch('scalex:12:25:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour > 24
>>> switch('scalex:20:8:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: to_hour comes before from_hour
>>> switch('peaks:10:34000') # doctest: +ELLIPSIS <function ...>
>>> switch('npeaks:10:5') # doctest: +ELLIPSIS <function ...>
>>> switch('foo') Traceback (most recent call last): ValueError: invalid scenario: foo
Expand source code
def switch(label): """Return a callback function to modify the demand. >>> switch('unchanged') # doctest: +ELLIPSIS <function ...> >>> switch('roll:10') # doctest: +ELLIPSIS <function ...> >>> switch('scale:5') # doctest: +ELLIPSIS <function ...> >>> switch('scalex:0:10:5') # doctest: +ELLIPSIS <function ...> >>> switch('shift:100:10:12') # doctest: +ELLIPSIS <function ...> >>> switch('shift:100:-2:12') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour < 0 >>> switch('shift:100:12:25') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour > 24 >>> switch('scalex:-1:12:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour < 0 >>> switch('scalex:12:25:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: hour > 24 >>> switch('scalex:20:8:20') # doctest: +ELLIPSIS Traceback (most recent call last): ValueError: to_hour comes before from_hour >>> switch('peaks:10:34000') # doctest: +ELLIPSIS <function ...> >>> switch('npeaks:10:5') # doctest: +ELLIPSIS <function ...> >>> switch('foo') Traceback (most recent call last): ValueError: invalid scenario: foo """ for (predicate, callback) in switch_table: if predicate(label): return callback(label) raise ValueError(f'invalid scenario: {label}')
def unchanged(_)
-
No demand modification.
>>> unchanged("any") # doctest: +ELLIPSIS <function unchanged.<locals>.<lambda> at ...>
Expand source code
def unchanged(_): """No demand modification. >>> unchanged("any") # doctest: +ELLIPSIS <function unchanged.<locals>.<lambda> at ...> """ return lambda context: context