Source code for component

from abc import ABCMeta, abstractmethod
from FINE import utils
import warnings
import pyomo.environ as pyomo
import pandas as pd


[docs]class Component(metaclass=ABCMeta): """ Doc """
[docs] def __init__(self, esM, name, dimension, hasCapacityVariable, capacityVariableDomain='continuous', capacityPerPlantUnit=1, hasIsBuiltBinaryVariable=False, bigM=None, locationalEligibility=None, capacityMin=None, capacityMax=None, sharedPotentialID=None, capacityFix=None, isBuiltFix=None, investPerCapacity=0, investIfBuilt=0, opexPerCapacity=0, opexIfBuilt=0, interestRate=0.08, economicLifetime=10): """ Constructor for creating an Conversion class instance. **Required arguments:** :param esM: energy system model to which the component should be added. Used for unit checks. :type esM: EnergySystemModel instance from the FINE package :param name: name of the component. Has to be unique (i.e. no other components with that name can already exist in the EnergySystemModel instance to which the component is added). :type name: string :param hasCapacityVariable: specifies if the component should be modeled with a capacity or not. Examples:\n * An electrolyzer has a capacity given in GW_electric -> hasCapacityVariable is True. * In the energy system, biogas can, from a model perspective, be converted into methane (and then used in conventional power plants which emit CO2) by getting CO2 from the environment. Thus, using biogas in conventional power plants is, from a balance perspective, CO2 free. This conversion is purely theoretical and does not require a capacity -> hasCapacityVariable is False. * A electricity cable has a capacity given in GW_electric -> hasCapacityVariable is True. * If the transmission capacity of a component is unlimited hasCapacityVariable is False. * A wind turbine has a capacity given in GW_electric -> hasCapacityVariable is True. * Emitting CO2 into the environment is not per se limited by a capacity -> hasCapacityVariable is False.\n :type hasCapacityVariable: boolean **Default arguments:** :param capacityVariableDomain: the mathematical domain of the capacity variables, if they are specified. By default, the domain is specified as 'continuous' and thus declares the variables as positive (>=0) real values. The second input option that is available for this parameter is 'discrete', which declares the variables as positive (>=0) integer values. |br| * the default value is 'continuous' :type capacityVariableDomain: string ('continuous' or 'discrete') :param capacityPerPlantUnit: capacity of one plant of the component (in the specified physicalUnit of the plant). The default is 1, thus the number of plants is equal to the installed capacity. This parameter should be specified when using a 'discrete' capacityVariableDomain. It can be specified when using a 'continuous' variable domain. |br| * the default value is 1 :type capacityPerPlantUnit: strictly positive float :param hasIsBuiltBinaryVariable: specifies if binary decision variables should be declared for\n * each eligible location of the component, which indicate if the component is built at that location or not (dimension=1dim). * each eligible connection of the transmission component, which indicate if the component is built between two locations or not (dimension=2dim).\n The binary variables can be used to enforce one-time investment cost or capacity-independent annual operation cost. If a minimum capacity is specified and this parameter is set to True, the minimum capacities are only considered if a component is built (i.e. if a component is built at that location, it has to be built with a minimum capacity of XY GW, otherwise it is set to 0 GW). |br| * the default value is False :type hasIsBuiltBinaryVariable: boolean :param bigM: the bigM parameter is only required when the hasIsBuiltBinaryVariable parameter is set to True. In that case, it is set as a strictly positive float, otherwise it can remain a None value. If not None and the ifBuiltBinaryVariables parameter is set to True, the parameter enforces an artificial upper bound on the maximum capacities which should, however, never be reached. The value should be chosen as small as possible but as large as necessary so that the optimal values of the designed capacities are well below this value after the optimization. |br| * the default value is None :type bigM: None or strictly positive float :param locationalEligibility: Pandas\n * Series that indicates if a component can be built at a location (=1) or not (=0) (dimension=1dim) or * Pandas DataFrame that indicates if a component can be built between two locations (=1) or not (=0) (dimension=2dim).\n If not specified and a maximum or fixed capacity or time series is given, the parameter will be set based on these inputs. If the parameter is specified, a consistency check is done to ensure that the parameters indicate the same locational eligibility. If the parameter is not specified and also no other of the parameters is specified it is assumed that the component is eligible in each location and all values are set to 1. This parameter is key for ensuring small built times of the optimization problem by avoiding the declaration of unnecessary variables and constraints. |br| * the default value is None :type locationalEligibility:\n * None or * Pandas Series with values equal to 0 and 1. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with values equal to 0 and 1. The column and row indices of the DataFrame have to equal the in the energy system model specified locations. :param capacityMin: if specified, Pandas Series (dimension=1dim) or Pandas DataFrame (dimension=2dim) indicating minimum capacities else None. If binary decision variables are declared the minimum capacity is only enforced if the component is built . |br| * the default value is None :type capacityMin: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param capacityMax: if specified, Pandas Series (dimension=1dim) or Pandas DataFrame (dimension=2dim) indicating maximum capacities else None. |br| * the default value is None :type capacityMax: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param sharedPotentialID: if specified, indicates that the component has to share its maximum potential capacity with other components (i.e. due to space limitations). The shares of how much of the maximum potential is used have to add up to less then 100%. |br| * the default value is None :type sharedPotentialID: string :param capacityFix: if specified, Pandas Series (dimension=1dim) or Pandas DataFrame (dimension=2dim) indicating fixed capacities else None. |br| * the default value is None :type capacityFix: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param isBuiltFix: if specified, Pandas Series (dimension=1dim) or Pandas DataFrame (dimension=2dim) indicating fixed decisions in which or between which locations the component is built (i.e. sets the isBuilt binary variables) else None. |br| * the default value is None :type isBuiltFix: * None or * Pandas Series with with values equal to 0 and 1. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with values equal to 0 and 1. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param investPerCapacity: the invest of a component is obtained by multiplying the built capacities of the component (in the physicalUnit of the component) with the investPerCapacity factor. The investPerCapacity can either be given as\n * 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) (dimension=1dim) or * a float or a Pandas DataFrame with location specific values. The cost unit in which the parameter is given has to match the one specified in the energy system model divided by the there specified lengthUnit (i.e. Euro/m, Dollar/m, 1e6 Euro/km) (dimension=2dim)\n |br| * the default value is 0 :type investPerCapacity: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param investIfBuilt: a capacity-independent invest which only arises in a location if a component is built at that location. The investIfBuilt can either be given as\n * 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) (dimension=1dim) or * a float or a Pandas DataFrame with location specific values. The cost unit in which the parameter is given has to match the one specified in the energy system model divided by the there specified lengthUnit (i.e. Euro/m, Dollar/m, 1e6 Euro/km) (dimension=2dim)\n |br| * the default value is 0 :type investIfBuilt: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param opexPerCapacity: annual operational cost which are only a function of the capacity of the component (in the physicalUnit of the component) and not of the specific operation itself are obtained by multiplying the capacity of the component at a location with the opexPerCapacity factor. The opexPerCapacity can either be given as\n * 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) (dimension=1dim) or * a float or a Pandas DataFrame with location specific values. The cost unit in which the parameter is given has to match the one specified in the energy system model divided by the there specified lengthUnit (i.e. Euro/m, Dollar/m, 1e6 Euro/km) (dimension=2dim)\n |br| * the default value is 0 :type opexPerCapacity: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param opexIfBuilt: a capacity-independent annual operational cost which only arises in a location if a component is built at that location. The opexIfBuilt can either be given as\n * 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) (dimension=1dim) or * a float or a Pandas DataFrame with location specific values. The cost unit in which the parameter is given has to match the one specified in the energy system model divided by the there specified lengthUnit (i.e. Euro/m, Dollar/m, 1e6 Euro/km) (dimension=2dim)\n |br| * the default value is 0 :type opexIfBuilt: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param interestRate: interest rate which is considered for computing the annuities of the invest of the component (depreciates the invests over the economic lifetime). A value of 0.08 corresponds to an interest rate of 8%. |br| * the default value is 0.08 :type interestRate: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param economicLifetime: economic lifetime of the component which is considered for computing the annuities of the invest of the component (aka depreciation time). |br| * the default value is 10 :type economicLifetime: * None or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param modelingClass: to the Component connected modeling class. |br| * the default value is ModelingClass :type modelingClass: a class inherting from ComponentModeling """ # Set general component data utils.isEnergySystemModelInstance(esM) self.name = name self.dimension = dimension self.modelingClass = ComponentModel # Set design variable modeling parameters utils.checkDesignVariableModelingParameters(capacityVariableDomain, hasCapacityVariable, capacityPerPlantUnit, hasIsBuiltBinaryVariable, bigM) self.hasCapacityVariable = hasCapacityVariable self.capacityVariableDomain = capacityVariableDomain self.capacityPerPlantUnit = capacityPerPlantUnit self.hasIsBuiltBinaryVariable = hasIsBuiltBinaryVariable self.bigM = bigM # Set economic data elig = locationalEligibility self.investPerCapacity = utils.checkAndSetCostParameter(esM, name, investPerCapacity, dimension, elig) self.investIfBuilt = utils.checkAndSetCostParameter(esM, name, investIfBuilt, dimension, elig) self.opexPerCapacity = utils.checkAndSetCostParameter(esM, name, opexPerCapacity, dimension, elig) self.opexIfBuilt = utils.checkAndSetCostParameter(esM, name, opexIfBuilt, dimension, elig) self.interestRate = utils.checkAndSetCostParameter(esM, name, interestRate, dimension, elig) self.economicLifetime = utils.checkAndSetCostParameter(esM, name, economicLifetime, dimension, elig) self.CCF = utils.getCapitalChargeFactor(self.interestRate, self.economicLifetime) # Set location-specific design parameters utils.checkLocationSpecficDesignInputParams(esM, hasCapacityVariable, hasIsBuiltBinaryVariable, capacityMin, capacityMax, capacityFix, locationalEligibility, isBuiltFix, sharedPotentialID, dimension=dimension) self.locationalEligibility = locationalEligibility self.sharedPotentialID = sharedPotentialID self.capacityMin, self.capacityMax, self.capacityFix = capacityMin, capacityMax, capacityFix self.isBuiltFix = isBuiltFix
# # # Variables at optimum (set after optimization) # self.capacityVariablesOptimum = None # self.isBuiltVariablesOptimum = None # self.operationVariablesOptimum = {} def addToEnergySystemModel(self, esM): esM.isTimeSeriesDataClustered = False if self.name in esM.componentNames: if esM.componentNames[self.name] == self.modelingClass.__name__ and esM.verbose < 2: warnings.warn('Component identifier ' + self.name + ' already exists. Data will be overwritten.') elif esM.componentNames[self.name] != self.modelingClass.__name__ : raise ValueError('Component name ' + self.name + ' is not unique.') else: esM.componentNames.update({self.name: self.modelingClass.__name__}) mdl = self.modelingClass.__name__ if mdl not in esM.componentModelingDict: esM.componentModelingDict.update({mdl: self.modelingClass()}) esM.componentModelingDict[mdl].componentsDict.update({self.name: self}) def prepareTSAInput(self, rateFix, rateMax, rateName, rateWeight, weightDict, data): data_ = rateFix if rateFix is not None else rateMax if data_ is not None: data_ = data_.copy() uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns] data_.rename(columns={loc: self.name + rateName + loc for loc in data_.columns}, inplace=True) weightDict.update({id: rateWeight for id in uniqueIdentifiers}), data.append(data_) return weightDict, data def getTSAOutput(self, rate, rateName, data): if rate is not None: uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns] data_ = data[uniqueIdentifiers].copy() data_.rename(columns={self.name + rateName + loc: loc for loc in rate.columns}, inplace=True) return data_ else: return None @abstractmethod def setTimeSeriesData(self, hasTSA): raise NotImplementedError @abstractmethod def getDataForTimeSeriesAggregation(self): raise NotImplementedError @abstractmethod def setAggregatedTimeSeriesData(self, data): raise NotImplementedError
class ComponentModel(metaclass=ABCMeta): """ Doc """ def __init__(self): self.abbrvName = '' self.dimension = '' self.componentsDict = {} self.capacityVariablesOptimum, self.isBuiltVariablesOptimum = None, None self.operationVariablesOptimum = {} self.optSummary = None #################################################################################################################### # Functions for declaring design and operation variables sets # #################################################################################################################### def initDesignVarSet(self, pyM): """ Declares set for capacity variables in the pyomo object for a modeling class """ compDict, abbrvName = self.componentsDict, self.abbrvName def initDesignVarSet(pyM): return ((loc, compName) for compName, comp in compDict.items() for loc in comp.locationalEligibility.index if comp.locationalEligibility[loc] == 1 and comp.hasCapacityVariable) setattr(pyM, 'designDimensionVarSet_' + abbrvName, pyomo.Set(dimen=2, initialize=initDesignVarSet)) def initContinuousDesignVarSet(self, pyM): """ Declares set for continuous number of installed components in the pyomo object for a modeling class """ compDict, abbrvName = self.componentsDict, self.abbrvName def initContinuousDesignVarSet(pyM): return ((loc, compName) for loc, compName in getattr(pyM, 'designDimensionVarSet_' + abbrvName) if compDict[compName].capacityVariableDomain == 'continuous') setattr(pyM, 'continuousDesignDimensionVarSet_' + abbrvName, pyomo.Set(dimen=2, initialize=initContinuousDesignVarSet)) def initDiscreteDesignVarSet(self, pyM): """ Declares set for discrete number of installed components in the pyomo object for a modeling class """ compDict, abbrvName = self.componentsDict, self.abbrvName def initDiscreteDesignVarSet(pyM): return ((loc, compName) for loc, compName in getattr(pyM, 'designDimensionVarSet_' + abbrvName) if compDict[compName].capacityVariableDomain == 'discrete') setattr(pyM, 'discreteDesignDimensionVarSet_' + abbrvName, pyomo.Set(dimen=2, initialize=initDiscreteDesignVarSet)) def initDesignDecisionVarSet(self, pyM): """ Declares set for design decision variables in the pyomo object for a modeling class """ compDict, abbrvName = self.componentsDict, self.abbrvName def initDesignDecisionVarSet(pyM): return ((loc, compName) for loc, compName in getattr(pyM, 'designDimensionVarSet_' + abbrvName) if compDict[compName].hasIsBuiltBinaryVariable) setattr(pyM, 'designDecisionVarSet_' + abbrvName, pyomo.Set(dimen=2, initialize=initDesignDecisionVarSet)) def initOpVarSet(self, esM, pyM): """ Declares operation related sets (operation variables and mapping sets) in the pyomo object for a modeling class """ compDict, abbrvName = self.componentsDict, self.abbrvName # Set for operation variables def initOpVarSet(pyM): return ((loc, compName) for compName, comp in compDict.items() for loc in comp.locationalEligibility.index if comp.locationalEligibility[loc] == 1) setattr(pyM, 'operationVarSet_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpVarSet)) # TODO more generic formulation? if self.dimension == '1dim': # Dictionary which lists all components of the modeling class at one location setattr(pyM, 'operationVarDict_' + abbrvName, {loc: {compName for compName in compDict if (loc, compName) in getattr(pyM, 'operationVarSet_' + abbrvName)} for loc in esM.locations}) elif self.dimension == '2dim': # Dictionaries which list all outgoing and incoming components at a location setattr(pyM, 'operationVarDictOut_' + abbrvName, {loc: {loc_: {compName for compName in compDict if (loc + '_' + loc_, compName) in getattr(pyM, 'operationVarSet_' + abbrvName)} for loc_ in esM.locations} for loc in esM.locations}) setattr(pyM, 'operationVarDictIn_' + abbrvName, {loc: {loc_: {compName for compName in compDict if (loc_ + '_' + loc, compName) in getattr(pyM, 'operationVarSet_' + abbrvName)} for loc_ in esM.locations} for loc in esM.locations}) #################################################################################################################### # Functions for declaring operation mode sets # #################################################################################################################### def declareOperationModeSets(self, pyM, constrSetName, rateMax, rateFix): compDict, abbrvName = self.componentsDict, self.abbrvName varSet = getattr(pyM, 'operationVarSet_' + abbrvName) def initOpConstrSet1(pyM): return ((loc, compName) for loc, compName in varSet if compDict[compName].hasCapacityVariable and getattr(compDict[compName], rateMax) is None and getattr(compDict[compName], rateFix) is None) setattr(pyM, constrSetName + '1_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpConstrSet1)) def initOpConstrSet2(pyM): return ((loc, compName) for loc, compName in varSet if compDict[compName].hasCapacityVariable and getattr(compDict[compName], rateFix) is not None) setattr(pyM, constrSetName + '2_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpConstrSet2)) def initOpConstrSet3(pyM): return ((loc, compName) for loc, compName in varSet if compDict[compName].hasCapacityVariable and getattr(compDict[compName], rateMax) is not None) setattr(pyM, constrSetName + '3_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpConstrSet3)) def initOpConstrSet4(pyM): return ((loc, compName) for loc, compName in varSet if not compDict[compName].hasCapacityVariable and getattr(compDict[compName], rateFix) is not None) setattr(pyM, constrSetName + '4_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpConstrSet4)) def initOpConstrSet5(pyM): return ((loc, compName) for loc, compName in varSet if not compDict[compName].hasCapacityVariable and getattr(compDict[compName], rateMax) is not None) setattr(pyM, constrSetName + '5_' + abbrvName, pyomo.Set(dimen=2, initialize=initOpConstrSet5)) #################################################################################################################### # Functions for declaring variables # #################################################################################################################### def declareCapacityVars(self, pyM): """ Declares capacity variables """ abbrvName = self.abbrvName def capBounds(pyM, loc, compName): """ Function for setting lower and upper capacity bounds """ comp = self.componentsDict[compName] return (comp.capacityMin[loc] if (comp.capacityMin is not None and not comp.hasIsBuiltBinaryVariable) else 0, comp.capacityMax[loc] if comp.capacityMax is not None else None) setattr(pyM, 'cap_' + abbrvName, pyomo.Var(getattr(pyM, 'designDimensionVarSet_' + abbrvName), domain=pyomo.NonNegativeReals, bounds=capBounds)) def declareRealNumbersVars(self, pyM): """ Declares variables representing the (continuous) number of installed components [-] """ abbrvName = self.abbrvName setattr(pyM, 'nbReal_' + abbrvName, pyomo.Var(getattr(pyM, 'continuousDesignDimensionVarSet_' + abbrvName), domain=pyomo.NonNegativeReals)) def declareIntNumbersVars(self, pyM): """ Declares variables representing the (discrete/integer) number of installed components [-] """ abbrvName = self.abbrvName setattr(pyM, 'nbInt_' + abbrvName, pyomo.Var(getattr(pyM, 'discreteDesignDimensionVarSet_' + abbrvName), domain=pyomo.NonNegativeIntegers)) def declareBinaryDesignDecisionVars(self, pyM): """ Declares binary variables [-] indicating if a component is considered at a location or not [-] """ abbrvName = self.abbrvName setattr(pyM, 'designBin_' + abbrvName, pyomo.Var(getattr(pyM, 'designDecisionVarSet_' + abbrvName), domain=pyomo.Binary)) def declareOperationVars(self, pyM, opVarName): """ Declares operation variables """ abbrvName = self.abbrvName setattr(pyM, opVarName + '_' + abbrvName, pyomo.Var(getattr(pyM, 'operationVarSet_' + abbrvName), pyM.timeSet, domain=pyomo.NonNegativeReals)) #################################################################################################################### # Functions for declaring time independent constraints # #################################################################################################################### def capToNbReal(self, pyM): """ Determine the components' capacities from the number of installed units """ compDict, abbrvName = self.componentsDict, self.abbrvName capVar, nbRealVar = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'nbReal_' + abbrvName) nbRealVarSet = getattr(pyM, 'continuousDesignDimensionVarSet_' + abbrvName) def capToNbReal(pyM, loc, compName): return capVar[loc, compName] == nbRealVar[loc, compName] * compDict[compName].capacityPerPlantUnit setattr(pyM, 'ConstrCapToNbReal_' + abbrvName, pyomo.Constraint(nbRealVarSet, rule=capToNbReal)) def capToNbInt(self, pyM): """ Determine the components' capacities from the number of installed units """ compDict, abbrvName = self.componentsDict, self.abbrvName capVar, nbIntVar = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'nbInt_' + abbrvName) nbIntVarSet = getattr(pyM, 'discreteDesignDimensionVarSet_' + abbrvName) def capToNbInt(pyM, loc, compName): return capVar[loc, compName] == nbIntVar[loc, compName] * compDict[compName].capacityPerPlantUnit setattr(pyM, 'ConstrCapToNbInt_' + abbrvName, pyomo.Constraint(nbIntVarSet, rule=capToNbInt)) def bigM(self, pyM): """ Enforce the consideration of the binary design variables of a component """ compDict, abbrvName = self.componentsDict, self.abbrvName capVar, designBinVar = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'designBin_' + abbrvName) designBinVarSet = getattr(pyM, 'designDecisionVarSet_' + abbrvName) def bigM(pyM, loc, compName): return capVar[loc, compName] <= designBinVar[loc, compName] * compDict[compName].bigM setattr(pyM, 'ConstrBigM_' + abbrvName, pyomo.Constraint(designBinVarSet, rule=bigM)) def capacityMinDec(self, pyM): """ Enforce the consideration of minimum capacities for components with design decision variables """ compDict, abbrvName, dim = self.componentsDict, self.abbrvName, self.dimension capVar, designBinVar = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'designBin_' + abbrvName) designBinVarSet = getattr(pyM, 'designDecisionVarSet_' + abbrvName) def capacityMinDec(pyM, loc, compName): return (capVar[loc, compName] >= compDict[compName].capacityMin[loc] * designBinVar[loc, compName] if compDict[compName].capacityMin is not None else pyomo.Constraint.Skip) setattr(pyM, 'ConstrCapacityMinDec_' + abbrvName, pyomo.Constraint(designBinVarSet, rule=capacityMinDec)) def capacityFix(self, pyM): """ Sets, if applicable, the installed capacities of a component """ compDict, abbrvName, dim = self.componentsDict, self.abbrvName, self.dimension capVar = getattr(pyM, 'cap_' + abbrvName) capVarSet = getattr(pyM, 'designDimensionVarSet_' + abbrvName) def capacityFix(pyM, loc, compName): return (capVar[loc, compName] == compDict[compName].capacityFix[loc] if compDict[compName].capacityFix is not None else pyomo.Constraint.Skip) setattr(pyM, 'ConstrCapacityFix_' + abbrvName, pyomo.Constraint(capVarSet, rule=capacityFix)) def designBinFix(self, pyM): """ Sets, if applicable, the installed capacities of a component """ compDict, abbrvName, dim = self.componentsDict, self.abbrvName, self.dimension designBinVar = getattr(pyM, 'designBin_' + abbrvName) designBinVarSet = getattr(pyM, 'designDecisionVarSet_' + abbrvName) def designBinFix(pyM, loc, compName): return (designBinVar[loc, compName] == compDict[compName].isBuiltFix[loc] if compDict[compName].isBuiltFix is not None else pyomo.Constraint.Skip) setattr(pyM, 'ConstrDesignBinFix_' + abbrvName, pyomo.Constraint(designBinVarSet, rule=designBinFix)) #################################################################################################################### # Functions for declaring time dependent constraints # #################################################################################################################### def operationMode1(self, pyM, esM, constrName, constrSetName, opVarName, factorName=None, isStateOfCharge=False): """ Defines operation modes """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet1 = getattr(pyM, constrSetName + '1_' + abbrvName) factor1 = 1 if isStateOfCharge else esM.hoursPerTimeStep # Operation [energyUnit] limited by the installed capacity [powerUnit] multiplied by the hours per time step def op1(pyM, loc, compName, p, t): factor2 = 1 if factorName is None else getattr(compDict[compName], factorName) return opVar[loc, compName, p, t] <= factor1 * factor2 * capVar[loc, compName] setattr(pyM, constrName + '1_' + abbrvName, pyomo.Constraint(constrSet1, pyM.timeSet, rule=op1)) def operationMode2(self, pyM, esM, constrName, constrSetName, opVarName, isStateOfCharge=False): """ Defines operation modes """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet2 = getattr(pyM, constrSetName + '2_' + abbrvName) factor = 1 if isStateOfCharge else esM.hoursPerTimeStep # Operation [energyUnit] equal to the installed capacity [powerUnit] multiplied by operation time series # [powerUnit/powerUnit] and the hours per time step [h]) def op2(pyM, loc, compName, p, t): return opVar[loc, compName, p, t] == capVar[loc, compName] * \ compDict[compName].operationRateFix[loc][p, t] * factor setattr(pyM, constrName + '2_' + abbrvName, pyomo.Constraint(constrSet2, pyM.timeSet, rule=op2)) def operationMode3(self, pyM, esM, constrName, constrSetName, opVarName, isStateOfCharge=False): """ Defines operation modes """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet3 = getattr(pyM, constrSetName + '3_' + abbrvName) factor = 1 if isStateOfCharge else esM.hoursPerTimeStep # Operation [energyUnit] limited by the installed capacity [powerUnit] multiplied by operation time series # [powerUnit/powerUnit] and the hours per time step [h]) def op3(pyM, loc, compName, p, t): return opVar[loc, compName, p, t] <= capVar[loc, compName] * \ compDict[compName].operationRateMax[loc][p, t] * factor setattr(pyM, constrName + '3_' + abbrvName, pyomo.Constraint(constrSet3, pyM.timeSet, rule=op3)) def operationMode4(self, pyM, esM, constrName, constrSetName, opVarName): """ Defines operation modes """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet4 = getattr(pyM, constrSetName + '4_' + abbrvName) # Operation [energyUnit] equal to the operation time series [energyUnit] def op4(pyM, loc, compName, p, t): return opVar[loc, compName, p, t] == compDict[compName].operationRateFix[loc][p, t] setattr(pyM, constrName + '4_' + abbrvName, pyomo.Constraint(constrSet4, pyM.timeSet, rule=op4)) def operationMode5(self, pyM, esM, constrName, constrSetName, opVarName): """ Defines operation modes """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet5 = getattr(pyM, constrSetName + '5_' + abbrvName) # Operation [energyUnit] limited by the operation time series [energyUnit] def op5(pyM, loc, compName, p, t): return opVar[loc, compName, p, t] <= compDict[compName].operationRateMax[loc][p, t] setattr(pyM, constrName + '5_' + abbrvName, pyomo.Constraint(constrSet5, pyM.timeSet, rule=op5)) #################################################################################################################### # Functions for declaring component contributions to basic energy system constraints and the objective function # #################################################################################################################### def getSharedPotentialContribution(self, pyM, key, loc): """ Gets the share which the components of the modeling class have on a shared maximum potential at a location """ compDict, abbrvName = self.componentsDict, self.abbrvName capVar = getattr(pyM, 'cap_' + abbrvName) capVarSet = getattr(pyM, 'designDimensionVarSet_' + abbrvName) return sum(capVar[loc, compName] / compDict[compName].capacityMax[loc] for compName in compDict if compDict[compName].sharedPotentialID == key and (loc, compName) in capVarSet) def getLocEconomicsTD(self, pyM, esM, factorNames, varName, loc, compName, getOptValue=False): var = getattr(pyM, varName + '_' + self.abbrvName) factors = [getattr(self.componentsDict[compName], factorName)[loc] for factorName in factorNames] factor = 1. for factor_ in factors: factor *= factor_ if not getOptValue: return (factor * sum(var[loc, compName, p, t] * esM.periodOccurrences[p] for p, t in pyM.timeSet)/esM.numberOfYears) else: return (factor * sum(var[loc, compName, p, t].value * esM.periodOccurrences[p] for p, t in pyM.timeSet)/esM.numberOfYears) def getLocEconomicsTI(self, pyM, factorNames, varName, loc, compName, divisorName='', getOptValue=False): var = getattr(pyM, varName + '_' + self.abbrvName) factors = [getattr(self.componentsDict[compName], factorName)[loc] for factorName in factorNames] divisor = getattr(self.componentsDict[compName], divisorName)[loc] if not divisorName == '' else 1 factor = 1./divisor for factor_ in factors: factor *= factor_ if not getOptValue: return factor * var[loc, compName] else: return factor * var[loc, compName].value def getEconomicsTI(self, pyM, factorNames, varName, divisorName='', getOptValue=False): var = getattr(pyM, varName + '_' + self.abbrvName) return sum(self.getLocEconomicsTI(pyM, factorNames, varName, loc, compName, divisorName, getOptValue) for loc, compName in var) def getEconomicsTD(self, pyM, esM, factorNames, varName, dictName, getOptValue=False): indices = getattr(pyM, dictName + '_' + self.abbrvName).items() if self.dimension == '1dim': return sum(self.getLocEconomicsTD(pyM, esM, factorNames, varName, loc, compName, getOptValue) for loc, compNames in indices for compName in compNames) else: return sum(self.getLocEconomicsTD(pyM, esM, factorNames, varName, loc + '_' + loc_, compName, getOptValue) for loc, subDict in indices for loc_, compNames in subDict.items() for compName in compNames) def setOptimalValues(self, esM, pyM, indexColumns, plantUnit, unitApp='', costApp=1): compDict, abbrvName = self.componentsDict, self.abbrvName capVar = getattr(esM.pyM, 'cap_' + abbrvName) binVar = getattr(esM.pyM, 'designBin_' + abbrvName) props = ['capacity', 'isBuilt', 'capexCap', 'capexIfBuilt', 'opexCap', 'opexIfBuilt', 'TAC', 'invest'] units = ['[-]', '[-]', '[' + esM.costUnit + '/a]', '[' + esM.costUnit + '/a]', '[' + esM.costUnit + '/a]', '[' + esM.costUnit + '/a]', '[' + esM.costUnit + '/a]', '[' + esM.costUnit + ']'] 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], '[' + getattr(compDict[x[0]], plantUnit) + unitApp + ']') if x[1] == 'capacity' else x, tuples)) mIndex = pd.MultiIndex.from_tuples(tuples, names=['Component', 'Property', 'Unit']) optSummary = pd.DataFrame(index=mIndex, columns=sorted(indexColumns)).sort_index() # Get and set optimal variable values and contributions to the total annual cost and invest values = capVar.get_values() optVal = utils.formatOptimizationOutput(values, 'designVariables', '1dim') optVal_ = utils.formatOptimizationOutput(values, 'designVariables', self.dimension, compDict=compDict) self.capacityVariablesOptimum = optVal_ # Check if the installed capacities are close to a bigM value for components with design decision variables for compName, comp in compDict.items(): if comp.hasIsBuiltBinaryVariable and optVal.loc[compName].max().max() >= comp.bigM * 0.9 \ and esM.verbose < 2: warnings.warn('the capacity of component ' + compName + ' is in one or more locations close or equal ' 'to the chosen Big M. Consider rerunning the simulation with a higher Big M.') if optVal is not None: i = optVal.apply(lambda cap: cap * compDict[cap.name].investPerCapacity[cap.index], axis=1) cx = optVal.apply(lambda cap: cap * compDict[cap.name].investPerCapacity[cap.index] / compDict[cap.name].CCF[cap.index], axis=1) ox = optVal.apply(lambda cap: cap * compDict[cap.name].opexPerCapacity[cap.index], axis=1) optSummary.loc[ [(ix, 'capacity', '[' + getattr(compDict[ix], plantUnit) + unitApp + ']') for ix in optVal.index], optVal.columns] = optVal.values optSummary.loc[[(ix, 'invest', '[' + esM.costUnit + ']') for ix in i.index], i.columns] = \ i.values * costApp optSummary.loc[[(ix, 'capexCap', '[' + esM.costUnit + '/a]') for ix in cx.index], cx.columns] = \ cx.values * costApp optSummary.loc[[(ix, 'opexCap', '[' + esM.costUnit + '/a]') for ix in ox.index], ox.columns] = \ ox.values * costApp values = binVar.get_values() optVal = utils.formatOptimizationOutput(values, 'designVariables', '1dim') optVal_ = utils.formatOptimizationOutput(values, 'designVariables', self.dimension, compDict=compDict) self.isBuiltVariablesOptimum = optVal_ if optVal is not None: i = optVal.apply(lambda dec: dec * compDict[dec.name].investIfBuilt[dec.index], axis=1) cx = optVal.apply(lambda dec: dec * compDict[dec.name].investIfBuilt[dec.index] / compDict[dec.name].CCF[dec.index], axis=1) ox = optVal.apply(lambda dec: dec * compDict[dec.name].opexIfBuilt[dec.index], axis=1) optSummary.loc[[(ix, 'isBuilt', '[-]') for ix in optVal.index], optVal.columns] = optVal.values optSummary.loc[[(ix, 'invest', '[' + esM.costUnit + ']') for ix in cx.index], cx.columns] += \ i.values * costApp optSummary.loc[[(ix, 'capexIfBuilt', '[' + esM.costUnit + '/a]') for ix in cx.index], cx.columns] = cx.values * costApp optSummary.loc[[(ix, 'opexIfBuilt', '[' + esM.costUnit + '/a]') for ix in ox.index], ox.columns] = ox.values * costApp # 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) == 'capexCap') | (optSummary.index.get_level_values(1) == 'opexCap') | (optSummary.index.get_level_values(1) == 'capexIfBuilt')].groupby(level=0).sum().values return optSummary def getOptimalValues(self): return {'capacityVariables': {'values': self.capacityVariablesOptimum, 'timeDependent': False, 'dimension': self.dimension}, 'isBuiltVariables': {'values': self.isBuiltVariablesOptimum, 'timeDependent': False, 'dimension': self.dimension}, 'operationVariablesOptimum': {'values': self.operationVariablesOptimum, 'timeDependent': True, 'dimension': self.dimension}} @abstractmethod def declareVariables(self, esM, pyM): raise NotImplementedError @abstractmethod def declareComponentConstraints(self, esM, pyM): raise NotImplementedError @abstractmethod def hasOpVariablesForLocationCommodity(self, esM, loc, commod): raise NotImplementedError @abstractmethod def getCommodityBalanceContribution(self, pyM, commod, loc, p, t): raise NotImplementedError @abstractmethod def getObjectiveFunctionContribution(self, esM, pyM): raise NotImplementedError