0

Background - I'm trying to optimize the operation of a battery energy storage system (BESS). I've used the This Github Project as the basis.

Problem - Most BESS systems have a depth of discharge limit. I.e. for a 400 MWh battery, you can only use about 320 MWhs of it. The last 20% is essentially reserved/unusable. I've been trying to add this as an additional constraint but I've had no luck

What I've tried

I've tried putting in bounds to my Battery Storage Param, I've tried adding a constraint (as below) but both have resulted in the error 'No value for uninitialized NumericValue object Ein[0]' (Ein being the param for Energy In)

Attempt 1

model.S = Var(model.T, bounds=(model.Smin, model.Smax))

Attempt 2

def dod_limit(model, t):
    "Depth of Discharge limit"
    return model.Smin[t] <= model.S[t]

model.dod = Constraint(model.T, rule=dod_limit)

Help Requested

Does anyone know how I can write the constraint for the depth of discharge limit?

Full code for completeness

# ---------------------
# Main Functions
# ---------------------
def model_to_df(model, first_period, last_period):
    """
    Create a dataframe with hourly charge, discharge, state of charge, and
    price columns from a pyomo model. Only uses data from between the first
    (inclusive) and last (exclusive) hours.

    Parameters
    ----------
    model : pyomo model
        Model that has been solved

    first_period : int
        First hour of data to keep
    last_period: int
        The final hour of data to keep

    Returns
    -------
    dataframe

    """
    # Need to increase the first & last hour by 1 because of pyomo indexing
    # and add 1 to the value of last model hour because of the range
    # second model hour by 1
    periods = range(model.T[first_period + 1], model.T[last_period + 1] + 1)
    Ein = [value(model.Ein[i]) for i in periods]
    Eout = [value(model.Eout[i]) for i in periods]
    rrp = [model.P.extract_values()[None][i] for i in periods]
    charge_state = [value(model.S[i]) for i in periods]

    df_dict = dict(
        period=periods,
        Ein=Ein,
        Eout=Eout,
        rrp=rrp,
        charge_state=charge_state
    )

    df = pd.DataFrame(df_dict)

    return df


def optimize_year(df, mwh, mw, ef, soc, first_model_period, last_model_period):
    """
    Optimize the charge/discharge behavior of a battery storage unit over a
    full year. Assume perfect foresight of electricity prices. The battery
    has a discharge constraint equal to its storage capacity and round-trip
    efficiency of 85%.

    Parameters
    ----------
    df : dataframe
        dataframe with columns of hourly LBMP and the hour of the year
    mwh : int
        size of bess
    mw : int
        storage capacity of bess
    ef  : double
        round trip efficiency of bess
    soc  : double
        State of Charge
    first_model_period : int, optional
        Set the first hour of the year to be considered in the optimization
        (the default is 0)
    last_model_period : int, optional
        Set the last hour of the year to be considered in the optimization (the
        default is 8759)

    Returns
    -------
    dataframe
        hourly state of charge, charge/discharge behavior, lbmp, and time stamp
    """

    # Filter the data
    df = df.loc[first_model_period:last_model_period, :]

    model = ConcreteModel()

    # Define model parameters
    model.T = Set(initialize=df.period.tolist(), doc='periods of year', ordered=True)
    model.Rmax = Param(initialize=mw / 2, doc='Max rate of power flow (MW) in or out', within=Any)
    model.Smax = Param(initialize=mwh, doc='Max storage (MWh)', within=Any)
    model.Smin = Param(initialize=mwh * 0.2, doc='Min storage (MWh)', within=Any)
    model.Dmax = Param(initialize=mwh * np.sqrt(ef) * 0.8, doc='Max discharge in 24 hour period', within=Any)
    model.P = Param(initialize=df.rrp.tolist(), doc='price for each period', within=Any)
    eta = ef  # Round trip storage efficiency

    # Charge, discharge, and state of charge
    # Could use bounds for the first 2 instead of constraints
    model.Ein = Var(model.T, domain=NonNegativeReals)
    model.Eout = Var(model.T, domain=NonNegativeReals)
    model.S = Var(model.T, bounds=(model.Smin, model.Smax))

    # Set all constraints
    def storage_state(model, t):
        'Storage changes with flows in/out and efficiency losses'
        # Set first period state of charge to 0
        if t == model.T.first():
            return model.S[t] == soc
        else:
            return (model.S[t] == (model.S[t - 1]
                                   + (model.Ein[t - 1] * np.sqrt(eta))
                                   - (model.Eout[t - 1] / np.sqrt(eta))))

    model.charge_state = Constraint(model.T, rule=storage_state)

    def dod_limit(model, t):
        "Depth of Discharge limit"
        return model.Smin[t] <= model.S[t]

    model.dod = Constraint(model.T, rule=dod_limit)

    def discharge_constraint(model, t):
        "Maximum dischage within a single period"
        return model.Eout[t] <= model.Rmax

    model.discharge = Constraint(model.T, rule=discharge_constraint)

    def charge_constraint(model, t):
        "Maximum charge within a single period"
        return model.Ein[t] <= model.Rmax

    model.charge = Constraint(model.T, rule=charge_constraint)

    # Without a constraint the model would discharge in the final period
    # even when SOC was 0.
    def positive_charge(model, t):
        'Limit discharge to the amount of charge in battery, including losses'
        return model.Eout[t] <= model.S[t] * np.sqrt(eta)

    model.positive_charge = Constraint(model.T, rule=positive_charge)

    def discharge_limit(model, t):
        "Limit on discharge within a 24 hour period"
        max_t = model.T.last()

        # Check all t until the last 24 hours
        if t < max_t-24:
            return sum(model.Eout[i] for i in range(t, t+24)) <= model.Dmax
        else:
            return Constraint.Skip

    model.limit_out = Constraint(model.T, rule=discharge_limit)

    # Define the battery income, expenses, and profit
    income = sum(df.loc[t, 'rrp'] * model.Eout[t] for t in model.T)
    expenses = sum(df.loc[t, 'rrp'] * model.Ein[t] for t in model.T)
    profit = income - expenses
    model.objective = Objective(expr=profit, sense=maximize)

    # Solve the model
    solver = SolverFactory('glpk')
    solver.solve(model)

    results_df = model_to_df(model, first_period=first_model_period, last_period=last_model_period)
    results_df['local_time'] = df.loc[:, 'local_time']

    return results_df

2 Answers 2

1

Either of those 2 approaches should work. Your model isn't reproducible without some work, so not quick to verify.

On your second approach, and what you have in your code, realize that your model.Smin is not indexed. It is just a constant. So you do have an error here:

def dod_limit(model, t):
    "Depth of Discharge limit"
    return model.Smin[t] <= model.S[t]

It should be just:

    return model.Smin <= model.S[t]  # note model.Smin is not indexed

I'm concerned that the error you are showing is for something else. If this fix above doesn't fix things, can you edit your post with the full stack trace ?

Sign up to request clarification or add additional context in comments.

Comments

0

figured it out. The two methods I described above in my question do indeed work, I just needed to set initial values for my variables.

model.Ein = Var(model.T, bounds=(0, model.Rmax), initialize=0)
model.Eout = Var(model.T, bounds=(0, model.Rmax), initialize=0)
model.S = Var(model.T, bounds=(model.Smin, model.Smax), initialize=soc)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.