Combined Heat and Power
The CHPAsset class represents an asset that can generate both heat and power at the same time and models starting procedures, minimum loads, ramping and a minimum runtime.
Technical Details
Input Parameters
Possible input parameters of the CHPAsset are
Ramp
Conversion efficiency
from heat to power.Maximum share heat
Minimum capacity
for every timestepMaximum capacity
for every timestepDispatch power
from the previous timestepDispatch heat
from the previous timestepNumber
of timesteps the asset has already been runningNumber
of timesteps the asset has already been ofMinimum runtime
Minimum downtime
Start ramp specification:
Minimum capacity
for timestep after startingMaximum capacity
for timestep after starting
Shutdown ramp specification:
Minimum capacity
for timestep before shutting offMaximum capacity
for timestep before shutting off
Output variables
The following variables are computed for a CHPAsset that runs for
Dispatch power
, for every timestepDispatch heat
, for every timestep“On”-variable
, for every timestep“Start”-variable
, for every timestep“Shutdown”-variable
, for every timestep
When they are not needed to enforce a minimum runtime, start costs or similar, the “on” and “start” variables are removed.
Variable bounds
The output variables are within the following bounds in every timestep
Constraints
The following constraints are used in the asset
Minimum and maximum capacity: The virtual dispatch
is 0 when , i.e. the asset is “off”, it is bounded by the start or shutdown specifications during the start and shutdown, and otherwise it is between minimum and maximum capacity, i.e. for . It should be noted that the term is 1 if timestep is part of the start ramp and 0 otherwise. Analogously, is 1 if timestep is part of the shutdown ramp and 0 otherwise. If and , i.e. the asset is in the starting process at timestep , the first timesteps are instead bounded by for every .- Ramp: Ensure that the increase/decrease of the virtual dispatch
is bounded by , unless during timesteps that belong to the start or shutdown ramp, i.e. for . Start constraints: (Only applicable when there is no start- or shutdown ramp specification, otherwise the start variables are defined simultaneously with the shutdown variables, see “start and shutdown constraints”) When the “on” variable changes from “off” to “on”, the start variable has to be 1, i.e.
for . If , i.e. previous state is “off”, we need the additional conditionStart and shutdown constraints: (Only applicable when start or shutdown ramp are defined) Ensure that the start and shutdown variables are set correctly, i.e
for .Minimum runtime: Ensure that the asset follows the minimum runtime
and is on during the start and shutdown ramp, i.e. for and . If and , we need the additional condition for all .Minimum downtime: If the asset turns off, it has to stay off for the minimum downtime
, i.e. for and . If and , we need the additional condition for all .Maximum share heat: Bound the heat dispatch by a percentage of the power dispatch, i.e.
for .
Example Portfolio
Imports
[1]:
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
import sys
import os
myDir = os.path.join(os.getcwd(), '../..')
sys.path.append(myDir)
addDir = os.path.join(os.getcwd(), '../../../..')
sys.path.append(addDir)
import eaopack as eao
from eaopack.portfolio import Portfolio
from eaopack.basic_classes import Timegrid
from eaopack.optimization import Results
Defining a portfolio
[2]:
node_power = eao.assets.Node('node_power')
node_heat = eao.assets.Node('node_heat')
node_gas = eao.assets.Node('node_gas')
timegrid = eao.assets.Timegrid(dt.date(2021, 1, 1), dt.date(2021, 1, 2), freq='15min', main_time_unit='h')
simple_contract_power = eao.assets.SimpleContract(name='SC_power', price='price_power', nodes=node_power,
min_cap=-30., max_cap=20)
simple_contract_heat = eao.assets.SimpleContract(name='SC_heat', price='price_heat', nodes=node_heat,
min_cap=-30., max_cap=20.)
simple_contract_gas = eao.assets.SimpleContract(name='SC_gas', price='price_gas', nodes=node_gas,
min_cap=-30., max_cap=20.)
chpasset = eao.assets.CHPAsset(name='CHP',
nodes=[node_power, node_heat, node_gas],
min_cap=5.,
max_cap=30.,
extra_costs=0,
conversion_factor_power_heat=0.5,
max_share_heat=0.5,
ramp=10,
start_ramp_lower_bounds=[1, 2],
shutdown_ramp_lower_bounds=[2, 3],
ramp_freq="15min",
start_costs=0.,
running_costs=0.,
min_runtime=0.,
time_already_running=0.,
min_downtime=0.,
time_already_off=1.,
last_dispatch=0.,
start_fuel=0)
prices = {'price_power': np.sin(0.5*np.pi * np.arange(timegrid.T)*np.pi/10)+2,
'price_heat': np.cos(0.5*np.pi * np.arange(timegrid.T)*np.pi/10)+2,
'price_gas': np.ones(timegrid.T)*2,
}
portfolio = eao.portfolio.Portfolio([chpasset, simple_contract_power, simple_contract_heat, simple_contract_gas])
eao.network_graphs.create_graph(portf = portfolio)
Performing the Optimization
[3]:
op = portfolio.setup_optim_problem(prices, timegrid)
res = op.optimize(solver="GLPK_MI")
# res = op.optimize(solver="GLPK", make_soft_problem=True) # Relax bool variables, i.e. allow "half on"
out = eao.io.extract_output(portfolio, op, res, prices)
out['summary']
...MIP problem configured. Beware of potentially long optimization and other issues inherent to MIP
[3]:
Values | |
---|---|
Parameter | |
status | successful |
value | 243.759802 |
Plotting the Results
[4]:
# Define the grid size to display the figures:
num_imgs_per_row = 3
num_nodes = len(portfolio.nodes)
num_img_rows=int(np.ceil((num_nodes+2)/num_imgs_per_row))
fig, ax = plt.subplots(num_img_rows, num_imgs_per_row, tight_layout = True, figsize=(18,5*num_img_rows))
ax = ax.reshape(-1)
# Plot the dispatch for each node:
current_ax=0
dispatch_per_node = {node: [] for node in portfolio.nodes}
for mycol in out['dispatch'].columns.values:
for node in dispatch_per_node:
if '(' + node + ')' in mycol:
dispatch_per_node[node].append(mycol)
for node in dispatch_per_node:
out['dispatch'][dispatch_per_node[node]].plot(ax=ax[current_ax], style='-x')
ax[current_ax].set_title("Dispatch " + node)
current_ax+=1
# Plot internal variables:
for v in out['internal_variables']:
out['internal_variables'][v].plot(ax=ax[current_ax], style='-x', label=v)
if out['internal_variables'].shape[1] == 0:
ax[current_ax].text(0.4, 0.5, "No bool variables present")
ax[current_ax].set_title('Bool Variables')
ax[current_ax].legend()
current_ax+=1
# Plot prices
for key in out['prices']:
out['prices'][key].plot(ax=ax[current_ax], style='-x', label=key)
ax[current_ax].set_title('Prices')
ax[current_ax].legend()
# Remove empty axes:
for i in range(current_ax+1, len(ax)):
fig.delaxes(ax[i])
[ ]: