-1

When running a stochastic programming problem in Pyomo, the resulting solution works only when running 10 precisely the same scenarios but the results remain zero when running different scenarios.

I aim to run 10 different scenarios with corresponding probabilities in a stochastic fashion. To provide some context, the problem concerns a cost-minimization battery dispatch problem with uncertainty regarding wind power production.

As said before, when running the 10 similar scenarios with corresponding probabilities 0.1 this results in the following plot. The wind scenarios are described in a [s, t] tuple-indexed dictionary. The wind parameter is indexed similarly in the objective function and constraints.

Battery dispatch with 10 similar scenarios

However, when running 10 different scenarios all with probability 0.1 the resulting plot ends up empty. The resulting plot is provided below. It says that no primal feasible solution can be found, but I'm not sure why this is the case.

Empty battery dispatch plot 10 different scenarios

Hence my question, how do I retrieve indexed results?

## Import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import pyomo.environ as pyo

load_data = ([5.76186621, 5.37067822, 5.29351123, 5.52940967, 5.39076172,
       5.43196387, 5.48100537, 5.81333301, 5.69059522, 5.48861719,
       6.69299756, 5.19573291, 5.09057129, 5.21503565, 5.21353174,
       5.39826172, 5.23854395, 5.54866699, 5.22932715, 5.57071094,
       5.68876563, 5.39178857, 5.37514307, 5.63663672, 5.73555957,
       5.81333301, 5.32496875, 5.8409707 , 5.1021416 , 5.44398438,
       5.30204785, 5.63930811, 5.50765918, 5.17532715, 5.24306397,
       5.25783154, 5.2048418 , 5.91519141, 5.90498242, 5.44157129,
       5.84772656, 5.39375586, 5.50630371, 5.41415381, 5.34967041,
       5.58918799, 4.95105859, 5.34705859]) # Electrical load of the machine [MW]
three_days = ([ 66.75,  50.74,  50.  ,  50.72,  51.3 ,  48.6 ,  64.46,  85.66,
       100.  ,  67.8 ,  50.28,  48.12,  45.57,  40.26,  39.09,  38.94,
        40.71,  44.84,  70.72,  80.  ,  80.1 ,  78.16,  55.  ,  47.92,
        46.65,  48.7 ,  47.85,  50.08,  50.  ,  46.  ,  58.  ,  72.97,
        75.12,  51.47,  45.91,  45.4 ,  42.9 ,  42.71,  41.26,  41.84,
        44.  ,  66.36,  54.09,  69.83,  88.32,  63.6 ,  58.88,  47.56]) # Electricity price data [$/MWh]
park_data = ([10.548, 15.84 , 10.548,  3.636,  6.525,  0.   ,  0.315, 10.548,
        6.525,  0.315,  0.315,  6.525,  6.525, 10.548, 10.548,  6.525,
        6.525, 10.548, 10.548, 10.548,  6.525,  6.525,  6.525,  6.525,
        6.525,  3.636,  6.525,  6.525,  6.525,  6.525,  3.636,  3.636,
        0.315,  0.315,  0.   ,  0.315,  0.315,  1.656,  3.636,  1.656,
        3.636,  1.656,  0.315,  0.   ,  0.   ,  0.   ,  0.   ,  0.   ]) # Wind power production [MW]

park_data_2 = [x*0.9 for x in park_data]
Scen_prob = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] # Scenario probabilities
Scen_prob = np.array(Scen_prob) # List to array
Scen_prob = Scen_prob.reshape(10,1) # List to array

# 10 similar scenarios
raw_wind_data = [park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data,
                 park_data]

# 10 different scenarios
# raw_wind_data = [park_data,
#                  park_data,
#                  park_data,
#                  park_data,
#                  park_data,
#                  park_data_2,
#                  park_data_2,
#                  park_data_2,
#                  park_data_2,
#                  park_data_2]

#Transform wind data to dataframe
raw_wind_df = pd.DataFrame(raw_wind_data)
raw_wind_df = raw_wind_df.transpose()
raw_wind_df.columns = ['Scenario 1', 'Scenario 2', 'Scenario 3', 'Scenario 4', 'Scenario 5', 'Scenario 6', 'Scenario 7', 'Scenario 8', 'Scenario 9', 'Scenario 10']
ax = raw_wind_df.plot()
plt.show()

def build_model(price_data, horizon_length, scenario_length, load_calc, park_calc, raw_wind_data):
    """ Builds HORIZON pyomo model
    :param raw_wind_data:
    :param scenario_length:
    :param price_data: dictionary of price data
    :param horizon_length: number of timesteps to evaluate
    :param include_pbc: Boolean, include periodic boundary condition constraint
    :param load_calc: dictionary of laod data
    :param park_calc: dictionary of windpark power production
    :return: m: pyomo model
    """
    m = pyo.ConcreteModel()
    ### BEGIN SOLUTION

    # Probability distribution per scenario
    vector = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

    ## Sets
    # Save the number of timesteps
    m.N = horizon_length
    m.S = len(scenario_length)

    # Define the horizon set starting at hour 1 until horizon length +1
    m.HORIZON = pyo.Set(initialize=range(1, m.N + 1))
    # Define scenario set
    m.SCENARIO = pyo.Set(initialize=range(1, m.S + 1))

    ## Parameters
    # Round trip efficiency
    m.teta = pyo.Param(initialize=0.95)

    # Energy [MWh] in battery at t=0
    m.E0 = pyo.Param(initialize=2.0, mutable=True)

    # Guarantee of origin for local wind [€/MWh]
    m.goNL = pyo.Param(initialize=5)

    # Guarantee of origin for grid power [€/MWh]
    m.goBE = pyo.Param(initialize=150)

    # Maximum discharge power
    m.d_max = pyo.Param(initialize=5)

    # Maximum charge power
    m.c_max = pyo.Param(initialize=5)

    # Maximum export power
    m.im_max = pyo.Param(initialize=10)

    # Maximum import power
    m.ex_max = pyo.Param(initialize=100)

    ## CREATE DICTS FOR DATA: Price, Load & Calc
    # Create empty dictionary
    price_data_dict = {}

    # Loop over Price data elements of numpy array
    for i in range(0, len(price_data)):
        # Add element to data_dict
        price_data_dict[i + 1] = price_data[i]

    # Create empty dictionary
    load_data_dict = {}

    # Loop over Load data elements of numpy array
    for i in range(0, len(load_calc)):
        # Add element to data_dict
        load_data_dict[i + 1] = load_calc[i]

    # Create empty dictionary
    park_data_dict = {}

    # Loop over Wind park data elements of numpy array
    for i in range(0, len(park_calc)):
        # Add element to data_dict
        park_data_dict[i + 1] = park_calc[i]

    # Create empty dictionary
    prob_dict = {}

    # Loop over probability data elements of numpy array
    for i in range(0, len(vector)):
        # Add element to prob_dict
        prob_dict[i + 1] = vector[i]

    wind_1 = {}
    for i in range(len(raw_wind_data)):
        for j in range(len(raw_wind_data[0])):
            wind_1[(i + 1, j + 1)] = raw_wind_data[i][j]

    # Price data
    m.price = pyo.Param(m.HORIZON, initialize=price_data_dict, domain=pyo.Reals, mutable=True)

    # Load data
    m.Load = pyo.Param(m.HORIZON, initialize=load_data_dict, domain=pyo.Reals, mutable=True)

    # Wind park data
    m.wind = pyo.Param(m.SCENARIO, m.HORIZON, initialize=wind_1, mutable=True) #park_data_dict

    # Scenario probability
    m.prob = pyo.Param(m.SCENARIO, initialize=prob_dict)  # Was Scen_prob

    ## Variables
    ## Battery related variables
    # Charging rate [MW]
    m.c = pyo.Var(m.HORIZON, initialize=0.0, bounds=(0, 10), domain=pyo.NonNegativeReals)  # initialize=0.0,

    # Discharging rate [MW]
    m.d = pyo.Var(m.HORIZON, initialize=0.0, bounds=(0, 10), domain=pyo.NonNegativeReals) # initialize=0.0,

    # Battery power
    m.Bat = pyo.Var(m.HORIZON, initialize=0.0, domain=pyo.NonNegativeReals) # initialize=0.0,

    # Binary variables charging and grid
    m.u = pyo.Var(m.HORIZON, initialize=0.0, domain=pyo.Binary) # initialize=0.0,
    m.v = pyo.Var(m.HORIZON, initialize=0.0, domain=pyo.Binary) # initialize=0.0,

    # Energy (state-of-charge) [MWh]
    m.E = pyo.Var(m.HORIZON, initialize=2.0, bounds=(0, 5), domain=pyo.NonNegativeReals) # initialize=2.0,
    m.G_im = pyo.Var(m.HORIZON, initialize=0.0, bounds=(0, 10), domain=pyo.NonNegativeReals) # initialize=0,
    m.G_ex = pyo.Var(m.HORIZON, initialize=0.0, bounds=(0, 100), domain=pyo.NonNegativeReals) # initialize=0,
    m.grid = pyo.Var(m.HORIZON, initialize=m.Load, bounds=(0, 10), domain=pyo.NonNegativeReals) #  initialize=m.Load,

    def objfun(m):
        return sum((m.price[t] + m.goBE) * m.G_im[t] + (m.price[t] + m.goNL) * sum(m.prob[s] * m.wind[s, t] for s in m.SCENARIO) for t in m.HORIZON)
    m.OBJ = pyo.Objective(rule=objfun, sense=pyo.minimize)

    def PowerBalance(m, t):
        return m.Load[t] + m.c[t] == m.grid[t] + m.d[t]

    def EnergyBalance(m, t):
        # First timestep
        if t == 1:
            return m.E[t] == m.E0 + m.c[t] * m.teta - m.d[t] / m.teta
            # Subsequent timesteps
        else:
            return m.E[t] == m.E[t - 1] + m.c[t] * m.teta - m.d[t] / m.teta

    def GridBalance(m, s, t):
        return m.grid[t] == m.wind[s, t] + m.G_im[t] - m.G_ex[t] # for s in m.SCENARIO for t in m.HORIZON)

    def ImMax(m, t):
        return m.G_ex[t] - m.v[t] * m.ex_max <= 0

    def ExMax(m, t):
        return m.G_im[t] + m.v[t] * m.im_max <= m.im_max

    def ChargeMax(m, t):
        return m.d[t] - m.u[t] * m.d_max <= 0

    def DischargeMax(m, t):
        return m.c[t] + m.u[t] * m.c_max <= m.c_max

    m.EnergyBalance_Con = pyo.Constraint(m.HORIZON, rule=EnergyBalance)
    m.PowerBalance_Con = pyo.Constraint(m.HORIZON, rule=PowerBalance)
    m.GridBalance_Con = pyo.Constraint(m.SCENARIO, m.HORIZON, rule=GridBalance)
    m.ChargeMax_Con = pyo.Constraint(m.HORIZON, rule=ChargeMax)
    m.DischargeMax_Con = pyo.Constraint(m.HORIZON, rule=DischargeMax)
    m.ImMax_Con = pyo.Constraint(m.HORIZON, rule=ImMax)
    m.ExMax_Con = pyo.Constraint(m.HORIZON, rule=ExMax)
    ## END SOLUTION
    return m

def extract_solution(m):
    '''
    Function to extract the solution from the solved pyomo model
    Inputs:
        m: Solved pyomo model
    Outputs:
        c_control: numpy array of charging power values, MW
        d_control: numpy array of discharging power values, MW
        E_control: numpy array of energy stored values, MWh
        t: numpy array of time indices from the Pyomo model, hr
    '''
    c_control = np.empty(m.N)
    d_control = np.empty(m.N)
    E_control = np.empty(m.N)
    price_control = np.empty(m.N)
    Load_control = np.empty(m.N)
    Grid_control = np.empty(m.N)
    G_im_control = np.empty(m.N)
    G_ex_control = np.empty(m.N)
    t = np.empty(m.N)
    ### BEGIN SOLUTION
    # Loop over elements of HORIZON set.
    count = 0
    for i in m.HORIZON:
        t[count] = pyo.value(i)
        # Use value( ) function to extract the solution for each variable and append to the results lists
        c_control[count] = pyo.value(m.c[i])
        # Adding negative sign to discharge for plotting
        d_control[count] = -pyo.value(m.d[i])
        E_control[count] = pyo.value(m.E[i])
        price_control[count] = pyo.value(m.price[i])
        Load_control[count] = pyo.value(m.Load[i])
        Grid_control[count] = pyo.value(m.grid[i])
        G_im_control[count] = pyo.value(m.G_im[i])
        G_ex_control[count] = pyo.value(m.G_ex[i])
        count = count + 1
    ### END SOLUTION

    ## Calculate revenue
    # include minus sign in order to have proper values [Charge = Neg][Discharge = Pos]
    all_control_sum = np.add(-c_control, -d_control)
    # original: [Charge = Pos][Discharge = Neg]
    all_control_norm = np.add(c_control, d_control)
    my_data_list = list(price_control)
    revenue_hour = [a * b for a, b in zip(all_control_sum, my_data_list)]
    revenue_hour_array = np.array(revenue_hour)
    CumRev = revenue_hour_array.cumsum()

    return c_control, d_control, Load_control, Grid_control, G_im_control, G_ex_control, t

def plot_solution(c_control, d_control, Load_control, Grid_control, G_im_control, G_ex_control, t=None):
    plt.figure()
    # Make a plot
    plt.plot(t, Grid_control, 'm.-', label='Cold ironing power')  # Energy level Was P_ci
    plt.plot(t, G_im_control, 'k.-', label='Grid import')
    plt.plot(t, G_ex_control, 'b.-', label='Grid export')
    plt.plot(t, Load_control, 'g.-', label='Load Power')
    # plt.plot(t, P_grid, 'm.-', label='Grid Power')
    plt.plot(t, park_data, 'y.-', label='Wind Power')  # was P_wind
    plt.plot(t, c_control, 'c.-', label='Charge')
    plt.plot(t, d_control, 'c.-', label='Discharge')
    plt.title('Check: P_ci is ideally lower than P_Load')
    plt.xlabel('Time (hr)')
    plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15),
               ncol=3, fancybox=True, shadow=True)
    plt.ylabel('Cold ironing and load energy [MWh]')  # Cumulative revenue [€]
    plt.xticks()  # 72 was len(three_days) and 10 was int(step_size)
    # plt.legend()
    plt.grid(True)
    plt.show()

    fig, ax = plt.subplots()
    # Make a plot
    plt.step(t, c_control, 'b.-')  # Energy level
    plt.step(t, d_control, 'b.-')
    plt.xlabel('Time (hr)')
    plt.ylabel('Energy in battery [MWh]', color="blue")  # Cumulative revenue [€]
    plt.xticks(range(0, 50, 10))  # 72 was len(three_days) and 10 was int(step_size)
    plt.grid(True)
    plt.title('Charging based on day-ahead price')
    # insert the second plot
    ax2 = ax.twinx()
    ax2.step(t, three_days, 'r.-')  # 96 was three_days
    ax2.set_ylabel("Day-ahead price [EUR/MWh]", color="red")
    # Make sure axis label fit
    plt.tight_layout()
    plt.show()

    fig, ax = plt.subplots()
    # Make a plot
    plt.step(t, c_control, 'b.-')  # Energy level
    plt.step(t, d_control, 'b.-')
    plt.xlabel('Time (hr)')
    plt.ylabel('Energy in battery [MWh]', color="blue")  # Cumulative revenue [€]
    plt.xticks(range(0, 50, 10))  # 72 was len(three_days) and 10 was int(step_size)
    plt.grid(True)
    plt.title('Charging and Wind speeds')
    # insert the second plot
    ax2 = ax.twinx()
    ax2.step(t, park_data, 'k.-')  # 96 was three_days
    ax2.set_ylabel("Wind power production [MW]", color="black")
    # Make sure axis label fit
    plt.tight_layout()
    plt.show()
    return

## BEGIN SOLUTION
# Build model
m_og = build_model(three_days, len(three_days), Scen_prob, load_data, park_data, raw_wind_data)
# Specify the solver
solver = pyo.SolverFactory('glpk')
results = solver.solve(m_og, tee=True)
# Extract solution
c_control, d_control, Load_control, Grid_control, G_im_control, G_ex_control, t = extract_solution(m_og)
plot_solution(c_control, d_control, Load_control, Grid_control, G_im_control, G_ex_control, t)
  • This question is a mess. You have about 7 questions rolled into 1 here... results are empty, infeasible, there are "R" and "S" that are not defined, there is claim that initializing changes behavior.... Suggest you narrow this down to **1 issue** and provide code necessary to replicate the *exact* problem you are having. There is no data here and there is no `solve` statement so it is near impossible to QA any results. Also, for clarity, this is a 1-stage model and you are running 1 "blended" scenario (in the obj function) as far as I can see – AirSquid Oct 07 '22 at 15:40
  • Please trim your code to make it easier to find your problem. Follow these guidelines to create a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Community Oct 07 '22 at 15:56
  • It is still not possible to replicate any of the issues you are having. Suggest you remove all unnecessary code relative to the extraction of the solution, and just include enough data to run the model under the conditions you are concerned about. The example should be a minimal, reproducible example. https://stackoverflow.com/help/minimal-reproducible-example – AirSquid Oct 07 '22 at 22:02
  • @AirSquid ; Thanks you for your feedback, I edited my code input and questions. Hopefully this is clear enough, and you are able to help. – Thomas Weenk Oct 09 '22 at 14:30

1 Answers1

0

OK. Got something breathing...

You have a math/logic problem in your model. First though, you said that the model was "empty" which I assumed meant producing zeros when you had differing "wind" scenarios...that is not quite true. It is INFEASIBLE when the wind scenarios differ, and there is a significant difference there. Be sure you are checking the solver status carefully when troubleshooting.

Here's your problem:

def GridBalance(m, s, t):
        return m.grid[t] == m.wind[s, t] + m.G_im[t] - m.G_ex[t] # for s in m.SCENARIO for t in m.HORIZON)

this "hard equality" (with equals sign) cannot be simultaneously true for multiple wind values that differ in different scenarios. You'll have to reformulate or reconsider how you are handling scenarios.

I used this for testing with random wind 0-10. You may find it useful:

raw_wind_data = [  [random()*10 for _ in range(len(park_data))] 
                        for scenario in range(10)]
AirSquid
  • 10,214
  • 2
  • 7
  • 31