Exemple #1
0
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
Exemple #3
0
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,
        ))
Exemple #4
0
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)
Exemple #5
0
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()
Exemple #6
0
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,
        )
Exemple #7
0
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
Exemple #8
0
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,
        )
Exemple #9
0
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
Exemple #10
0
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,
        ))
Exemple #11
0
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
Exemple #14
0
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,)
Exemple #15
0
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
Exemple #16
0
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
Exemple #18
0
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,
        ))
Exemple #19
0
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]
    )
Exemple #21
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
Exemple #22
0
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)
Exemple #23
0
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)
Exemple #24
0
    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,
Exemple #27
0
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
Exemple #28
0
    "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