from FINE.component import Component, ComponentModel
from FINE import utils
import warnings
import pandas as pd
import pyomo.environ as pyomo
[docs]class Conversion(Component):
#TODO
"""
Conversion class
The functionality of the the Conversion class is fourfold:
* ...
The parameter which are stored in an instance of the class refer to:
* ...
Instances of this class provide function for
* ...
Last edited: July 27, 2018
|br| @author: Lara Welder
"""
[docs] def __init__(self, esM, name, physicalUnit, commodityConversionFactors, hasCapacityVariable=True,
capacityVariableDomain='continuous', capacityPerPlantUnit=1, linkedConversionCapacityID=None,
hasIsBuiltBinaryVariable=False, bigM=None,
operationRateMax=None, operationRateFix=None, tsaWeight=1,
locationalEligibility=None, capacityMin=None, capacityMax=None, sharedPotentialID=None,
capacityFix=None, isBuiltFix=None,
investPerCapacity=0, investIfBuilt=0, opexPerOperation=0, opexPerCapacity=0,
opexIfBuilt=0, interestRate=0.08, economicLifetime=10):
# TODO: allow that the time series data or min/max/fixCapacity/eligibility is only specified for
# TODO: eligible locations
"""
Constructor for creating an Conversion class instance. Capacities are given in the plants
physicalUnit.
The Conversion component specific input arguments are described below. The general component
input arguments are described in the Component class.
**Required arguments:**
:param physicalUnit: reference physical unit of the plants to which maximum capacity limitations,
cost parameters and the operation time series refer to.
:type physicalUnit: string
:param commodityConversionFactors: conversion factors with which commodities are converted into each
other with one unit of operation (dictionary). Each commodity which is converted in this component
is indicated by a string in this dictionary. The conversion factor related to this commodity is
given as a float. A negative value indicates that the commodity is consumed. A positive value
indicates that the commodity is produced. Check unit consistency when specifying this parameter! Examples:\n
* An electrolyzer converts, simply put, electricity into hydrogen with an electrical efficiency
of 70%. The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is
given in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue
-> the commodityConversionFactors are defined as {'electricity':-1,'hydrogen':0.7}.
* A fuel cell converts, simply put, hydrogen into electricity with an efficiency of 60%.\n
The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is given in
GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue -> the
commodityConversionFactors are defined as {'electricity':1,'hydrogen':-1/0.6}.\n
:type commodityConversionFactors: dictionary, assigns commodities (string) to a conversion factors
(float)
**Default arguments:**
:param linkedConversionCapacityID: if specifies, indicates that all conversion components with the
this ID have to have the same capacity.
|br| * the default value is None
:type linkedConversionCapacityID: string
:param operationRateMax: if specified indicates a maximum operation rate for each location and each time
step by a positive float. If hasCapacityVariable is set to True, the values are given relative
to the installed capacities (i.e. in that case a value of 1 indicates a utilization of 100% of the
capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
of the physicalUnit of the plant for each time step.
|br| * the default value is None
:type operationRateMax: None or Pandas DataFrame with positive (>= 0) entries. The row indices have
to match the in the energy system model specified time steps. The column indices have to match the
in the energy system model specified locations.
:param operationRateFix: if specified indicates a fixed operation rate for each location and each time
step by a positive float. If hasCapacityVariable is set to True, the values are given relative
to the installed capacities (i.e. in that case a value of 1 indicates a utilization of 100% of the
capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
of the physicalUnit of the plant for each time step.
|br| * the default value is None
:type operationRateFix: None or Pandas DataFrame with positive (>= 0) entries. The row indices have
to match the in the energy system model specified time steps. The column indices have to match the
in the energy system model specified locations.
:param tsaWeight: weight with which the time series of the component should be considered when applying
time series aggregation.
|br| * the default value is 1
:type tsaWeight: positive (>= 0) float
:param opexPerOperation: cost which is directly proportional to the operation of the component
is obtained by multiplying the opexPerOperation parameter with the annual sum of the
operational time series of the components. The opexPerOperation can either be given as a
float or a Pandas Series with location specific values.
The cost unit in which the parameter is given has to match the one specified in the energy
system model (i.e. Euro, Dollar, 1e6 Euro).
|br| * the default value is 0
:type opexPerOperation: positive (>=0) float or Pandas Series with positive (>=0) values.
The indices of the series have to equal the in the energy system model specified locations.
"""
Component. __init__(self, esM, name, dimension='1dim', hasCapacityVariable=hasCapacityVariable,
capacityVariableDomain=capacityVariableDomain, capacityPerPlantUnit=capacityPerPlantUnit,
hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable, bigM=bigM,
locationalEligibility=locationalEligibility, capacityMin=capacityMin,
capacityMax=capacityMax, sharedPotentialID=sharedPotentialID, capacityFix=capacityFix,
isBuiltFix=isBuiltFix, investPerCapacity=investPerCapacity, investIfBuilt=investIfBuilt,
opexPerCapacity=opexPerCapacity, opexIfBuilt=opexIfBuilt, interestRate=interestRate,
economicLifetime=economicLifetime)
# Set general conversion data
utils.checkCommodities(esM, set(commodityConversionFactors.keys()))
utils.checkCommodityUnits(esM, physicalUnit)
if linkedConversionCapacityID is not None:
utils.isString(linkedConversionCapacityID)
self.commodityConversionFactors = commodityConversionFactors
self.physicalUnit = physicalUnit
self.modelingClass = ConversionModel
self.linkedConversionCapacityID = linkedConversionCapacityID
# Set additional economic data
self.opexPerOperation = utils.checkAndSetCostParameter(esM, name, opexPerOperation, '1dim',
locationalEligibility)
# Set location-specific operation parameters
if operationRateMax is not None and operationRateFix is not None:
operationRateMax = None
if esM.verbose < 2:
warnings.warn('If operationRateFix is specified, the operationRateMax parameter is not required.\n' +
'The operationRateMax time series was set to None.')
utils.checkOperationTimeSeriesInputParameters(esM, operationRateMax, locationalEligibility)
utils.checkOperationTimeSeriesInputParameters(esM, operationRateFix, locationalEligibility)
self.fullOperationRateMax = utils.setFormattedTimeSeries(operationRateMax)
self.aggregatedOperationRateMax = None
self.operationRateMax = None
self.fullOperationRateFix = utils.setFormattedTimeSeries(operationRateFix)
self.aggregatedOperationRateFix = None
self.operationRateFix = None
utils.isPositiveNumber(tsaWeight)
self.tsaWeight = tsaWeight
# Set locational eligibility
operationTimeSeries = operationRateFix if operationRateFix is not None else operationRateMax
self.locationalEligibility = \
utils.setLocationalEligibility(esM, self.locationalEligibility, self.capacityMax, self.capacityFix,
self.isBuiltFix, self.hasCapacityVariable, operationTimeSeries)
def addToEnergySystemModel(self, esM):
super().addToEnergySystemModel(esM)
def setTimeSeriesData(self, hasTSA):
self.operationRateMax = self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
self.operationRateFix = self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
def getDataForTimeSeriesAggregation(self):
weightDict, data = {}, []
weightDict, data = self.prepareTSAInput(self.fullOperationRateFix, self.fullOperationRateMax,
'_operationRate_', self.tsaWeight, weightDict, data)
return (pd.concat(data, axis=1), weightDict) if data else (None, {})
def setAggregatedTimeSeriesData(self, data):
self.aggregatedOperationRateFix = self.getTSAOutput(self.fullOperationRateFix, '_operationRate_', data)
self.aggregatedOperationRateMax = self.getTSAOutput(self.fullOperationRateMax, '_operationRate_', data)
class ConversionModel(ComponentModel):
""" Doc """
def __init__(self):
self.abbrvName = 'conv'
self.dimension = '1dim'
self.componentsDict = {}
self.capacityVariablesOptimum, self.isBuiltVariablesOptimum = None, None
self.operationVariablesOptimum = None
self.optSummary = None
####################################################################################################################
# Declare sparse index sets #
####################################################################################################################
def declareLinkedCapacityDict(self, pyM):
linkedComponentsDict, linkedComponentsList, compDict = {}, [], self.componentsDict
# Collect all conversion components with the same linkedConversionComponentID
for comp in compDict.values():
if comp.linkedConversionCapacityID is not None:
linkedComponentsDict.setdefault(comp.linkedConversionCapacityID, []).append(comp)
# Pair the components with the same linkedConversionComponentID with each other and check that
# they have the same locational eligibility
for key, values in linkedComponentsDict.items():
if len(values) > 1:
linkedComponentsList.extend([(loc, values[i].name, values[i+1].name) for i in range(len(values)-1)
for loc, v in values[i].locationalEligibility.items() if v == 1])
for comps in linkedComponentsList:
index1 = compDict[comps[1]].locationalEligibility.index
index2 = compDict[comps[2]].locationalEligibility.index
if not index1.equals(index2):
raise ValueError('Conversion components ', comps[1], 'and', comps[2],
'are linked but do not have the same locationalEligibility.')
setattr(pyM, 'linkedComponentsList_' + self.abbrvName, linkedComponentsList)
def declareSets(self, esM, pyM):
""" Declares sets and dictionaries """
# Declare design variable sets
self.initDesignVarSet(pyM)
self.initContinuousDesignVarSet(pyM)
self.initDiscreteDesignVarSet(pyM)
self.initDesignDecisionVarSet(pyM)
# Declare operation variable set
self.initOpVarSet(esM, pyM)
# Declare operation variable set
self.declareOperationModeSets(pyM, 'opConstrSet', 'operationRateMax', 'operationRateFix')
# Declare linked components dictionary
self.declareLinkedCapacityDict(pyM)
####################################################################################################################
# Declare variables #
####################################################################################################################
def declareVariables(self, esM, pyM):
""" Declares design and operation variables """
# Capacity variables in [physicalUnit]
self.declareCapacityVars(pyM)
# (Continuous) numbers of installed components in [-]
self.declareRealNumbersVars(pyM)
# (Discrete/integer) numbers of installed components in [-]
self.declareIntNumbersVars(pyM)
# Binary variables [-] indicating if a component is considered at a location or not in [-]
self.declareBinaryDesignDecisionVars(pyM)
# Operation of component [physicalUnit*hour]
self.declareOperationVars(pyM, 'op')
####################################################################################################################
# Declare component constraints #
####################################################################################################################
def linkedCapacity(self, pyM):
""" Enforces all Conversion components with the same linkedConversionCapacityID have the same capacity """
compDict, abbrvName = self.componentsDict, self.abbrvName
capVar, linkedList = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'linkedComponentsList_' + self.abbrvName)
def linkedCapacity(pyM, loc, compName1, compName2):
return capVar[loc, compName1] == capVar[loc, compName2]
setattr(pyM, 'ConstrLinkedCapacity_' + abbrvName, pyomo.Constraint(linkedList, rule=linkedCapacity))
def declareComponentConstraints(self, esM, pyM):
""" Declares time independent and dependent constraints"""
################################################################################################################
# Declare time independent constraints #
################################################################################################################
# Determine the components' capacities from the number of installed units
self.capToNbReal(pyM)
# Determine the components' capacities from the number of installed units
self.capToNbInt(pyM)
# Enforce the consideration of the binary design variables of a component
self.bigM(pyM)
# Enforce the consideration of minimum capacities for components with design decision variables
self.capacityMinDec(pyM)
# Sets, if applicable, the installed capacities of a component
self.capacityFix(pyM)
# Sets, if applicable, the binary design variables of a component
self.designBinFix(pyM)
# Links, if applicable, the capacity of components with the same linkedConversionCapacityID
self.linkedCapacity(pyM)
################################################################################################################
# Declare time dependent constraints #
################################################################################################################
# Operation [energyUnit] limited by the installed capacity [powerUnit] multiplied by the hours per time step
self.operationMode1(pyM, esM, 'ConstrOperation', 'opConstrSet', 'op')
# Operation [energyUnit] equal to the installed capacity [powerUnit] multiplied by operation time series
# [powerUnit/powerUnit] and the hours per time step [h])
self.operationMode2(pyM, esM, 'ConstrOperation', 'opConstrSet', 'op')
# Operation [energyUnit] limited by the installed capacity [powerUnit] multiplied by operation time series
# [powerUnit/powerUnit] and the hours per time step [h])
self.operationMode3(pyM, esM, 'ConstrOperation', 'opConstrSet', 'op')
# Operation [energyUnit] equal to the operation time series [energyUnit]
self.operationMode4(pyM, esM, 'ConstrOperation', 'opConstrSet', 'op')
# Operation [energyUnit] limited by the operation time series [energyUnit]
self.operationMode5(pyM, esM, 'ConstrOperation', 'opConstrSet', 'op')
####################################################################################################################
# Declare component contributions to basic EnergySystemModel constraints and its objective function #
####################################################################################################################
def getSharedPotentialContribution(self, pyM, key, loc):
return super().getSharedPotentialContribution(pyM, key, loc)
def hasOpVariablesForLocationCommodity(self, esM, loc, commod):
return any([(commod in comp.commodityConversionFactors and comp.commodityConversionFactors[commod] != 0)
and comp.locationalEligibility[loc] == 1 for comp in self.componentsDict.values()])
def getCommodityBalanceContribution(self, pyM, commod, loc, p, t):
compDict, abbrvName = self.componentsDict, self.abbrvName
opVar, opVarDict = getattr(pyM, 'op_' + abbrvName), getattr(pyM, 'operationVarDict_' + abbrvName)
return sum(opVar[loc, compName, p, t] * compDict[compName].commodityConversionFactors[commod]
for compName in opVarDict[loc] if commod in compDict[compName].commodityConversionFactors)
def getObjectiveFunctionContribution(self, esM, pyM):
capexCap = self.getEconomicsTI(pyM, ['investPerCapacity'], 'cap', 'CCF')
capexDec = self.getEconomicsTI(pyM, ['investIfBuilt'], 'designBin', 'CCF')
opexCap = self.getEconomicsTI(pyM, ['opexPerCapacity'], 'cap')
opexDec = self.getEconomicsTI(pyM, ['opexIfBuilt'], 'designBin')
opexOp = self.getEconomicsTD(pyM, esM, ['opexPerOperation'], 'op', 'operationVarDict')
return capexCap + capexDec + opexCap + opexDec + opexOp
####################################################################################################################
# Return optimal values of the component class #
####################################################################################################################
def setOptimalValues(self, esM, pyM):
compDict, abbrvName = self.componentsDict, self.abbrvName
opVar = getattr(pyM, 'op_' + abbrvName)
# Set optimal design dimension variables and get basic optimization summary
optSummaryBasic = super().setOptimalValues(esM, pyM, esM.locations, 'physicalUnit')
# Set optimal operation variables and append optimization summary
optVal = utils.formatOptimizationOutput(opVar.get_values(), 'operationVariables', '1dim', esM.periodsOrder)
self.operationVariablesOptimum = optVal
props = ['operation', 'opexOp']
units = ['[-]', '[' + esM.costUnit + '/a]']
tuples = [(compName, prop, unit) for compName in compDict.keys() for prop, unit in zip(props, units)]
tuples = list(map(lambda x: (x[0], x[1], '[' + compDict[x[0]].physicalUnit + '*h/a]')
if x[1] == 'operation' else x, tuples))
mIndex = pd.MultiIndex.from_tuples(tuples, names=['Component', 'Property', 'Unit'])
optSummary = pd.DataFrame(index=mIndex, columns=sorted(esM.locations)).sort_index()
if optVal is not None:
opSum = optVal.sum(axis=1).unstack(-1)
ox = opSum.apply(lambda op: op * compDict[op.name].opexPerOperation[op.index], axis=1)
optSummary.loc[[(ix, 'operation', '[' + compDict[ix].physicalUnit + '*h/a]') for ix in opSum.index],
opSum.columns] = opSum.values/esM.numberOfYears
optSummary.loc[[(ix, 'opexOp', '[' + esM.costUnit + '/a]') for ix in ox.index], ox.columns] = \
ox.values/esM.numberOfYears
optSummary = optSummary.append(optSummaryBasic).sort_index()
# Summarize all contributions to the total annual cost
optSummary.loc[optSummary.index.get_level_values(1) == 'TAC'] = \
optSummary.loc[(optSummary.index.get_level_values(1) == 'TAC') |
(optSummary.index.get_level_values(1) == 'opexOp')].groupby(level=0).sum().values
self.optSummary = optSummary
def getOptimalValues(self):
return super().getOptimalValues()