Source code for transmission

from FINE.component import Component, ComponentModel
from FINE import utils
import warnings
import pyomo.environ as pyomo
import pandas as pd


[docs]class Transmission(Component): """ Doc """
[docs] def __init__(self, esM, name, commodity, losses=0, distances=None, hasCapacityVariable=True, capacityVariableDomain='continuous', capacityPerPlantUnit=1, 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): """ Constructor for creating an Conversion class instance. The Transmission component specific input arguments are described below. The general component input arguments are described in the Component class. **Required arguments:** :param commodity: to the component related commodity. :type commodity: string **Default arguments:** :param losses: losses per lengthUnit (lengthUnit as specified in the energy system model). This loss factor can capture simple linear losses trans_in_ij=(1-losses*distance)*trans_out_ij (with trans being the commodity flow at a certain point in time and i and j being locations in the energy system). The losses can either be given as a float or a Pandas DataFrame with location specific values. |br| * the default value is 0 :type losses: positive float (0 <= float <= 1) or Pandas DataFrame with positive values (0 <= float <= 1). The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param distances: distances between locations, given in the lengthUnit (lengthUnit as specified in the energy system model). |br| * the default value is None :type distances: positive float (>= 0) or Pandas DataFrame with positive values (>= 0). The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :param operationRateMax: if specified, indicates a maximum operation rate for all possible connections (both directions) of the transmission component at 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 commodityUnit, referring to the transmitted commodity (before considering losses) during one 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 are combinations of locations (as defined in the energy system model), separated by a underscore (i.e. "location1_location2"). The first location indicates where the commodity is coming from. The second one location indicates where the commodity is going too. If a flow is specified from location i to location j, it also has to be specified from j to i. :param operationRateFix: if specified, indicates a fixed operation rate for all possible connections (both directions) of the transmission component at 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 commodityUnit, referring to the transmitted commodity (before considering losses) during one 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 are combinations of locations (as defined in the energy system model), separated by a underscore (i.e. "location1_location2"). The first location indicates where the commodity is coming from. The second one location indicates where the commodity is going too. If a flow is specified from location i to location j, it also has to be specified from j to i. :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 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 (i.e. Euro, Dollar, 1e6 Euro). |br| * the default value is 0 :type opexPerOperation: positive (>=0) float 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. """ # TODO add unit checks # Preprocess two-dimensional data self.locationalEligibility = utils.preprocess2dimData(locationalEligibility) self.capacityMax = utils.preprocess2dimData(capacityMax) self.capacityFix = utils.preprocess2dimData(capacityFix) self.isBuiltFix = utils.preprocess2dimData(isBuiltFix) # 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, hasCapacityVariable, operationTimeSeries, '2dim') self._mapC, self._mapL, self._mapI = {}, {}, {} for loc1 in esM.locations: for loc2 in esM.locations: if loc1 + '_' + loc2 in self.locationalEligibility.index: if self.locationalEligibility[loc1 + '_' + loc2] == 0: self.locationalEligibility[loc1 + '_' + loc2].drop(inplace=True) self._mapC.update({loc1 + '_' + loc2: (loc1, loc2)}) self._mapL.setdefault(loc1, {}).update({loc2: loc1 + '_' + loc2}) self._mapI.update({loc1 + '_' + loc2: loc2 + '_' + loc1}) self.capacityMin = utils.preprocess2dimData(capacityMin, self._mapC) self.investPerCapacity = utils.preprocess2dimData(investPerCapacity, self._mapC) self.investIfBuilt = utils.preprocess2dimData(investIfBuilt, self._mapC) self.opexPerCapacity = utils.preprocess2dimData(opexPerCapacity, self._mapC) self.opexIfBuilt = utils.preprocess2dimData(opexIfBuilt, self._mapC) self.interestRate = utils.preprocess2dimData(interestRate, self._mapC) self.economicLifetime = utils.preprocess2dimData(economicLifetime, self._mapC) Component. __init__(self, esM, name, dimension='2dim', hasCapacityVariable=hasCapacityVariable, capacityVariableDomain=capacityVariableDomain, capacityPerPlantUnit=capacityPerPlantUnit, hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable, bigM=bigM, locationalEligibility=self.locationalEligibility, capacityMin=self.capacityMin, capacityMax=self.capacityMax, sharedPotentialID=sharedPotentialID, capacityFix=self.capacityFix, isBuiltFix=self.isBuiltFix, investPerCapacity=self.investPerCapacity, investIfBuilt=self.investIfBuilt, opexPerCapacity=self.opexPerCapacity, opexIfBuilt=self.opexIfBuilt, interestRate=self.interestRate, economicLifetime=self.economicLifetime) # Set general component data utils.checkCommodities(esM, {commodity}) self.commodity, self.commodityUnit = commodity, esM.commodityUnitsDict[commodity] self.distances = utils.preprocess2dimData(distances, self._mapC) self.losses = utils.preprocess2dimData(losses, self._mapC) self.distances = utils.checkAndSetDistances(self.distances, self.locationalEligibility, esM) self.losses = utils.checkAndSetTransmissionLosses(self.losses, self.distances, self.locationalEligibility) self.modelingClass = TransmissionModel # Set additional economic data self.opexPerOperation = utils.checkAndSetCostParameter(esM, name, opexPerOperation, '2dim', self.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, self.locationalEligibility, '2dim') utils.checkOperationTimeSeriesInputParameters(esM, operationRateFix, self.locationalEligibility, '2dim') self.fullOperationRateMax = utils.setFormattedTimeSeries(operationRateMax) self.aggregatedOperationRateMax = None self.operationRateMax = utils.setFormattedTimeSeries(operationRateMax) self.fullOperationRateFix = utils.setFormattedTimeSeries(operationRateFix) self.aggregatedOperationRateFix = None self.operationRateFix = utils.setFormattedTimeSeries(operationRateFix) utils.isPositiveNumber(tsaWeight) self.tsaWeight = tsaWeight
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 TransmissionModel(ComponentModel): """ Doc """ def __init__(self): self.abbrvName = 'trans' self.dimension = '2dim' self.componentsDict = {} self.capacityVariablesOptimum, self.isBuiltVariablesOptimum = None, None self.operationVariablesOptimum = None self.optSummary = None #################################################################################################################### # Declare sparse index sets # #################################################################################################################### 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 variables # #################################################################################################################### def declareVariables(self, esM, pyM): """ Declares design and operation variables """ # Capacity variables in [commodityUnit] 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 [commodityUnit] self.declareOperationVars(pyM, 'op') #################################################################################################################### # Declare component constraints # #################################################################################################################### def symmetricalCapacity(self, pyM): """ Enforces that the capacity between location_1 and location_2 is the same as the one between location_2 and location_1 """ compDict, abbrvName = self.componentsDict, self.abbrvName capVar, capVarSet = getattr(pyM, 'cap_' + abbrvName), getattr(pyM, 'designDimensionVarSet_' + abbrvName) def symmetricalCapacity(pyM, loc, compName): return capVar[loc, compName] == capVar[compDict[compName]._mapI[loc], compName] setattr(pyM, 'ConstrSymmetricalCapacity_' + abbrvName, pyomo.Constraint(capVarSet, rule=symmetricalCapacity)) def operationMode1_2dim(self, pyM, esM, constrName, constrSetName, opVarName): """ Operation [commodityUnit*hour] limited by the installed capacity [commodityUnit] multiplied by the hours per time step. Since the flow should either go in one direction or the other, the limitation can be enforced on the sum of the forward and backward flow over the line. This leads to one of the flow variables being set to zero if a basic solution is obtained during optimization. """ compDict, abbrvName = self.componentsDict, self.abbrvName opVar, capVar = getattr(pyM, opVarName + '_' + abbrvName), getattr(pyM, 'cap_' + abbrvName) constrSet1 = getattr(pyM, constrSetName + '1_' + abbrvName) def op1(pyM, loc, compName, p, t): return opVar[loc, compName, p, t] + opVar[compDict[compName]._mapI[loc], compName, p, t] <= \ capVar[loc, compName] * esM.hoursPerTimeStep setattr(pyM, constrName + '_' + abbrvName, pyomo.Constraint(constrSet1, pyM.timeSet, rule=op1)) 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) # Enforce that the capacity cap_loc1_loc2 is the same as cap_loc2_loc1 self.symmetricalCapacity(pyM) ################################################################################################################ # Declare time dependent constraints # ################################################################################################################ # Operation [energyUnit] limited by the installed capacity [powerUnit] multiplied by the hours per time step self.operationMode1_2dim(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([comp.commodity == commod and (loc + '_' + loc_ in comp.locationalEligibility.index or loc_ + '_' + loc in comp.locationalEligibility.index) for comp in self.componentsDict.values() for loc_ in esM.locations]) def getCommodityBalanceContribution(self, pyM, commod, loc, p, t): compDict, abbrvName = self.componentsDict, self.abbrvName opVar, opVarDictIn = getattr(pyM, 'op_' + abbrvName), getattr(pyM, 'operationVarDictIn_' + abbrvName) opVarDictOut = getattr(pyM, 'operationVarDictOut_' + abbrvName) return sum(opVar[loc_ + '_' + loc, compName, p, t] * (1 - compDict[compName].losses[loc_ + '_' + loc] * compDict[compName].distances[loc_ + '_' + loc]) for loc_ in opVarDictIn[loc].keys() for compName in opVarDictIn[loc][loc_] if commod in compDict[compName].commodity) - \ sum(opVar[loc + '_' + loc_, compName, p, t] for loc_ in opVarDictOut[loc].keys() for compName in opVarDictOut[loc][loc_] if commod in compDict[compName].commodity) def getObjectiveFunctionContribution(self, esM, pyM): capexCap = self.getEconomicsTI(pyM, ['investPerCapacity', 'distances'], 'cap', 'CCF') * 0.5 capexDec = self.getEconomicsTI(pyM, ['investIfBuilt', 'distances'], 'designBin', 'CCF') * 0.5 opexCap = self.getEconomicsTI(pyM, ['opexPerCapacity', 'distances'], 'cap') * 0.5 opexDec = self.getEconomicsTI(pyM, ['opexIfBuilt', 'distances'], 'designBin') * 0.5 opexOp = self.getEconomicsTD(pyM, esM, ['opexPerOperation'], 'op', 'operationVarDictOut') return capexCap + capexDec + opexCap + opexDec + opexOp def setOptimalValues(self, esM, pyM): compDict, abbrvName = self.componentsDict, self.abbrvName opVar = getattr(pyM, 'op_' + abbrvName) mapC = {loc1 + '_' + loc2: (loc1, loc2) for loc1 in esM.locations for loc2 in esM.locations} # Set optimal design dimension variables and get basic optimization summary optSummaryBasic = super().setOptimalValues(esM, pyM, mapC.keys(), 'commodityUnit', costApp=0.5) # Set optimal operation variables and append optimization summary optVal = utils.formatOptimizationOutput(opVar.get_values(), 'operationVariables', '1dim', esM.periodsOrder) optVal_ = utils.formatOptimizationOutput(opVar.get_values(), 'operationVariables', '2dim', esM.periodsOrder, compDict=compDict) self.operationVariablesOptimum = optVal_ props = ['operation', 'opexOp'] units = ['[-]', '[' + esM.costUnit + '/a]', '[' + 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]].commodityUnit + '*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(mapC.keys())).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].commodityUnit + '*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 * 0.5 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 # Split connection indices to two location indices optSummary = optSummary.stack() indexNew = [] for tup in optSummary.index.tolist(): loc1, loc2 = mapC[tup[3]] indexNew.append((tup[0], tup[1], tup[2], loc1, loc2)) optSummary.index = pd.MultiIndex.from_tuples(indexNew) optSummary = optSummary.unstack(level=-1) self.optSummary = optSummary def getOptimalValues(self): return super().getOptimalValues()