简体   繁体   English

scipy.optimize.minimize SLSQP间接约束问题

[英]scipy.optimize.minimize SLSQP indirect constraints problem

I am struggling with minimize ( method=SLSQL ) and need help.我在minimize ( method=SLSQL ) 方面苦苦挣扎,需要帮助。 This is a simplified car battery (dis)charging problem to prop up the power grid during reduced stability (peak demand).这是一个简化的汽车电池(放电)充电问题,用于在稳定性降低(峰值需求)期间支撑电网。 What I expect to happen is that during such instability, the battery gets discharged as much as possible when the price of electricity is highest and charged when the price is lowest, and the grid is stable again.我期望发生的情况是,在这种不稳定期间,电池会在电价最高时尽可能多地放电,并在电价最低时充电,然后电网再次稳定。 Instead, the optimization happily says it succeeded but either didn't optimize anything or ignores one or more constraints.相反,优化很高兴地说它成功了,但要么没有优化任何东西,要么忽略了一个或多个约束。 I played around with the parameters passed to SLSQP, and when I set esp=1 , things start happening, but it never comes close to discharging the battery completely, and does not attempt to recharge (much).我尝试使用传递给 SLSQP 的参数,当我设置esp=1时,事情开始发生,但它永远不会接近电池完全放电,并且不会尝试充电(很多)。 It can happen, that it discharges the battery beyond zero to -2 or so.可能会发生,它将电池放电超过零至 -2 左右。

Here someone suggested using transformations to enforce variable limits, but in this case, it's an indirect constraint applied to the combination of several variables. 这里有人建议使用转换来强制执行变量限制,但在这种情况下,它是应用于多个变量组合的间接约束。 Are transforms still an option?转换仍然是一种选择吗?

The constraint that the battery should be recharged to 1 at the end of the cycle is ignored, too.电池应在循环结束时充电至 1 的约束也被忽略。

I imagine SLSQP is not the best search algorithm for this, and I would be thankful for suggestions for better options for the long term.我想 SLSQP 不是最好的搜索算法,对于长期更好的选择的建议,我将不胜感激。 Even telling me what kind of problem (in optimization terms) this is would be very much appreciated.即使告诉我这是什么样的问题(在优化方面),我也会非常感激。 Short term, I would be very thankful to point out how I can get SLSQP to work.短期内,我将非常感谢指出如何让 SLSQP 工作。

UPDATE: Someone told me that this might be a linear programming problem.更新:有人告诉我这可能是一个线性规划问题。 How does that work with such flexible price data if that is the case?如果是这样的话,它如何与如此灵活的价格数据一起使用? Where can I find an example explaining how to tackle it?我在哪里可以找到解释如何解决它的示例?

import numpy as np
from scipy.optimize import minimize, OptimizeResult
from typing import TypedDict

SOC_MAX = 1.0  # normalized
ONE_MINUTE= 60
CHARGING_SOC_PER_SEC = SOC_MAX / (3*60*60)  # fully charge in 3h
CHARGE_TIME_MAX_SECONDS = 3*60*60 # in seconds


time_a = np.arange(171901, step=900)    
price_a = np.array([
       143.08, 143.08, 143.08, 140.25, 140.25, 140.25, 140.25, 130.84,
       130.84, 130.84, 130.84, 130.03, 130.03, 130.03, 130.03, 138.7 ,
       138.7 , 138.7 , 138.7 , 169.95, 169.95, 169.95, 169.95, 204.37,
       204.37, 204.37, 204.37, 230.09, 230.09, 230.09, 230.09, 219.09,
       219.09, 219.09, 219.09, 207.62, 207.62, 207.62, 207.62, 213.26,
       213.26, 213.26, 213.26, 206.2 , 206.2 , 206.2 , 206.2 , 211.11,
       211.11, 211.11, 211.11, 215.9 , 215.9 , 215.9 , 215.9 , 227.28,
       227.28, 227.28, 227.28, 234.97, 234.97, 234.97, 234.97, 257.99,
       257.99, 257.99, 257.99, 274.33, 274.33, 274.33, 274.33, 236.  ,
       236.  , 236.  , 236.  , 202.95, 202.95, 202.95, 202.95, 183.63,
       183.63, 183.63, 183.63, 165.92, 165.92, 165.92, 165.92, 145.07,
       145.07, 145.07, 145.07, 165.39, 165.39, 165.39, 165.39, 152.51,
       152.51, 152.51, 152.51, 145.87, 145.87, 145.87, 145.87, 143.06,
       143.06, 143.06, 143.06, 145.38, 145.38, 145.38, 145.38, 159.61,
       159.61, 159.61, 159.61, 183.77, 183.77, 183.77, 183.77, 210.8 ,
       210.8 , 210.8 , 210.8 , 213.77, 213.77, 213.77, 213.77, 203.33,
       203.33, 203.33, 203.33, 200.97, 200.97, 200.97, 200.97, 199.02,
       199.02, 199.02, 199.02, 193.72, 193.72, 193.72, 193.72, 179.7 ,
       179.7 , 179.7 , 179.7 , 165.57, 165.57, 165.57, 165.57, 163.94,
       163.94, 163.94, 163.94, 178.01, 178.01, 178.01, 178.01, 200.93,
       200.93, 200.93, 200.93, 201.01, 201.01, 201.01, 201.01, 193.47,
       193.47, 193.47, 193.47, 165.32, 165.32, 165.32, 165.32, 135.09,
       135.09, 135.09, 135.09, 125.56, 125.56, 125.56, 125.56, 104.86,
       104.86, 104.86, 104.86, 109.41, 109.41, 109.41, 109.41, 111.09])

stability_ranges = np.array([[ 33,  53],
       [ 71, 119],
       [131, 191]])
instability_ranges = np.array([[ 21,  32],
       [ 54,  70],
       [120, 130]])

booking_dt = np.dtype([("start", int),
                       ("duration", int),
                       ("soc", float),  # state of charge of the battery
                       ("delta_soc", float),  # change in battery charge
                       ("price", float)])  # this is what the user is billed.

class OptimisationItem(TypedDict, total=False):
    start: int # this might be not needed, as its the key in the dict
    duration: int
    soc: float  # state of charge of the battery
    delta_soc: float  # change in battery charge
    price: float # this is what the user is billed.
    bound_high: int
    bound_low: int


def integrate(start, duration):
    x_start = start
    x_end = start + duration
    x_data = np.linspace(x_start, x_end, 10)
    y_interp = np.interp(x_data, time_a, price_a)
    area = np.trapz(y_interp, x_data)
    return area


"""the battery can not be charged more than 100% and not less than 0%"""
def soc_constraint(x):
    soc = SOC_MAX
    durations = x[1::2]
    for i, j in durations.reshape(-1, 2):
        soc -= i * CHARGING_SOC_PER_SEC
        if soc < 0:
            break
        soc += j * CHARGING_SOC_PER_SEC
        if soc > SOC_MAX:
            soc = SOC_MAX - soc
            break
    return soc

"""the (dis)charging should happen only within the bounds"""
def bounds_constraint(x, bounds):
    end_points= bounds - (x[::2] + x[1::2])
    smallest_end_point = np.min(end_points)
    return smallest_end_point

"""the battery should be fully charge at the end of the cycle"""
def end_constraint(x, plan):
    final_soc_gap = x[-1] * CHARGING_SOC_PER_SEC - (SOC_MAX - plan["soc"][- 1])
    return final_soc_gap

def objective_func(x: np.array, plan) -> float:
    events= x.reshape(-1, 2)

    # insert the optimizsation variables into the plan
    plan["start"] = events[:,0]
    plan["duration"] = events[:,1]

    # start values for soc=SOC_MAX and delta_soc: 0
    plan["soc"][-1] = SOC_MAX
    plan["delta_soc"][-1] = 0
    plan["duration"][-1] = 0

    # straighten out the plan, calculate price and soc
    for step in range(len(plan)):
        plan["soc"][step] = plan["soc"][step - 1] + plan["delta_soc"][step - 1]
        if  step % 2 == 0:
            plan["delta_soc"][step] = - plan["duration"][step] * CHARGING_SOC_PER_SEC
            plan["price"][step] = - integrate(plan["start"][step], plan["duration"][step])
        else:
            plan["delta_soc"][step] = plan["duration"][step] * CHARGING_SOC_PER_SEC
            plan["price"][step] = + integrate(plan["start"][step], plan["duration"][step])

    price: float = plan["price"].sum()
    return price

def optimise_discharge_plan(plan: np.array, discharge_events_d) -> np.array:
    x_initial = []
    bounds = []
    constraints = []
    bounds_high_l = []
    for time in plan["start"]:
        data_set = discharge_events_d[time]
        x_initial.append(time)
        x_initial.append(data_set["duration"])
        bound_low = data_set["bound_low"]
        bound_high = data_set["bound_high"]
        bounds_high_l.append(bound_high)
        bounds.append((bound_low, bound_high))
        bounds.append( (0, min(CHARGE_TIME_MAX_SECONDS, bound_high - bound_low)))
    bounds_high = np.array(bounds_high_l)
    constraints.append({"type": "ineq", "fun": bounds_constraint, "args": (bounds_high,)})
    constraints.append({"type": "eq", "fun": end_constraint, "args": (plan,)})
    constraints.append({"type": "ineq", "fun": soc_constraint})


    result:OptimizeResult = minimize(objective_func, np.array(x_initial),
                                     args=plan,
                                     method="SLSQP",
                                     bounds=bounds,
                                     constraints=constraints,
                                     #options={"eps": 1 , "maxiter": 1000, "ftol": 1.0, "disp": True},
                                     )
    print(result.x.reshape(-1, 4),"\n", result, "\n",plan)


def main():

    discharge_events_l:list[booking_dt] = []
    discharge_events_d: dict = {}
    for range_cnt, (instability_range, stability_range) in enumerate(zip(instability_ranges, stability_ranges)):
    # build up list of discharge times, that we can modify in the optimisation step
        if instability_range[0] == instability_range[1]:
            time_max_price = instability_range[0]
        else:
            max_index = np.argmax(price_a[instability_range[0]:instability_range[1]])
            time_max_price = time_a[instability_range[0] + max_index]
        instability_item: OptimisationItem = {"bound_low": time_a[instability_range[0]],
                                              "bound_high": time_a[instability_range[1]]+15*60 -1,
                                              "duration": ONE_MINUTE, #  inital value
                                              "start": time_max_price,}
        discharge_events_d[time_max_price] = instability_item

        if stability_range[0] == stability_range[1]:
            time_min_price = stability_range[0]
        else:
            min_index = np.argmin(price_a[stability_range[0]:stability_range[1]])
            time_min_price = time_a[stability_range[0] + min_index]

        stability_item: OptimisationItem = {"bound_low": time_a[stability_range[0]],
                                            "bound_high": time_a[stability_range[1]]+15*60 -1,
                                            "duration": ONE_MINUTE,
                                            "start": time_min_price,}
        discharge_events_d[time_min_price] = stability_item

        event: booking_dt = np.zeros(2, dtype=booking_dt)
        # discharge
        event["start"][0] = time_max_price
        event["duration"][0] = ONE_MINUTE
        # charge
        event["start"][1] = time_min_price
        event["duration"][1] = ONE_MINUTE
        discharge_events_l.append(event)

    discharge_plan = np.array(discharge_events_l, dtype=booking_dt).reshape(-1)
    optimise_discharge_plan(discharge_plan, discharge_events_d)

if __name__ == '__main__':
    main()

One minor issue why the SLSQP algorithm failed was that it was too greedy and didn't want to buy/charge the battery. SLSQP 算法失败的一个小问题是它过于贪婪,不想购买/充电电池。 I introduced a medium reference against which the algorithm buys and sells, which got it to work "well enough" for now.我引入了一个媒介参考,算法根据该参考进行买卖,这让它现在“足够好”地工作。

在此处输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM