def run_esM_without_DSM(): esM_without, load_without_dsm, _, _, _, _ = dsm_test_esM() esM_without.add( fn.Sink(esM=esM_without, name='load', commodity='electricity', hasCapacityVariable=False, operationRateFix=load_without_dsm)) esM_without.optimize(timeSeriesAggregation=False, solver='gurobi') # without dsm fig, ax = fn.plotOperation(esM_without, 'cheap', 'location', figsize=(4, 2), fontsize=10) ax.set_title('Cheap generator') fig, ax = fn.plotOperation(esM_without, 'expensive', 'location', figsize=(4, 2), fontsize=10) ax.set_title('Expensive generator') fig, ax = fn.plotOperation(esM_without, 'load', 'location', figsize=(4, 2), fontsize=10) ax.set_title('Load') plt.show()
def test_shadowCostOutPut(minimal_test_esM): ''' Get the minimal test system, and check if the fulllload hours of electrolyzer are above 4000. ''' esM = minimal_test_esM esM.optimize(solver='glpk') SP = fn.getShadowPrices(esM.pyM, esM.pyM.ConstrOperation4_srcSnk, dualValues=None, hasTimeSeries=True, periodOccurrences=esM.periodOccurrences, periodsOrder=esM.periodsOrder) assert np.round(SP.sum(), 4) == 0.2955 esM.cluster(numberOfTypicalPeriods=2, numberOfTimeStepsPerPeriod=1) esM.optimize(timeSeriesAggregation=True, solver='glpk') SP = fn.getShadowPrices(esM.pyM, esM.pyM.ConstrOperation4_srcSnk, dualValues=None, hasTimeSeries=True, periodOccurrences=esM.periodOccurrences, periodsOrder=esM.periodsOrder) assert np.round(SP.sum(), 4) == 0.3296 assert len(SP) == 4
def test_initializeTransmission(): """ Tests if Transmission components are initialized without error if just required parameters are given. """ # Define general parameters for esM-instance locations = ["cluster_1", "cluster_2", "cluster_3", "cluster_4"] commodityUnitDict = {"commodity1": "commodity_unit"} commodities = {"commodity1"} # Initialize esM-instance esM = fn.EnergySystemModel( locations=set(locations), commodities=commodities, numberOfTimeSteps=4, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="cost_unit", lengthUnit="length_unit", ) # Initialize Transmission esM.add( fn.Transmission( esM=esM, name="Transmission_1", commodity="commodity1", hasCapacityVariable=True, ))
def test_rampUpMax(): # read in original results results = [8.0, 10.0, 0.0, 0.0, 4.0, 5.0, 5.0, 0.0, 0.0, 4.0, 8.0, 5.0, 4.0, 0.0, 4.0 , 5.0, 5.0, 0.0, 0.0, 4.0] # 2. Create an energy system model instance locations = {'example_region1', 'example_region2'} commodityUnitDict = {'electricity': r'GW$_{el}$', 'methane': r'GW$_{CH_{4},LHV}$'} commodities = {'electricity', 'methane'} esM = fn.EnergySystemModel(locations=locations, commodities=commodities, numberOfTimeSteps=20, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit='1e9 Euro', lengthUnit='km', verboseLogLevel=0) data_cost = {'example_region1': [10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10], 'example_region2': [10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]} data_cost_df = pd.DataFrame(data=data_cost) esM.add(fn.Source(esM=esM, name='Natural gas purchase', commodity='methane', hasCapacityVariable=False, commodityCostTimeSeries= data_cost_df)) # 4. Add conversion components to the energy system model ### Combined cycle gas turbine plants data_cap =pd.Series(index=['example_region1','example_region2' ], data=[10,10]) esM.add(fn.ConversionDynamic(esM=esM, name='unrestricted', physicalUnit=r'GW$_{el}$', commodityConversionFactors={'electricity':1, 'methane':-1/0.625}, capacityFix = data_cap, partLoadMin = 0.1, bigM =100, investPerCapacity=0.65, opexPerCapacity=0.021, opexPerOperation =10, interestRate=0.08, economicLifetime=33)) esM.add(fn.ConversionDynamic(esM=esM, name='restricted', physicalUnit=r'GW$_{el}$', commodityConversionFactors={'electricity':1, 'methane':-1/0.625}, capacityFix = data_cap, partLoadMin = 0.3, bigM= 100, rampUpMax=0.4, investPerCapacity=0.5, opexPerCapacity=0.021, opexPerOperation =1, interestRate=0.08, economicLifetime=33)) data_demand = {'example_region1': [10,10,2,2,4,5,5,2,2,10,10,5,4,2,4,5,5,2,2,4], 'example_region2': [10,10,2,2,4,5,5,2,2,10,10,5,4,2,4,5,5,2,2,4]} data_demand_df = pd.DataFrame(data= data_demand) esM.add(fn.Sink(esM=esM, name='Electricity demand', commodity='electricity', hasCapacityVariable=False, operationRateFix=data_demand_df)) esM.optimize(timeSeriesAggregation=False, solver = 'glpk') print('restricted dispatch:\n') print(esM.componentModelingDict['ConversionDynamicModel'].operationVariablesOptimum.xs('restricted')) print('unrestricted dispatch:\n') print(esM.componentModelingDict['ConversionDynamicModel'].operationVariablesOptimum.xs('unrestricted')) # print(esM.componentModelingDict['ConversionDynamicModel'].operationVariablesOptimum.xs('unrestricted')) # test if here solved fits with original results # test if here solved fits with original results testresults = esM.componentModelingDict["ConversionDynamicModel"].operationVariablesOptimum.xs('restricted') np.testing.assert_array_almost_equal(testresults.values[0], results,decimal=2) np.testing.assert_array_almost_equal(testresults.values[1], results,decimal=2)
def run_esM_without_DSM(): esM_without, load_without_dsm, _, _, _, _ = dsm_test_esM() esM_without.add( fn.Sink( esM=esM_without, name="load", commodity="electricity", hasCapacityVariable=False, operationRateFix=load_without_dsm, ) ) esM_without.optimize(timeSeriesAggregation=False) # without dsm fig, ax = fn.plotOperation( esM_without, "cheap", "location", figsize=(4, 2), fontsize=10 ) ax.set_title("Cheap generator") fig, ax = fn.plotOperation( esM_without, "expensive", "location", figsize=(4, 2), fontsize=10 ) ax.set_title("Expensive generator") fig, ax = fn.plotOperation( esM_without, "load", "location", figsize=(4, 2), fontsize=10 ) ax.set_title("Load") plt.show()
def test_ConversionDynamicNeedsCapacity(): esM = fn.EnergySystemModel( locations={ "example_region1", }, commodities={"electricity", "methane"}, commodityUnitsDict={ "electricity": r"GW$_{el}$", "methane": r"GW$_{th}$" }, verboseLogLevel=2, ) with pytest.raises(ValueError, match=r".*hasCapacityVariable.*"): fn.ConversionDynamic( esM=esM, name="restricted", physicalUnit=r"GW$_{el}$", commodityConversionFactors={ "electricity": 1, "methane": -1 / 0.625 }, partLoadMin=0.3, bigM=100, rampDownMax=0.5, investPerCapacity=0.5, opexPerCapacity=0.021, opexPerOperation=1, interestRate=0.08, economicLifetime=33, hasCapacityVariable=False, )
def dsm_test_esM(): """ Generate a simple energy system model with one node, two fixed generators and one load time series for testing demand side management functionality. """ # load without dsm now = pd.Timestamp.now().round('h') number_of_time_steps = 24 #t_index = pd.date_range(now, now + pd.DateOffset(hours=number_of_timeSteps - 1), freq='h') t_index = range(number_of_time_steps) load_without_dsm = pd.Series([80.] * number_of_time_steps, index=t_index) timestep_up = 10 timestep_down = 20 load_without_dsm[timestep_up:timestep_down] += 40. time_shift = 3 cheap_capacity = 100. expensive_capacity = 20. # set up energy model esM = fn.EnergySystemModel( locations={'location'}, commodities={'electricity'}, numberOfTimeSteps=number_of_time_steps, commodityUnitsDict={'electricity': r'MW$_{el}$'}, hoursPerTimeStep=1, costUnit='1 Euro', lengthUnit='km', verboseLogLevel=1) esM.add( fn.Source(esM=esM, name='cheap', commodity='electricity', hasCapacityVariable=False, operationRateMax=pd.Series(cheap_capacity, index=t_index), opexPerOperation=25)) esM.add( fn.Source(esM=esM, name='expensive', commodity='electricity', hasCapacityVariable=False, operationRateMax=pd.Series(expensive_capacity, index=t_index), opexPerOperation=50)) esM.add( fn.Source(esM=esM, name='back-up', commodity='electricity', hasCapacityVariable=False, operationRateMax=pd.Series(1000, index=t_index), opexPerOperation=1000)) return esM, load_without_dsm, timestep_up, timestep_down, time_shift, cheap_capacity
def test_ConversionDynamicNeedsHigherOperationRate(): numberOfTimeSteps = 4 locations = {"ElectrolyzerLocation", "IndustryLocation"} esM = fn.EnergySystemModel( locations=locations, commodities={"electricity", "methane"}, commodityUnitsDict={ "electricity": r"GW$_{el}$", "methane": r"GW$_{th}$" }, numberOfTimeSteps=numberOfTimeSteps, verboseLogLevel=2, ) operationRateMax = pd.DataFrame( [ [ 0.2, 0.4, 1.0, 1.0, ], [ 0.0, 0.0, 0.0, 0.0, ], ], index=locations, columns=range(0, numberOfTimeSteps), ).T with pytest.raises(ValueError, match=r".*operationRateMax.*"): fn.ConversionDynamic( esM=esM, name="restricted", physicalUnit=r"GW$_{el}$", commodityConversionFactors={ "electricity": 1, "methane": -1 / 0.625 }, partLoadMin=0.3, bigM=100, rampDownMax=0.5, operationRateMax=operationRateMax, investPerCapacity=0.5, opexPerCapacity=0.021, opexPerOperation=1, interestRate=0.08, economicLifetime=33, )
def test_QPinvest(): numberOfTimeSteps = 4 hoursPerTimeStep = 2190 # Create an energy system model instance esM = fn.EnergySystemModel(locations={'location1'}, commodities={'electricity', 'hydrogen'}, numberOfTimeSteps=numberOfTimeSteps, commodityUnitsDict={'electricity': r'kW$_{el}$', 'hydrogen': r'kW$_{H_{2},LHV}$'}, hoursPerTimeStep=hoursPerTimeStep, costUnit='1 Euro', lengthUnit='km', verboseLogLevel=2) # time step length [h] timeStepLength = numberOfTimeSteps * hoursPerTimeStep ### Buy electricity at the electricity market costs = pd.Series([0.5,0.4,0.2,0.5], index=[0,1,2,3]) esM.add(fn.Source(esM=esM, name='Electricity market', commodity='electricity', hasCapacityVariable=False, commodityCostTimeSeries = costs, )) # euro/kWh ### Electrolyzers esM.add(fn.Conversion(esM=esM, name='Electrolyzer', physicalUnit=r'kW$_{el}$', commodityConversionFactors={'electricity':-1, 'hydrogen':0.7}, hasCapacityVariable=True, investPerCapacity=500, # euro/kW opexPerCapacity=500*0.025, interestRate=0.08, economicLifetime=10, QPcostScale=0.1, capacityMin=0, capacityMax=10)) ### Industry site demand = pd.Series([10000., 10000., 10000., 10000.], index = [0,1,2,3]) esM.add(fn.Sink(esM=esM, name='Industry site', commodity='hydrogen', hasCapacityVariable=False, operationRateFix = demand, )) ### Optimize (just executed if gurobi is installed) flag=True try: esM.optimize(timeSeriesAggregation=False, solver = 'gurobi') except: flag=False if flag: invest = round(esM.getOptimizationSummary('ConversionModel').loc['Electrolyzer']. \ loc['invest']['location1'].values.astype(float)[0],3) assert invest == 3148.179
def test_initializeTransmission_withSeries(): """ Tests if Transmission components are initialized without error if additional parameters are given as data series. """ # Define general parameters for esM-instance locations = ["cluster_1", "cluster_2", "cluster_3", "cluster_4"] commodityUnitDict = {"commodity1": "commodity_unit"} commodities = {"commodity1"} # Initialize esM-instance esM = fn.EnergySystemModel( locations=set(locations), commodities=commodities, numberOfTimeSteps=4, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="cost_unit", lengthUnit="length_unit", ) # Set capacityMin, capacityMax, opexPerOperation and opexPerCapacity as float idx = [ "cluster_1_cluster_2", "cluster_1_cluster_3", "cluster_1_cluster_4", "cluster_2_cluster_1", "cluster_2_cluster_3", "cluster_2_cluster_4", "cluster_3_cluster_1", "cluster_3_cluster_2", "cluster_3_cluster_4", "cluster_4_cluster_1", "cluster_4_cluster_2", "cluster_4_cluster_3", ] capMax = pd.Series([2, 3, 3, 4, 5, 6, 2, 3, 3, 1, 6, 4], index=idx) opexPerOp = capMax * 0.02 # Initialize Transmission esM.add( fn.Transmission( esM=esM, name="Transmission_1", commodity="commodity1", hasCapacityVariable=True, capacityMin=capMax, opexPerOperation=opexPerOp, ))
def test_initializeTransmission_withDataFrame(): """ Tests if Transmission components are initialized without error if additional parameters are given as DataFrame. """ # Define general parameters for esM-instance locations = ["cluster_1", "cluster_2", "cluster_3", "cluster_4"] commodityUnitDict = {"commodity1": "commodity_unit"} commodities = {"commodity1"} # Initialize esM-instance esM = fn.EnergySystemModel( locations=set(locations), commodities=commodities, numberOfTimeSteps=4, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="cost_unit", lengthUnit="length_unit", ) # Set locationalEligibility, capacityMin, capacityMax, opexPerOperation and opexPerCapacity as DataFrame elig_data = np.array([[0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 0]]) elig_df = pd.DataFrame(elig_data, index=locations, columns=locations) capMin_df = elig_df * 2 capMax_df = elig_df * 3 opexPerOp_df = elig_df * 0.02 opexPerOp_df.loc["cluster_1", "cluster_2"] = 0.03 opexPerCap_df = elig_df * 0.1 # Initialize Transmission esM.add( fn.Transmission( esM=esM, name="Transmission_1", commodity="commodity1", hasCapacityVariable=True, locationalEligibility=elig_df, capacityMax=capMax_df, capacityMin=capMin_df, opexPerOperation=opexPerOp_df, opexPerCapacity=opexPerCap_df, ))
def test_conversion_factors_as_series(): """ Input as pandas.Series for one location. """ esM = create_core_esm() esM.add( fn.Conversion( esM=esM, name="Electrolyzers_VarConvFac", physicalUnit=r"kW$_{el}$", commodityConversionFactors=pd.Series( [0.7, -1], index=["hydrogen", "electricity"] ), # Here we add a Series of time invariant conversion factors. hasCapacityVariable=True, investPerCapacity=1000, # euro/kW opexPerCapacity=500 * 0.025, interestRate=0.08, capacityMax=1000, economicLifetime=10, locationalEligibility=pd.Series([1], ["ElectrolyzerLocation"]), ) ) # optimize esM.optimize(timeSeriesAggregation=False, solver="glpk")
def test_init_full_load_hours(minimal_test_esM): import FINE as fn import pandas as pd # load minimal test system esM = minimal_test_esM # add a component with yearly minimal load hours esM.add( fn.Conversion( esM=esM, name="Electrolyzers_minFLH", physicalUnit=r"kW$_{el}$", commodityConversionFactors={"electricity": -1, "hydrogen": 0.7}, hasCapacityVariable=True, investPerCapacity=500, # euro/kW opexPerCapacity=500 * 0.025, interestRate=0.08, economicLifetime=10, yearlyFullLoadHoursMin=5000, ) ) full_load_hours_min = esM.getComponent( "Electrolyzers_minFLH" ).yearlyFullLoadHoursMin full_load_hours_max = esM.getComponent( "Electrolyzers_minFLH" ).yearlyFullLoadHoursMax assert isinstance(full_load_hours_min, pd.Series) assert full_load_hours_max is None
def test_ConversionDynamicHasHigherOperationRate(): numberOfTimeSteps = 4 locations = {'ElectrolyzerLocation', 'IndustryLocation'} esM = fn.EnergySystemModel(locations=locations, commodities={'electricity','methane'}, commodityUnitsDict={'electricity': r'GW$_{el}$','methane': r'GW$_{th}$'}, numberOfTimeSteps = numberOfTimeSteps, verboseLogLevel=2) operationRateMax = pd.DataFrame([[0., 0.4, 1., 1.,],[0., 0., 0., 0.,]], index = locations, columns = range(0,numberOfTimeSteps)).T fn.ConversionDynamic(esM=esM, name='restricted', physicalUnit=r'GW$_{el}$', commodityConversionFactors={'electricity':1, 'methane':-1/0.625}, partLoadMin = 0.3, bigM= 100, rampDownMax=0.5, operationRateMax= operationRateMax, investPerCapacity=0.5, opexPerCapacity=0.021, opexPerOperation =1, interestRate=0.08, economicLifetime=33,)
def test_TSAmultiStage(minimal_test_esM): ''' Get the minimal test system, and check if the Error-Bounding-Approach works for it ''' # modify the minimal LP and change it to a MILP esM = minimal_test_esM # get the components with capacity variables electrolyzers = esM.getComponent('Electrolyzers') pressureTank = esM.getComponent('Pressure tank') pipelines = esM.getComponent('Pipelines') # set binary variables and define bigM electrolyzers.hasIsBuiltBinaryVariable = True pressureTank.hasIsBuiltBinaryVariable = True pipelines.hasIsBuiltBinaryVariable = True electrolyzers.investIfBuilt = pd.Series(2e5, index=esM.locations) pressureTank.investIfBuilt = pd.Series(1e5, index=esM.locations) pipelines.investIfBuilt.loc[:] = 100 electrolyzers.bigM = 30e4 pressureTank.bigM = 30e6 pipelines.bigM = 30e3 electrolyzers.investIfBuilt = pd.Series(2e5, index=esM.locations) pressureTank.investIfBuilt = pd.Series(1e5, index=esM.locations) electrolyzers.bigM = 30e4 pressureTank.bigM = 30e6 # optimize with 2 Stage Approach fn.optimizeTSAmultiStage(esM, relaxIsBuiltBinary=True, solver='glpk', numberOfTypicalPeriods=2, numberOfTimeStepsPerPeriod=1) # get gap gap = esM.gap assert gap > 0.1078 and gap < 0.1079
def readEnergySystemModelFromExcel(fileName='scenarioInput.xlsx'): """ Read energy system model from excel file. :param fileName: excel file name or path (including .xlsx ending) |br| * the default value is 'scenarioInput.xlsx' :type fileName: string :return: esM, esMData - an EnergySystemModel class instance and general esMData as a Series """ file = pd.ExcelFile(fileName) esMData = pd.read_excel(file, sheetname='EnergySystemModel', index_col=0, squeeze=True) esMData = esMData.apply(lambda v: ast.literal_eval(v) if type(v) == str and v[0] == '{' else v) kw = inspect.getargspec(fn.EnergySystemModel.__init__).args esM = fn.EnergySystemModel(**esMData[esMData.index.isin(kw)]) for comp in esMData['componentClasses']: data = pd.read_excel(file, sheetname=comp) dataKeys = set(data['name'].values) if comp + 'LocSpecs' in file.sheet_names: dataLoc = pd.read_excel(file, sheetname=comp + 'LocSpecs', index_col=[0, 1, 2]).sort_index() dataLocKeys = set(dataLoc.index.get_level_values(0).unique()) if not dataLocKeys <= dataKeys: raise ValueError('Invalid key(s) detected in ' + comp + '\n', dataLocKeys - dataKeys) if dataLoc.isnull().any().any(): raise ValueError('NaN values in ' + comp + 'LocSpecs data detected.') if comp + 'TimeSeries' in file.sheet_names: dataTS = pd.read_excel(file, sheetname=comp + 'TimeSeries', index_col=[0, 1, 2]).sort_index() dataTSKeys = set(dataTS.index.get_level_values(0).unique()) if not dataTSKeys <= dataKeys: raise ValueError('Invalid key(s) detected in ' + comp + '\n', dataTSKeys - dataKeys) if dataTS.isnull().any().any(): raise ValueError('NaN values in ' + comp + 'TimeSeries data detected.') for key, row in data.iterrows(): temp = row.dropna() temp = temp.drop(temp[temp == 'None'].index) temp = temp.apply(lambda v: ast.literal_eval(v) if type(v) == str and v[0] == '{' else v) if comp + 'LocSpecs' in file.sheet_names: dataLoc_ = dataLoc[dataLoc.index.get_level_values(0) == temp['name']] for ix in dataLoc_.index.get_level_values(1).unique(): temp.set_value(ix, dataLoc.loc[(temp['name'], ix)].squeeze()) if comp + 'TimeSeries' in file.sheet_names: dataTS_ = dataTS[dataTS.index.get_level_values(0) == temp['name']] for ix in dataTS_.index.get_level_values(1).unique(): temp.set_value(ix, dataTS_.loc[(temp['name'], ix)].T) kwargs = temp esM.add(getattr(fn, comp)(esM, **kwargs)) return esM, esMData
def create_core_esm(): """ We create a core esm that only consists of a source and a sink in one location. """ numberOfTimeSteps = 4 hoursPerTimeStep = 2190 # Create an energy system model instance esM = fn.EnergySystemModel( locations={"ElectrolyzerLocation"}, commodities={"electricity", "hydrogen"}, numberOfTimeSteps=numberOfTimeSteps, commodityUnitsDict={ "electricity": r"kW$_{el}$", "hydrogen": r"kW$_{H_{2},LHV}$", }, hoursPerTimeStep=hoursPerTimeStep, costUnit="1 Euro", lengthUnit="km", verboseLogLevel=2, ) # Source esM.add( fn.Source( esM=esM, name="Electricity market", commodity="electricity", hasCapacityVariable=False, ) ) # Sink demand = pd.Series(np.array([1.0, 1.0, 1.0, 1.0])) * hoursPerTimeStep esM.add( fn.Sink( esM=esM, name="Industry site", commodity="hydrogen", hasCapacityVariable=False, operationRateFix=demand, ) ) return esM
def test_initializeTransmission_withFloat(): """ Tests if Transmission components are initialized without error if additional parameters are given as float. """ # Define general parameters for esM-instance locations = ["cluster_1", "cluster_2", "cluster_3", "cluster_4"] commodityUnitDict = {"commodity1": "commodity_unit"} commodities = {"commodity1"} # Initialize esM-instance esM = fn.EnergySystemModel( locations=set(locations), commodities=commodities, numberOfTimeSteps=4, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="cost_unit", lengthUnit="length_unit", ) # Set capacityMin, capacityMax, opexPerOperation and opexPerCapacity as float capMin = 2 capMax = 3 opexPerOp = 0.02 opexPerCap = 0.1 # Initialize Transmission esM.add( fn.Transmission( esM=esM, name="Transmission_1", commodity="commodity1", hasCapacityVariable=True, capacityMax=capMax, capacityMin=capMin, opexPerOperation=opexPerOp, opexPerCapacity=opexPerCap, ))
def test_fullloadhours_above(minimal_test_esM): ''' Get the minimal test system, and check if the fulllload hours of electrolyzer are above 4000. ''' esM = minimal_test_esM esM.optimize(timeSeriesAggregation=False, solver='glpk') # Plot the operational heat map fig, ax = fn.plotOperationColorMap(esM, 'Electrolyzers', 'ElectrolyzerLocation', figsize=(4, 3), nbTimeStepsPerPeriod=1, nbPeriods=4, yticks=[0, 1])
def test_linkedQuantityID(minimal_test_esM): """ """ esM = minimal_test_esM # get components electrolyzer = esM.getComponent("Electrolyzers") market = esM.getComponent("Electricity market") # create dummy component esM.add( fn.Conversion( esM=esM, name="Dummy", physicalUnit=r"kW$_{el}$", commodityConversionFactors={"electricity": -1, "electricity": 1}, hasCapacityVariable=True, capacityPerPlantUnit=1.0, opexPerCapacity=1.0, linkedQuantityID="test", ) ) # make electrolyzer sizing discrete electrolyzer.capacityPerPlantUnit = 1 electrolyzer.linkedQuantityID = "test" electrolyzer.opexPerCapacity = pd.Series(1, index=esM.locations) # optimize esM.optimize(timeSeriesAggregation=False, solver="glpk") assert ( esM.getOptimizationSummary("ConversionModel") .loc["Electrolyzers"] .loc["opexCap"]["ElectrolyzerLocation"] .values.astype(int)[0] == esM.getOptimizationSummary("ConversionModel") .loc["Dummy"] .loc["opexCap"]["ElectrolyzerLocation"] .values.astype(int)[0] )
def importFromDict(esmDict, compDict): """ Converts the dictionaries created by the exportToDict function to an EnergySystemModel. :param esMDict: dictionary created from exportToDict contains all esM information :type dict: dictionary instance :param compDict: dictionary create from exportToDict containing all component information :type dict: dictionary instance :return: esM - EnergySystemModel instance in which the optimized model is held """ esM = fn.EnergySystemModel(**esmDict) # add components for classname in compDict: # get class class_ = getattr(fn, classname) for comp in compDict[classname]: esM.add(class_(esM, **compDict[classname][comp])) return esM
def test_minimumDownTime(): # read in original results results = [ 5.0, 5.0, 0.0, 0.0, 0.0, 5.0, 5.0, 0.0, 0.0, 0.0, 5.0, 5.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 4.0, ] # 2. Create an energy system model instance locations = {"example_region1", "example_region2"} commodityUnitDict = {"electricity": r"GW$_{el}$", "methane": r"GW$_{CH_{4},LHV}$"} commodities = {"electricity", "methane"} esM = fn.EnergySystemModel( locations=locations, commodities=commodities, numberOfTimeSteps=20, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="1e9 Euro", lengthUnit="km", verboseLogLevel=0, ) data_cost = { "example_region1": [ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, ], "example_region2": [ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, ], } data_cost_df = pd.DataFrame(data=data_cost) esM.add( fn.Source( esM=esM, name="Natural gas purchase", commodity="methane", hasCapacityVariable=False, commodityCostTimeSeries=data_cost_df, ) ) # 4. Add conversion components to the energy system model ### Combined cycle gas turbine plants data_cap = pd.Series(index=["example_region1", "example_region2"], data=[10, 10]) esM.add( fn.ConversionDynamic( esM=esM, name="unrestricted", physicalUnit=r"GW$_{el}$", commodityConversionFactors={"electricity": 1, "methane": -1 / 0.625}, capacityFix=data_cap, partLoadMin=0.1, bigM=100, investPerCapacity=0.65, opexPerCapacity=0.021, opexPerOperation=10, interestRate=0.08, economicLifetime=33, ) ) esM.add( fn.ConversionDynamic( esM=esM, name="restricted", physicalUnit=r"GW$_{el}$", commodityConversionFactors={"electricity": 1, "methane": -1 / 0.625}, capacityFix=data_cap, partLoadMin=0.4, bigM=100, downTimeMin=3, investPerCapacity=0.5, opexPerCapacity=0.021, opexPerOperation=1, interestRate=0.08, economicLifetime=33, ) ) data_demand = { "example_region1": [5, 5, 2, 2, 4, 5, 5, 2, 2, 4, 5, 5, 2, 2, 4, 5, 5, 2, 2, 4], "example_region2": [5, 5, 2, 2, 4, 5, 5, 2, 2, 4, 5, 5, 2, 2, 4, 5, 5, 2, 2, 4], } data_demand_df = pd.DataFrame(data=data_demand) esM.add( fn.Sink( esM=esM, name="Electricity demand", commodity="electricity", hasCapacityVariable=False, operationRateFix=data_demand_df, ) ) esM.optimize(timeSeriesAggregation=False, solver="glpk") print("restricted dispatch:\n") print( esM.componentModelingDict[ "ConversionDynamicModel" ].operationVariablesOptimum.xs("restricted") ) print("unrestricted dispatch:\n") print( esM.componentModelingDict[ "ConversionDynamicModel" ].operationVariablesOptimum.xs("unrestricted") ) # print(esM.componentModelingDict['ConversionDynamicModel'].operationVariablesOptimum.xs('unrestricted')) # test if here solved fits with original results # test if here solved fits with original results testresults = esM.componentModelingDict[ "ConversionDynamicModel" ].operationVariablesOptimum.xs("restricted") np.testing.assert_array_almost_equal(testresults.values[0], results, decimal=2)
def test_watersupply(): # read in original results results = pd.read_csv(os.path.join( os.path.dirname(__file__), '_testInputFiles', 'waterSupplySystem_totalTransmission.csv'), index_col=[0, 1, 2], header=None, squeeze=True) # get parameters locations = [ 'House 1', 'House 2', 'House 3', 'House 4', 'House 5', 'House 6', 'Node 1', 'Node 2', 'Node 3', 'Node 4', 'Water treatment', 'Water tank' ] commodityUnitDict = {'clean water': 'U', 'river water': 'U'} commodities = {'clean water', 'river water'} esM = fn.EnergySystemModel(locations=set(locations), commodities=commodities, numberOfTimeSteps=8760, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit='1e3 Euro', lengthUnit='m') starttime = time.time() # Source riverFlow = pd.DataFrame(np.zeros((8760, 12)), columns=locations) np.random.seed(42) riverFlow.loc[:, 'Water treatment'] = np.random.uniform( 0, 4, (8760)) + 8 * np.sin(np.pi * np.arange(8760) / 8760) esM.add( fn.Source(esM=esM, name='River', commodity='river water', hasCapacityVariable=False, operationRateMax=riverFlow, opexPerOperation=0.05)) # Conversion eligibility = pd.Series([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], index=locations) esM.add( fn.Conversion(esM=esM, name='Water treatment plant', physicalUnit='U', commodityConversionFactors={ 'river water': -1, 'clean water': 1 }, hasCapacityVariable=True, locationalEligibility=eligibility, investPerCapacity=7, opexPerCapacity=0.02 * 7, interestRate=0.08, economicLifetime=20)) # Storage eligibility = pd.Series([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], index=locations) esM.add( fn.Storage(esM=esM, name='Water tank', commodity='clean water', hasCapacityVariable=True, chargeRate=1 / 24, dischargeRate=1 / 24, locationalEligibility=eligibility, investPerCapacity=0.10, opexPerCapacity=0.02 * 0.1, interestRate=0.08, economicLifetime=20)) # Transmission distances = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0], [0, 0, 0, 0, 38, 40, 0, 105, 0, 0, 0, 0], [0, 0, 38, 40, 0, 0, 105, 0, 100, 0, 0, 0], [38, 40, 0, 0, 0, 0, 0, 100, 0, 30, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 20, 50], [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0]]) distances = pd.DataFrame(distances, index=locations, columns=locations) # Old water pipes capacityFix = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0, 2, 0, 4, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 4], [0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0]]) capacityFix = pd.DataFrame(capacityFix, index=locations, columns=locations) isBuiltFix = capacityFix.copy() isBuiltFix[isBuiltFix > 0] = 1 esM.add( fn.Transmission(esM=esM, name='Old water pipes', commodity='clean water', losses=0.1e-2, distances=distances, hasCapacityVariable=True, hasIsBuiltBinaryVariable=True, bigM=100, capacityFix=capacityFix, isBuiltFix=isBuiltFix)) # New water pipes incidence = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]]) eligibility = pd.DataFrame(incidence, index=locations, columns=locations) esM.add( fn.Transmission(esM=esM, name='New water pipes', commodity='clean water', losses=0.05e-2, distances=distances, hasCapacityVariable=True, hasIsBuiltBinaryVariable=True, bigM=100, locationalEligibility=eligibility, investPerCapacity=0.1, investIfBuilt=0.5, interestRate=0.08, economicLifetime=50)) # Sink winterHours = np.append(range(8520, 8760), range(1920)) springHours, summerHours, autumnHours = np.arange(1920, 4128), np.arange( 4128, 6384), np.arange(6384, 8520) demand = pd.DataFrame(np.zeros((8760, 12)), columns=list(locations)) np.random.seed(42) demand[locations[0:6]] = np.random.uniform(0, 1, (8760, 6)) demand.loc[winterHours[(winterHours % 24 < 5) | (winterHours % 24 >= 23)]] = 0 demand.loc[springHours[(springHours % 24 < 4)]] = 0 demand.loc[summerHours[(summerHours % 24 < 5) | (summerHours % 24 >= 23)]] = 0 demand.loc[autumnHours[(autumnHours % 24 < 6) | (autumnHours % 24 >= 23)]] = 0 esM.add( fn.Sink(esM=esM, name='Water demand', commodity='clean water', hasCapacityVariable=False, operationRateFix=demand)) # # Optimize the system esM.cluster(numberOfTypicalPeriods=7) esM.optimize( timeSeriesAggregation=True, solver='glpk', optimizationSpecs='LogToConsole=1 OptimalityTol=1e-6 crossover=1') # # Selected results output esM.getOptimizationSummary("SourceSinkModel", outputLevel=2) # ### Storage esM.getOptimizationSummary("StorageModel", outputLevel=2) # ### Transmission esM.getOptimizationSummary("TransmissionModel", outputLevel=2) esM.componentModelingDict[ "TransmissionModel"].operationVariablesOptimum.sum(axis=1) # testresults = esM.componentModelingDict[ "TransmissionModel"].operationVariablesOptimum.sum(axis=1) print('Optimization took ' + str(time.time() - starttime)) # test if here solved fits with original results np.testing.assert_array_almost_equal(testresults.values, results.values, decimal=2)
def __init__(self, esM, name, commodity, hasCapacityVariable, tFwd, tBwd, operationRateFix, opexShift=1e-6, shiftUpMax=None, shiftDownMax=None, socOffsetDown=-1, socOffsetUp=-1, **kwargs): """ Constructor for creating an DemandSideManagement class instance. Note: the DemandSideManagement class inherits from the Sink class; kwargs provide input arguments to the Sink component. **Required arguments:** :param esM: energy system model to which the DemandSideManagement 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 underlying Sink 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 :param tFwd: the number of timesteps for backwards demand shifting. :type tFwd: integer (>0) :param tBwd: the number of timesteps for forwards demand shifting. :type tBwd: integer (>= 0) :param operationRateFix: specifies the original time series of the shiftable demand. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. 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 for each time step. :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 equal the in the energy system model specified locations. The data in ineligible locations are set to zero. **Default arguments:** :param opexShift: operational cost for shifting the demand (given in costUnit/commodityUnit). Setting this value also penalizes unreasonable, unnecessary shifting of demand. |br| * the default value is 1e-6 :type opexShift: positive float (>0) :param shiftUpMax: maximum amount of upwards shiftable commodity at one timestep. If None, the value is set equal to the maximum demand of the respective location. |br| * the default value is None :type shiftUpMax: positive float or None :param shiftDownMax: maximum amount of downwards shiftable commodity at one timestep. If None, the value is set equal to the maximum demand of the respective location. |br| * the default value is Nonde :type shiftDownMax: positive float or None :param socOffsetDown: determines whether the state of charge at the end of a period p has to be equal to the one at the beginning of a period p+1 (socOffsetDown=-1) or if it can be smaller at the beginning of p+1 (socOffsetDown>=0). In the latter case, the product of the parameter socOffsetDown and the actual soc offset is used as a penalty factor in the objective function. (usefull when infeasibilities are encountered when using DemandSideManagement and time series aggregation) |br| * the default value is -1 :type socOffsetDown: float :param socOffsetUp: determines whether the state of charge at the end of a period p has to be equal to the one at the beginning of a period p+1 (socOffsetUp=-1) or if it can be larger at the beginning of p+1 (socOffsetUp>=0). In the latter case, the product of the parameter socOffsetUp and the actual soc offset is used as a penalty factor in the objective function. (usefull when infeasibilities are encountered when using DemandSideManagement and time series aggregation) |br| * the default value is -1 :type socOffsetUp: float """ if esM.verbose < 2: warnings.warn('The DemandSideManagement component is currently in its BETA testing phase. ' + 'Infeasiblities can occur (in this case consider using socOffsetUp/ socOffsetDown). ' + 'Best results can be obtained when tFwd+tBwd+1 is a divisor of either the total number ' + 'of timesteps or the number of time steps per period. Use with care...') self.tBwd = tBwd self.tFwd = tFwd self.tDelta = tFwd+tBwd+1 operationRateFix = pd.concat([operationRateFix.iloc[-tBwd:], operationRateFix.iloc[:-tBwd]]).reset_index(drop=True) if shiftUpMax is None: self.shiftUpMax = operationRateFix.max() print('shiftUpMax was set to', operationRateFix.max()) else: self.shiftUpMax = shiftUpMax if shiftDownMax is None: self.shiftDownMax = operationRateFix.max() print('shiftDownMax was set to', operationRateFix.max()) else: self.shiftDownMax = shiftDownMax Sink.__init__(self, esM, name, commodity, hasCapacityVariable, operationRateFix=operationRateFix, **kwargs) self.modelingClass = DSMModel for i in range(self.tDelta): SOCmax = operationRateFix.copy() SOCmax[SOCmax > 0] = 0 SOCmax_ = pd.concat([operationRateFix[operationRateFix.index % self.tDelta == i]]*self.tDelta).\ sort_index().reset_index(drop=True) if (len(SOCmax_) > len(esM.totalTimeSteps)): SOCmax_ = pd.concat([SOCmax_.iloc[tFwd+tBwd-i:], SOCmax_.iloc[:tFwd+tBwd-i]]).reset_index(drop=True) print('tBwd+tFwd+1 is not a divisor of the total number of time steps of the energy system. ' + 'This shortens the shiftable timeframe of demand_' + str(i) + ' by ' + str(len(SOCmax_)-len(esM.totalTimeSteps)) + ' time steps') SOCmax = SOCmax_.iloc[:len(esM.totalTimeSteps)] elif len(SOCmax_) < len(esM.totalTimeSteps): SOCmax.iloc[0:len(SOCmax_.iloc[tFwd+tBwd-i:])] = SOCmax_.iloc[tFwd+tBwd-i:].values if len(SOCmax_.iloc[:tFwd+tBwd-i]) > 0: SOCmax.iloc[-len(SOCmax_.iloc[:tFwd+tBwd-i]):] = SOCmax_.iloc[:tFwd+tBwd-i].values else: SOCmax_ = pd.concat([SOCmax_.iloc[tFwd+tBwd-i:], SOCmax_.iloc[:tFwd+tBwd-i]]).reset_index(drop=True) SOCmax = SOCmax_ chargeOpRateMax = SOCmax.copy() if i < self.tDelta - 1: SOCmax[SOCmax.index % self.tDelta == i+1] = 0 else: SOCmax[SOCmax.index % self.tDelta == 0] = 0 dischargeFix = operationRateFix.copy() dischargeFix[dischargeFix.index % self.tDelta != i] = 0 opexPerChargeOpTimeSeries = pd.DataFrame([[opexShift for loc in self.locationalEligibility] for t in esM.totalTimeSteps], columns=self.locationalEligibility.index) opexPerChargeOpTimeSeries[(opexPerChargeOpTimeSeries.index - i ) % self.tDelta == tBwd + 1] = 0 esM.add(fn.StorageExtBETA(esM, name + '_' + str(i), commodity, stateOfChargeOpRateMax=SOCmax, dischargeOpRateFix=dischargeFix, hasCapacityVariable=False, chargeOpRateMax=chargeOpRateMax, opexPerChargeOpTimeSeries=opexPerChargeOpTimeSeries, doPreciseTsaModeling=True, socOffsetDown=socOffsetDown, socOffsetUp=socOffsetUp))
def test_commodityCostTimeSeries(): cwd = os.getcwd() data = getData() # read in original results results = pd.Series.from_csv( os.path.join(os.path.dirname(__file__), '..', 'examples', 'Multi-regional Energy System Workflow', 'totalBiogasPurchase.csv')) # 2. Create an energy system model instance locations = { 'cluster_0', 'cluster_1', 'cluster_2', 'cluster_3', 'cluster_4', 'cluster_5', 'cluster_6', 'cluster_7' } commodityUnitDict = { 'electricity': r'GW$_{el}$', 'methane': r'GW$_{CH_{4},LHV}$', 'biogas': r'GW$_{biogas,LHV}$', 'CO2': r'Mio. t$_{CO_2}$/h', 'hydrogen': r'GW$_{H_{2},LHV}$' } commodities = {'electricity', 'hydrogen', 'methane', 'biogas', 'CO2'} numberOfTimeSteps = 8760 hoursPerTimeStep = 1 esM = fn.EnergySystemModel(locations=locations, commodities=commodities, numberOfTimeSteps=8760, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit='1e9 Euro', lengthUnit='km', verboseLogLevel=0) CO2_reductionTarget = 1 # 3. Add commodity sources to the energy system model ## 3.1. Electricity sources ### Wind onshore esM.add( fn.Source(esM=esM, name='Wind (onshore)', commodity='electricity', hasCapacityVariable=True, operationRateMax=data['Wind (onshore), operationRateMax'], capacityMax=data['Wind (onshore), capacityMax'], investPerCapacity=1.1, opexPerCapacity=1.1 * 0.02, interestRate=0.08, economicLifetime=20)) data['Wind (onshore), operationRateMax'].sum() ### Wind offshore esM.add( fn.Source(esM=esM, name='Wind (offshore)', commodity='electricity', hasCapacityVariable=True, operationRateMax=data['Wind (offshore), operationRateMax'], capacityMax=data['Wind (offshore), capacityMax'], investPerCapacity=2.3, opexPerCapacity=2.3 * 0.02, interestRate=0.08, economicLifetime=20)) data['Wind (offshore), operationRateMax'].sum() ### PV esM.add( fn.Source(esM=esM, name='PV', commodity='electricity', hasCapacityVariable=True, operationRateMax=data['PV, operationRateMax'], capacityMax=data['PV, capacityMax'], investPerCapacity=0.65, opexPerCapacity=0.65 * 0.02, interestRate=0.08, economicLifetime=25)) data['PV, operationRateMax'].sum() ### Exisisting run-of-river hydroelectricity plants esM.add( fn.Source( esM=esM, name='Existing run-of-river plants', commodity='electricity', hasCapacityVariable=True, operationRateFix=data[ 'Existing run-of-river plants, operationRateFix'], tsaWeight=0.01, capacityFix=data['Existing run-of-river plants, capacityFix'], investPerCapacity=0, opexPerCapacity=0.208)) ## 3.2. Methane (natural gas and biogas) ### Natural gas esM.add( fn.Source(esM=esM, name='Natural gas purchase', commodity='methane', hasCapacityVariable=False, commodityCostTimeSeries=data[ 'Natural Gas, commodityCostTimeSeries'])) ### Biogas esM.add( fn.Source( esM=esM, name='Biogas purchase', commodity='biogas', operationRateMax=data['Biogas, operationRateMax'], hasCapacityVariable=False, commodityCostTimeSeries=data['Biogas, commodityCostTimeSeries'])) ## 3.3 CO2 ### CO2 esM.add( fn.Source(esM=esM, name='CO2 from enviroment', commodity='CO2', hasCapacityVariable=False, commodityLimitID='CO2 limit', yearlyLimit=366 * (1 - CO2_reductionTarget))) # 4. Add conversion components to the energy system model ### Combined cycle gas turbine plants esM.add( fn.Conversion(esM=esM, name='CCGT plants (methane)', physicalUnit=r'GW$_{el}$', commodityConversionFactors={ 'electricity': 1, 'methane': -1 / 0.625, 'CO2': 201 * 1e-6 / 0.625 }, hasCapacityVariable=True, investPerCapacity=0.65, opexPerCapacity=0.021, interestRate=0.08, economicLifetime=33)) ### New combined cycle gas turbine plants for biogas esM.add( fn.Conversion(esM=esM, name='New CCGT plants (biogas)', physicalUnit=r'GW$_{el}$', commodityConversionFactors={ 'electricity': 1, 'biogas': -1 / 0.635 }, hasCapacityVariable=True, investPerCapacity=0.7, opexPerCapacity=0.021, interestRate=0.08, economicLifetime=33)) ### New combined cycly gas turbines for hydrogen esM.add( fn.Conversion(esM=esM, name='New CCGT plants (hydrogen)', physicalUnit=r'GW$_{el}$', commodityConversionFactors={ 'electricity': 1, 'hydrogen': -1 / 0.6 }, hasCapacityVariable=True, investPerCapacity=0.7, opexPerCapacity=0.021, interestRate=0.08, economicLifetime=33)) ### Electrolyzers esM.add( fn.Conversion(esM=esM, name='Electroylzers', physicalUnit=r'GW$_{el}$', commodityConversionFactors={ 'electricity': -1, 'hydrogen': 0.7 }, hasCapacityVariable=True, investPerCapacity=0.5, opexPerCapacity=0.5 * 0.025, interestRate=0.08, economicLifetime=10)) ### rSOC capexRSOC = 1.5 esM.add( fn.Conversion(esM=esM, name='rSOEC', physicalUnit=r'GW$_{el}$', linkedConversionCapacityID='rSOC', commodityConversionFactors={ 'electricity': -1, 'hydrogen': 0.6 }, hasCapacityVariable=True, investPerCapacity=capexRSOC / 2, opexPerCapacity=capexRSOC * 0.02 / 2, interestRate=0.08, economicLifetime=10)) esM.add( fn.Conversion(esM=esM, name='rSOFC', physicalUnit=r'GW$_{el}$', linkedConversionCapacityID='rSOC', commodityConversionFactors={ 'electricity': 1, 'hydrogen': -1 / 0.6 }, hasCapacityVariable=True, investPerCapacity=capexRSOC / 2, opexPerCapacity=capexRSOC * 0.02 / 2, interestRate=0.08, economicLifetime=10)) # 5. Add commodity storages to the energy system model ## 5.1. Electricity storage ### Lithium ion batteries esM.add( fn.Storage(esM=esM, name='Li-ion batteries', commodity='electricity', hasCapacityVariable=True, chargeEfficiency=0.95, cyclicLifetime=10000, dischargeEfficiency=0.95, selfDischarge=1 - (1 - 0.03)**(1 / (30 * 24)), chargeRate=1, dischargeRate=1, doPreciseTsaModeling=False, investPerCapacity=0.151, opexPerCapacity=0.002, interestRate=0.08, economicLifetime=22)) ## 5.2. Hydrogen storage ### Hydrogen filled salt caverns esM.add( fn.Storage(esM=esM, name='Salt caverns (hydrogen)', commodity='hydrogen', hasCapacityVariable=True, capacityVariableDomain='continuous', capacityPerPlantUnit=133, chargeRate=1 / 470.37, dischargeRate=1 / 470.37, sharedPotentialID='Existing salt caverns', stateOfChargeMin=0.33, stateOfChargeMax=1, capacityMax=data['Salt caverns (hydrogen), capacityMax'], investPerCapacity=0.00011, opexPerCapacity=0.00057, interestRate=0.08, economicLifetime=30)) ## 5.3. Methane storage ### Methane filled salt caverns esM.add( fn.Storage(esM=esM, name='Salt caverns (biogas)', commodity='biogas', hasCapacityVariable=True, capacityVariableDomain='continuous', capacityPerPlantUnit=443, chargeRate=1 / 470.37, dischargeRate=1 / 470.37, sharedPotentialID='Existing salt caverns', stateOfChargeMin=0.33, stateOfChargeMax=1, capacityMax=data['Salt caverns (methane), capacityMax'], investPerCapacity=0.00004, opexPerCapacity=0.00001, interestRate=0.08, economicLifetime=30)) ## 5.4 Pumped hydro storage ### Pumped hydro storage esM.add( fn.Storage(esM=esM, name='Pumped hydro storage', commodity='electricity', chargeEfficiency=0.88, dischargeEfficiency=0.88, hasCapacityVariable=True, selfDischarge=1 - (1 - 0.00375)**(1 / (30 * 24)), chargeRate=0.16, dischargeRate=0.12, capacityFix=data['Pumped hydro storage, capacityFix'], investPerCapacity=0, opexPerCapacity=0.000153)) # 6. Add commodity transmission components to the energy system model ## 6.1. Electricity transmission ### AC cables esM.add( fn.LinearOptimalPowerFlow(esM=esM, name='AC cables', commodity='electricity', hasCapacityVariable=True, capacityFix=data['AC cables, capacityFix'], reactances=data['AC cables, reactances'])) ### DC cables esM.add( fn.Transmission(esM=esM, name='DC cables', commodity='electricity', losses=data['DC cables, losses'], distances=data['DC cables, distances'], hasCapacityVariable=True, capacityFix=data['DC cables, capacityFix'])) ## 6.2 Methane transmission ### Methane pipeline esM.add( fn.Transmission(esM=esM, name='Pipelines (biogas)', commodity='biogas', distances=data['Pipelines, distances'], hasCapacityVariable=True, hasIsBuiltBinaryVariable=False, bigM=300, locationalEligibility=data['Pipelines, eligibility'], capacityMax=data['Pipelines, eligibility'] * 15, sharedPotentialID='pipelines', investPerCapacity=0.000037, investIfBuilt=0.000314, interestRate=0.08, economicLifetime=40)) ## 6.3 Hydrogen transmission ### Hydrogen pipelines esM.add( fn.Transmission(esM=esM, name='Pipelines (hydrogen)', commodity='hydrogen', distances=data['Pipelines, distances'], hasCapacityVariable=True, hasIsBuiltBinaryVariable=False, bigM=300, locationalEligibility=data['Pipelines, eligibility'], capacityMax=data['Pipelines, eligibility'] * 15, sharedPotentialID='pipelines', investPerCapacity=0.000177, investIfBuilt=0.00033, interestRate=0.08, economicLifetime=40)) # 7. Add commodity sinks to the energy system model ## 7.1. Electricity sinks ### Electricity demand esM.add( fn.Sink(esM=esM, name='Electricity demand', commodity='electricity', hasCapacityVariable=False, operationRateFix=data['Electricity demand, operationRateFix'])) ## 7.2. Hydrogen sinks ### Fuel cell electric vehicle (FCEV) demand FCEV_penetration = 0.5 esM.add( fn.Sink(esM=esM, name='Hydrogen demand', commodity='hydrogen', hasCapacityVariable=False, operationRateFix=data['Hydrogen demand, operationRateFix'] * FCEV_penetration)) ## 7.3. CO2 sinks ### CO2 exiting the system's boundary esM.add( fn.Sink(esM=esM, name='CO2 to enviroment', commodity='CO2', hasCapacityVariable=False, commodityLimitID='CO2 limit', yearlyLimit=366 * (1 - CO2_reductionTarget))) # 8. Optimize energy system model esM.cluster(numberOfTypicalPeriods=3) esM.optimize(timeSeriesAggregation=True, solver='glpk') # test if here solved fits with original results testresults = esM.componentModelingDict[ "SourceSinkModel"].operationVariablesOptimum.xs('Biogas purchase').sum( axis=1) np.testing.assert_array_almost_equal(testresults.values, results.values, decimal=2)
locations = data["locations"] commodityUnitDict = { "electricity": "kW_el", "methane": "kW_CH4_LHV", "heat": "kW_th" } commodities = {"electricity", "methane", "heat"} numberOfTimeSteps = 8760 hoursPerTimeStep = 1 # %% esM = fn.EnergySystemModel( locations=locations, commodities=commodities, numberOfTimeSteps=8760, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="€", lengthUnit="m", verboseLogLevel=2, ) # %% [markdown] # # 3. Add commodity sources to the energy system model # %% [markdown] # ### Electricity Purchase # %% esM.add( fn.Source( esM=esM,
def getModel(): numberOfTimeSteps = 4 hoursPerTimeStep = 2190 # Create an energy system model instance esM = fn.EnergySystemModel( locations={"ElectrolyzerLocation", "IndustryLocation"}, commodities={"electricity", "hydrogen"}, numberOfTimeSteps=numberOfTimeSteps, commodityUnitsDict={ "electricity": r"kW$_{el}$", "hydrogen": r"kW$_{H_{2},LHV}$", }, hoursPerTimeStep=hoursPerTimeStep, costUnit="1 Euro", lengthUnit="km", verboseLogLevel=1, balanceLimit=None, lowerBound=False, ) # time step length [h] timeStepLength = numberOfTimeSteps * hoursPerTimeStep ### Buy electricity at the electricity market costs = pd.DataFrame( [ np.array([ 0.05, 0.0, 0.1, 0.051, ]), np.array([ 0.0, 0.0, 0.0, 0.0, ]), ], index=["ElectrolyzerLocation", "IndustryLocation"], ).T revenues = pd.DataFrame( [ np.array([ 0.0, 0.01, 0.0, 0.0, ]), np.array([ 0.0, 0.0, 0.0, 0.0, ]), ], index=["ElectrolyzerLocation", "IndustryLocation"], ).T maxpurchase = (pd.DataFrame( [ np.array([ 1e6, 1e6, 1e6, 1e6, ]), np.array([ 0.0, 0.0, 0.0, 0.0, ]), ], index=["ElectrolyzerLocation", "IndustryLocation"], ).T * hoursPerTimeStep) esM.add( fn.Source( esM=esM, name="Electricity market", commodity="electricity", hasCapacityVariable=False, operationRateMax=maxpurchase, commodityCostTimeSeries=costs, commodityRevenueTimeSeries=revenues, )) # eur/kWh ### Electrolyzers esM.add( fn.Conversion( esM=esM, name="Electrolyzers", physicalUnit=r"kW$_{el}$", commodityConversionFactors={ "electricity": -1, "hydrogen": 0.7 }, hasCapacityVariable=True, investPerCapacity=500, # euro/kW opexPerCapacity=500 * 0.025, interestRate=0.08, economicLifetime=10, )) ### Hydrogen filled somewhere esM.add( fn.Storage( esM=esM, name="Pressure tank", commodity="hydrogen", hasCapacityVariable=True, capacityVariableDomain="continuous", stateOfChargeMin=0.33, investPerCapacity=0.5, # eur/kWh interestRate=0.08, economicLifetime=30, )) ### Hydrogen pipelines esM.add( fn.Transmission( esM=esM, name="Pipelines", commodity="hydrogen", hasCapacityVariable=True, investPerCapacity=0.177, interestRate=0.08, economicLifetime=40, )) ### Industry site demand = (pd.DataFrame( [ np.array([ 0.0, 0.0, 0.0, 0.0, ]), np.array([ 6e3, 6e3, 6e3, 6e3, ]), ], index=["ElectrolyzerLocation", "IndustryLocation"], ).T * hoursPerTimeStep) esM.add( fn.Sink( esM=esM, name="Industry site", commodity="hydrogen", hasCapacityVariable=False, operationRateFix=demand, )) return esM
"wood", "biowaste", "bioslurry", "diesel", "nGasImp", "biogas", } numberOfTimeSteps = 8760 hoursPerTimeStep = 1 # %% esM = fn.EnergySystemModel( locations=locations, commodities=commodities, numberOfTimeSteps=8760, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=1, costUnit="1e6 Euro", lengthUnit="km", verboseLogLevel=0, ) # %% [markdown] # # 3. Sources # # Source components transfer a commodity from outside the system boundary of EnergyLand into the system. # %% [markdown] # ## 3.1 Electricity sources # %% [markdown] # ### Wind turbines
def optimizeTSAmultiStage(esM, declaresOptimizationProblem=True, relaxIsBuiltBinary=False, numberOfTypicalPeriods=30, numberOfTimeStepsPerPeriod=24, clusterMethod='hierarchical', logFileName='', threads=3, solver='gurobi', timeLimit=None, optimizationSpecs='', warmstart=False): """ Call the optimize function for a temporally aggregated MILP (so the model has to include hasIsBuiltBinaryVariables in all or some components). Fix the binary variables and run it again without temporal aggregation. Furthermore, a LP with relaxed binary variables can be solved to obtain both, an upper and lower bound for the fully resolved MILP. **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 **Default arguments:** :param declaresOptimizationProblem: states if the optimization problem should be declared (True) or not (False). (a) If true, the declareOptimizationProblem function is called and a pyomo ConcreteModel instance is built. (b) If false a previously declared pyomo ConcreteModel instance is used. |br| * the default value is True :type declaresOptimizationProblem: boolean :param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower bound of the problem. |br| * the default value is False :type declaresOptimizationProblem: boolean :param numberOfTypicalPeriods: states the number of typical periods into which the time series data should be clustered. The number of time steps per period must be an integer multiple of the total number of considered time steps in the energy system. Note: Please refer to the tsam package documentation of the parameter noTypicalPeriods for more information. |br| * the default value is 30 :type numberOfTypicalPeriods: strictly positive integer :param numberOfTimeStepsPerPeriod: states the number of time steps per period |br| * the default value is 24 :type numberOfTimeStepsPerPeriod: strictly positive integer :param clusterMethod: states the method which is used in the tsam package for clustering the time series data. Options are for example 'averaging','k_means','exact k_medoid' or 'hierarchical'. Note: Please refer to the tsam package documentation of the parameter clusterMethod for more information. |br| * the default value is 'hierarchical' :type clusterMethod: string :param logFileName: logFileName is used for naming the log file of the optimization solver output if gurobi is used as the optimization solver. If the logFileName is given as an absolute path (e.g. logFileName = os.path.join(os.getcwd(), 'Results', 'logFileName.txt')) the log file will be stored in the specified directory. Otherwise, it will be stored by default in the directory where the executing python script is called. |br| * the default value is 'job' :type logFileName: string :param threads: number of computational threads used for solving the optimization (solver dependent input) if gurobi is used as the solver. A value of 0 results in using all available threads. If a value larger than the available number of threads are chosen, the value will reset to the maximum number of threads. |br| * the default value is 3 :type threads: positive integer :param solver: specifies which solver should solve the optimization problem (which of course has to be installed on the machine on which the model is run). |br| * the default value is 'gurobi' :type solver: string :param timeLimit: if not specified as None, indicates the maximum solve time of the optimization problem in seconds (solver dependent input). The use of this parameter is suggested when running models in runtime restricted environments (such as clusters with job submission systems). If the runtime limitation is triggered before an optimal solution is available, the best solution obtained up until then (if available) is processed. |br| * the default value is None :type timeLimit: strictly positive integer or None :param optimizationSpecs: specifies parameters for the optimization solver (see the respective solver documentation for more information). Example: 'LogToConsole=1 OptimalityTol=1e-6' |br| * the default value is an empty string ('') :type timeLimit: string :param warmstart: specifies if a warm start of the optimization should be considered (not always supported by the solvers). |br| * the default value is False :type warmstart: boolean Last edited: February 20, 2020 |br| @author: FINE Developer Team (FZJ IEK-3) """ lowerBound = None if relaxIsBuiltBinary: esM.optimize(declaresOptimizationProblem=True, timeSeriesAggregation=False, relaxIsBuiltBinary=True, logFileName='relaxedProblem', threads=threads, solver=solver, timeLimit=timeLimit, optimizationSpecs=optimizationSpecs, warmstart=warmstart) lowerBound = esM.pyM.Obj() esM.cluster(numberOfTypicalPeriods=numberOfTypicalPeriods, numberOfTimeStepsPerPeriod=numberOfTimeStepsPerPeriod, clusterMethod=clusterMethod, solver=solver, sortValues=True) esM.optimize(declaresOptimizationProblem=True, timeSeriesAggregation=True, relaxIsBuiltBinary=False, logFileName='firstStage', threads=threads, solver=solver, timeLimit=timeLimit, optimizationSpecs=optimizationSpecs, warmstart=warmstart) # Set the binary variables to the values resulting from the first optimization step fn.fixBinaryVariables(esM) esM.optimize(declaresOptimizationProblem=True, timeSeriesAggregation=False, relaxIsBuiltBinary=False, logFileName='secondStage', threads=threads, solver=solver, timeLimit=timeLimit, optimizationSpecs=optimizationSpecs, warmstart=False) upperBound = esM.pyM.Obj() if lowerBound is not None: delta = upperBound - lowerBound gap = delta / upperBound esM.lowerBound, esM.upperBound = lowerBound, upperBound esM.gap = gap print('The real optimal value lies between ' + str(round(lowerBound, 2)) + ' and ' + str(round(upperBound, 2)) + ' with a gap of ' + str(round(gap * 100, 2)) + '%.')
def test_miniSystem(): locations = {'loc1', 'loc2'} numberOfTimeSteps = 365 hoursPerTimeStep = 24 commodities = {'electricity'} commodityUnitDict = {'electricity': r'GW$_{el}$'} esM = fn.EnergySystemModel(locations=locations, commodities=commodities, numberOfTimeSteps=numberOfTimeSteps, commodityUnitsDict=commodityUnitDict, hoursPerTimeStep=hoursPerTimeStep, costUnit='Euro', lengthUnit='km', verboseLogLevel=0) costTS = pd.DataFrame([[(j % 5) * (i + 1) for i in range(len(locations))] for j in range(numberOfTimeSteps)], columns=['loc1', 'loc2']) esM.add( fn.Source(esM=esM, name='Electricity purchase', commodity='electricity', hasCapacityVariable=False, commodityCostTimeSeries=costTS)) revenueTS = pd.DataFrame([[(j % 5) * (i + 1) for i in range(len(locations))] for j in range(numberOfTimeSteps)], columns=['loc1', 'loc2']) demandTS = pd.DataFrame([[i + 1 for i in range(len(locations))] for j in range(numberOfTimeSteps)], columns=['loc1', 'loc2']) esM.add( fn.Sink(esM=esM, name='Electricity demand', commodity='electricity', operationRateFix=demandTS, hasCapacityVariable=False, commodityRevenueTimeSeries=costTS)) esM.optimize(timeSeriesAggregation=False, solver='glpk') summary = esM.getOptimizationSummary("SourceSinkModel", outputLevel=2) assert summary.loc[('Electricity demand', 'TAC', '[Euro/a]'), 'loc1'] == 730 assert summary.loc[('Electricity demand', 'TAC', '[Euro/a]'), 'loc2'] == 2920 assert summary.loc[('Electricity purchase', 'TAC', '[Euro/a]'), 'loc1'] == -730 assert summary.loc[('Electricity purchase', 'TAC', '[Euro/a]'), 'loc2'] == -2920 esM.cluster(numberOfTypicalPeriods=5, numberOfTimeStepsPerPeriod=1) esM.optimize(timeSeriesAggregation=True, solver='glpk') summary = esM.getOptimizationSummary("SourceSinkModel", outputLevel=2) assert summary.loc[('Electricity demand', 'TAC', '[Euro/a]'), 'loc1'] == 730 assert summary.loc[('Electricity demand', 'TAC', '[Euro/a]'), 'loc2'] == 2920 assert summary.loc[('Electricity purchase', 'TAC', '[Euro/a]'), 'loc1'] == -730 assert summary.loc[('Electricity purchase', 'TAC', '[Euro/a]'), 'loc2'] == -2920