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 r

  • Conversion efficiency α from heat to power.

  • Maximum share heat β

  • Minimum capacity lt for every timestep t{0,,T1}

  • Maximum capacity ut for every timestep t{0,,T1}

  • Dispatch power x1 from the previous timestep

  • Dispatch heat w1 from the previous timestep

  • Number kon of timesteps the asset has already been running

  • Number koff of timesteps the asset has already been of

  • Minimum runtime mrun

  • Minimum downtime mdown

  • Start ramp specification:

    • Minimum capacity listart for timestep i{0,,kstart} after starting

    • Maximum capacity uistart for timestep i{0,,kstart} after starting

  • Shutdown ramp specification:

    • Minimum capacity lishutdown for timestep i{0,,kshutdown} before shutting off

    • Maximum capacity uishutdown for timestep i{0,,kshutdown} before shutting off

Output variables

The following variables are computed for a CHPAsset that runs for T timesteps:

  • Dispatch power xtR, for every timestep t{0,,T1}

  • Dispatch heat wtR, for every timestep t{0,,T1}

  • “On”-variable at{0,1}, for every timestep t{0,,T1}

  • “Start”-variable st{0,1}, for every timestep t{0,,T1}

  • “Shutdown”-variable dt{0,1}, for every timestep t{0,,T1}

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 t{0,,T1}:

  • 0xtut

  • 0wtβut

  • 0at1

  • 0st1

  • 0dt1

Constraints

The following constraints are used in the asset

  • Minimum and maximum capacity: The virtual dispatch x~t=xt+αwt is 0 when at=0, 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. x~tlt[ati=0kshutdown1dt+i+1i=0kstart1sti]+i=0kstart1listartsti+i=0kshutdown1lishutdowndt+i+1x~tut[ati=0kshutdown1dt+i+1i=0kstart1sti]+i=0kstart1uistartsti+i=0kshutdown1uishutdowndt+i+1 for t{0,,T1}. It should be noted that the term i=0kstart1sti is 1 if timestep t is part of the start ramp and 0 otherwise. Analogously, i=0kshutdown1dt+i+1 is 1 if timestep t is part of the shutdown ramp and 0 otherwise. If kon>0 and kon<kstart, i.e. the asset is in the starting process at timestep 0, the first kstartkon timesteps are instead bounded by lkon+istartx~iukon+istart for every i{0,,kstartkon1}.

  • Ramp: Ensure that the increase/decrease of the virtual dispatch x~t=xt+αwt is bounded by r, unless during timesteps that belong to the start or shutdown ramp, i.e.
    x~tx~t1r[at1i=0kshutdown1dt+i]ut1i=0kshutdown1dt+ix~tx~t1r[ati=0kstart1sti]+uti=0kstart1sti, for t{0,,T1}.
  • 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. statat1, for t{1,,T1}. If kon=0, i.e. previous state a1 is “off”, we need the additional condition s0a0.

  • Start and shutdown constraints: (Only applicable when start or shutdown ramp are defined) Ensure that the start and shutdown variables are set correctly, i.e stdt=atat1st+dt1 for t{0,,T1}.

  • Minimum runtime: Ensure that the asset follows the minimum runtime mrun and is on during the start and shutdown ramp, i.e. atsti for t{1,,T1} and i{1,,mrun+kstart+kshutdown1}. If kon>0 and mrun+kstart+kshutdown>kon, we need the additional condition ai1, for all i{0,,mrun+kstart+kshutdownkon1}.

  • Minimum downtime: If the asset turns off, it has to stay off for the minimum downtime mdown, i.e. at1ati1+ati for t{1,,T1} and i{1,,mdown1}. If koff>0 and mdown>koff, we need the additional condition ai0, for all i{0,,mdownkoff1}.

  • Maximum share heat: Bound the heat dispatch by a percentage of the power dispatch, i.e. wtβxt, for t{0,,T1}.

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)
../../_images/samples_combined_heat_power_chp_sample_8_0.svg

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])
../../_images/samples_combined_heat_power_chp_sample_12_0.svg
[ ]: