├── Benders_decomposition ├── KKTmatrix.py ├── MGCCGKKT.py ├── README.md └── twostageMG.py /Benders_decomposition: -------------------------------------------------------------------------------- 1 | from KKTmatrix import c,G, M, E, h,G1, M1, E1, h1 2 | from gurobipy import * 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | # 以KKT方法求解 6 | # 建立主问题 7 | tral = [] 8 | LB = -GRB.INFINITY 9 | UB = GRB.INFINITY 10 | lb = [] 11 | ub = [] 12 | MP = Model("MP") 13 | x = MP.addMVar((48,),vtype=GRB.BINARY,name='x_MP') 14 | y_mp = MP.addMVar((240,), lb=-GRB.INFINITY, name='y_mp') 15 | u_mp = MP.addMVar((48,),vtype=GRB.BINARY,name='u_mp') 16 | alpha = MP.addMVar((1,),obj=1,vtype=GRB.CONTINUOUS,name='alpha') 17 | MP.addConstr(alpha>=c.T@y_mp) 18 | MP.addConstr(G@y_mp >= h-M@u_mp-E@x, name="G1") 19 | MP.addConstr(G1@y_mp == h1-M1@u_mp-E1@x, name="G2") 20 | MP.optimize() 21 | MP_obj = MP.ObjVal 22 | LB = max(MP_obj, LB) 23 | 24 | bigM = 10**4 25 | k = 1 26 | SP = Model('SP') 27 | y = SP.addMVar((240,), lb=-GRB.INFINITY, name='y') 28 | u = SP.addMVar((48,),vtype=GRB.BINARY,name='u') 29 | pi1 = SP.addMVar((G.shape[0],), lb=-GRB.INFINITY,vtype=GRB.CONTINUOUS, name='pi1') 30 | pi2 = SP.addMVar((G1.shape[0],),lb=-GRB.INFINITY, vtype=GRB.CONTINUOUS, name='pi2') 31 | v = SP.addMVar((G.shape[0],), vtype=GRB.BINARY, name='v') 32 | w = SP.addMVar((G.shape[1],), vtype=GRB.BINARY, name='w') 33 | 34 | G11 = SP.addConstr(G@y >= h-M@u-E@x.x, name="G1") 35 | G2 = SP.addConstr(G1@y == h1-M1@u-E1@x.x, name="G2") 36 | 37 | SP.addConstr(G.T@pi1+G1.T@pi2<=c, name='pi') 38 | 39 | SP.addConstr(pi1 <= bigM*v, name='v1') 40 | G3 = SP.addConstr(G@y-h+E@x.x+M@u <= bigM*(1-v), name='v2') 41 | 42 | SP.addConstr(y <= bigM*w, name='w1') 43 | 44 | SP.addConstr(c-G.T@pi1-G1.T@pi2 <= bigM*(1-w), name='w2') 45 | 46 | SP.addConstr(y>=0) 47 | SP.addConstr(pi1>=0) 48 | SP.setObjective(c@y, GRB.MAXIMIZE) 49 | SP.optimize() 50 | UB = min(SP.objVal, UB) 51 | tral.append(abs(UB-LB)) 52 | lb.append(LB) 53 | ub.append(UB) 54 | # while abs(UB-LB) >= epsilon: 55 | for _ in range(2): 56 | MP.reset() 57 | # add x^{k+1} 58 | y_new = MP.addMVar((240,), lb=-GRB.INFINITY,vtype=GRB.CONTINUOUS) 59 | # eta>=bTx^{k+1} 60 | MP.addConstr(alpha >= c.T@y_new) 61 | MP.addConstr(G@y_new>=h-E@x-M@u.x) 62 | MP.addConstr(G1@y_new==h1-E1@x-M1@u.x) 63 | # Ey+Gx^{k+1}>=h-Mu_{k+1} 64 | SP.reset() 65 | MP.optimize() 66 | MP_obj = MP.objval 67 | LB = max(LB, MP_obj) 68 | SP.remove(G11) 69 | SP.remove(G2) 70 | SP.remove(G3) 71 | SP.update() 72 | G11 = SP.addConstr(G@y >= h-M@u-E@x.x, name="G1") 73 | G2 = SP.addConstr(G1@y == h1-M1@u-E1@x.x, name="G2") 74 | G3 = SP.addConstr(G@y-h+E@x.x+M@u <= bigM*(1-v), name='v2') 75 | SP.update() 76 | SP.optimize() 77 | # obtain the optimal y^{k+1} 78 | SP_obj = SP.ObjVal 79 | UB = min(UB, SP_obj) 80 | k += 1 81 | tral.append(abs(UB-LB)) 82 | lb.append(LB) 83 | ub.append(UB) 84 | if abs(UB-LB)<=10: 85 | break 86 | # go back to the MP 87 | print("经过{}次迭代".format(k)) 88 | print("上界为:{}".format(UB)) 89 | print("下界为:{}".format(LB)) 90 | -------------------------------------------------------------------------------- /KKTmatrix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Apr 14 19:43:04 2023 4 | 5 | @author: wyx 6 | """ 7 | from twostageMG import * 8 | 9 | A = model.getA().toarray() # 10 | b = np.array(model.RHS) 11 | sense = np.array(model.sense) 12 | f = np.array(model.obj) 13 | 14 | # 2. 加工系数矩阵,将A分为不等式约束Aineq和等式约束Aeq两块 15 | Aeq = A[sense == '=', :] # Submatrix corresponding to equality constraints 16 | beq = b[sense == '='] # RHS for equality constraints 17 | 18 | # Submatrix corresponding to less-or-equal constraints 19 | Ale = A[sense == '<', :] 20 | ble = b[sense == '<'] # RHS for LE constraints 21 | 22 | # Submatrix corresponding to greater-or-equal constraints 23 | Age = A[sense == '>', :] 24 | bge = b[sense == '>'] # RHS for GE constraints 25 | 26 | Aineq = np.vstack((-Ale, Age)) # 把所有的<=和>=组合在一起 27 | bineq = np.append(-ble, bge) # 这里用append使bineq为一个一维矩阵,而不是2行1列的二维矩阵,避免后面的运行错误 28 | # 提取第一阶段变量 29 | num_1 = len(Us_t)+len(Um_t) 30 | num_2 = len(Pg_t)+len(Ps_ch_t)+len(Ps_dis_t)+len(Pdr_t)+len(Pdr1_t)+len(Pdr2_t)+len(Pbuy_t)+len(Psell_t)+len(Ppv_t)+len(Pl_t) 31 | num_3 = len(B_pv_t)+len(B_l_t) 32 | 33 | G, M, E, h = [], [], [], [] 34 | G1, M1, E1, h1 = [], [], [], [] 35 | for row in np.arange(Aineq.shape[0]): 36 | # 第一阶段变量 37 | if not np.all(Aineq[row][num_1:] == 0): #第一阶段独有约束 38 | E.append(Aineq[row][:num_1]) #x 39 | G.append(Aineq[row][num_1:num_1+num_2]) #y 40 | M.append(Aineq[row][num_1+num_2:]) #u 41 | h.append(bineq[row]) #b 42 | for row in np.arange(Aeq.shape[0]): 43 | # 第一阶段变量 44 | if not np.all(Aeq[row][num_1:] == 0): #第一阶段独有约束 45 | E1.append(Aeq[row][:num_1]) #x 46 | G1.append(Aeq[row][num_1:num_1+num_2]) #y 47 | M1.append(Aeq[row][num_1+num_2:]) #u 48 | h1.append(beq[row]) #b 49 | 50 | G, M, E, h = np.array(G),np.array(M),np.array(E),np.array(h) 51 | G1, M1, E1, h1 = np.array(G1),np.array(M1),np.array(E1),np.array(h1) 52 | PVL = np.append(P_PV0, P_L0) 53 | # 建立矩阵模型 54 | m = Model('specification') 55 | x = m.addMVar((48,),vtype=GRB.BINARY,name='x') 56 | y = m.addMVar((240,),lb=-GRB.INFINITY,name='y') 57 | u = m.addMVar((48,),vtype=GRB.BINARY,name='u') 58 | f = np.array(model.obj) 59 | c = f[num_1:num_1+num_2] 60 | # 构建确定性模型 61 | m.setObjective(c@y,GRB.MINIMIZE) 62 | m.addConstr(G@y>=h-E@x-M@u) 63 | m.addConstr(G1@y==h1-E1@x-M1@u) 64 | m.optimize() 65 | 66 | if abs(m.objVal-model.objVal)<=0.000001: 67 | print("矩阵形式的模型与元素形式的模型一致") 68 | else: 69 | print("模型构建不一致,需要检查") 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /MGCCGKKT.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Apr 14 19:37:48 2023 4 | 5 | @author: wyx 6 | """ 7 | 8 | from KKTmatrix import c,G, M, E, h,G1, M1, E1, h1 9 | from gurobipy import * 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | # 以KKT方法求解 13 | # 建立主问题 14 | tral = [] 15 | LB = -GRB.INFINITY 16 | UB = GRB.INFINITY 17 | lb = [] 18 | ub = [] 19 | MP = Model("MP") 20 | x = MP.addMVar((48,),vtype=GRB.BINARY,name='x_MP') 21 | y_mp = MP.addMVar((240,), lb=-GRB.INFINITY, name='y_mp') 22 | u_mp = MP.addMVar((48,),vtype=GRB.BINARY,name='u_mp') 23 | alpha = MP.addMVar((1,),obj=1,vtype=GRB.CONTINUOUS,name='alpha') 24 | MP.addConstr(alpha>=c.T@y_mp) 25 | MP.addConstr(G@y_mp >= h-M@u_mp-E@x, name="G1") 26 | MP.addConstr(G1@y_mp == h1-M1@u_mp-E1@x, name="G2") 27 | MP.optimize() 28 | MP_obj = MP.ObjVal 29 | LB = max(MP_obj, LB) 30 | 31 | bigM = 10**4 32 | k = 1 33 | SP = Model('SP') 34 | y = SP.addMVar((240,), lb=-GRB.INFINITY, name='y') 35 | u = SP.addMVar((48,),vtype=GRB.BINARY,name='u') 36 | pi1 = SP.addMVar((G.shape[0],), lb=-GRB.INFINITY,vtype=GRB.CONTINUOUS, name='pi1') 37 | pi2 = SP.addMVar((G1.shape[0],),lb=-GRB.INFINITY, vtype=GRB.CONTINUOUS, name='pi2') 38 | v = SP.addMVar((G.shape[0],), vtype=GRB.BINARY, name='v') 39 | w = SP.addMVar((G.shape[1],), vtype=GRB.BINARY, name='w') 40 | l = SP.addMVar((G1.shape[0],), vtype=GRB.BINARY, name='l') 41 | 42 | G11 = SP.addConstr(G@y >= h-M@u-E@x.x, name="G1") 43 | G2 = SP.addConstr(G1@y == h1-M1@u-E1@x.x, name="G2") 44 | 45 | SP.addConstr(G.T@pi1+G1.T@pi2<=c, name='pi') 46 | 47 | SP.addConstr(pi1 <= bigM*v, name='v1') 48 | G3 = SP.addConstr(G@y-h+E@x.x+M@u <= bigM*(1-v), name='v2') 49 | SP.addConstr(pi2 <= bigM*l, name='l1') 50 | G4 = SP.addConstr(G1@y-h1+E1@x.x+M1@u <= bigM*(1-l), name='l2') 51 | SP.addConstr(y <= bigM*w, name='w1') 52 | 53 | SP.addConstr(c-G.T@pi1-G1.T@pi2 <= bigM*(1-w), name='w2') 54 | 55 | SP.addConstr(y>=0) 56 | SP.addConstr(pi1>=0) 57 | SP.setObjective(c@y, GRB.MAXIMIZE) 58 | SP.optimize() 59 | UB = min(SP.objVal, UB) 60 | tral.append(abs(UB-LB)) 61 | lb.append(LB) 62 | ub.append(UB) 63 | # while abs(UB-LB) >= epsilon: 64 | for _ in range(5): 65 | MP.reset() 66 | # add x^{k+1} 67 | y_new = MP.addMVar((240,), lb=-GRB.INFINITY,vtype=GRB.CONTINUOUS) 68 | # eta>=bTx^{k+1} 69 | MP.addConstr(alpha >= c.T@y_new) 70 | MP.addConstr(G@y_new>=h-E@x-M@u.x) 71 | MP.addConstr(G1@y_new==h1-E1@x-M1@u.x) 72 | # Ey+Gx^{k+1}>=h-Mu_{k+1} 73 | SP.reset() 74 | MP.optimize() 75 | MP_obj = MP.objval 76 | LB = max(LB, MP_obj) 77 | SP.remove(G11) 78 | SP.remove(G2) 79 | SP.remove(G3) 80 | SP.remove(G4) 81 | SP.update() 82 | G11 = SP.addConstr(G@y >= h-M@u-E@x.x, name="G1") 83 | G2 = SP.addConstr(G1@y == h1-M1@u-E1@x.x, name="G2") 84 | G3 = SP.addConstr(G@y-h+E@x.x+M@u <= bigM*(1-v), name='v2') 85 | G4 = SP.addConstr(G1@y-h1+E1@x.x+M1@u <= bigM*(1-l), name='l2') 86 | SP.update() 87 | SP.optimize() 88 | # obtain the optimal y^{k+1} 89 | SP_obj = SP.ObjVal 90 | UB = min(UB, SP_obj) 91 | k += 1 92 | tral.append(abs(UB-LB)) 93 | lb.append(LB) 94 | ub.append(UB) 95 | if abs(UB-LB)<=10: 96 | break 97 | # go back to the MP 98 | print("经过{}次迭代".format(k)) 99 | print("上界为:{}".format(UB)) 100 | print("下界为:{}".format(LB)) 101 | 102 | def plot_figure(): 103 | plt.figure(1) 104 | Pg = plt.bar(range(24),y.x[:24]) 105 | plt.figure(2) 106 | Ps_ch = plt.bar(range(24),y.x[24:48]) 107 | plt.figure(3) 108 | Ps_dis = plt.bar(range(24),y.x[48:72]) 109 | plt.figure(4) 110 | Pdr = plt.bar(range(24),y.x[72:96]) 111 | plt.figure(5) 112 | Pbuy = plt.plot(range(24),y.x[144:168]) 113 | Psell = plt.plot(range(24),y.x[168:192]) 114 | plt.figure(6) 115 | Ppv = plt.plot(range(24),y.x[192:216]) 116 | Pl = plt.plot(range(24),y.x[216:240]) 117 | plot_figure() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # two-stage-robust-MG 2 | 3 | 4 | 5 | 复现中国电机工程学报《微电网两阶段鲁棒优化经济调度方法》 6 | 7 | 根据文中的强对偶理论编程求解时出现了一些问题,因此重新推导了模型的KKT条件进行求解 8 | 9 | 语言:Python 3.10.1 + Gurobi 10.0.1 10 | 11 | 程序说明:twostageMG.py为非紧凑形式的约束,KKTmatrix.py将非紧凑形式的约束转化为紧凑形式,MGCCGKKT为采用KKT方法的CCG两阶段鲁棒求解程序,运行MGCCGKKT.py即可,如果想采用benders分解可以运行benders_decomposition.py 12 | 13 | ![image](https://user-images.githubusercontent.com/51228607/232202948-6b38c3f2-0d30-403a-bfc7-0d0c1014b106.png) 14 | 15 | 可控分布式电源出力 16 | 17 | ![image](https://user-images.githubusercontent.com/51228607/232203153-e5c4c9cf-462e-41c5-a436-24bc2c8ca893.png) 18 | 19 | 光伏出力 20 | 21 | ![image](https://user-images.githubusercontent.com/51228607/232203168-4ec9a185-1041-4f85-b580-3993cd200758.png) 22 | 23 | 负荷 24 | -------------------------------------------------------------------------------- /twostageMG.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Apr 13 19:09:18 2023 4 | 5 | @author: wyx 6 | """ 7 | # 导入 8 | from gurobipy import * 9 | import numpy as np 10 | 11 | # 定义常量 12 | dT = 1 13 | PG_max = 800 # DG出力上限 14 | PG_min = 80 # DG出力下限 15 | a = 0.67 # 成本系数 16 | b = 0 # 成本系数 17 | K_S = 0.38 # 单位充放电成本 18 | PS_max = 500 # 充放电功率上限 19 | ES_max = 1800 # 荷电状态上限 20 | ES_min = 400 # 荷电状态下限 21 | ES0 = 1000 # 初始荷电状态 22 | eta = 0.95 # 储能充放电效率 23 | K_DR = 0.32 # 单位调度成本 24 | D_DR = 2940 # 总用电需求 25 | D_DR_max = 200 # 最大用电需求 26 | D_DR_min = 50 # 最小用电需求 27 | P_DR0=[80,70,60,50,70,70,90,100,120,150,170,200,140,100,100,120,140,150,190,200,200,190,100,80] 28 | PM_max = 1500 # 最大交互功率 29 | lambda1 = [0.48, 0.48, 0.48, 0.48, 0.48, 0.48, 0.48, 0.9, 1.35, 1.35, 1.35, 30 | 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 1.35, 1.35, 1.35, 1.35, 1.35, 0.48] 31 | 32 | # 不确定 33 | P_PV0=np.array([0,0,0,0,0,0.0465,0.1466,0.3135,0.4756,0.5213,0.6563,1,0.7422,0.6817,0.4972,0.4629, 34 | 0.2808,0.0948,0.0109,0,0,0,0,0])*1500 35 | P_L0=np.array([0.4658,0.4601,0.5574,0.5325,0.5744,0.6061,0.6106,0.6636,0.741,0.708,0.7598,0.8766,0.7646, 36 | 0.7511,0.6721,0.5869,0.6159,0.6378,0.6142,0.6752,0.6397,0.5974,0.5432,0.4803])*1200 37 | delta_u_PV=0.15 38 | delta_u_L=0.1 39 | tau_L = 12 40 | tau_PV = 6 41 | # 定义变量 42 | T_set = np.arange(24) 43 | model = Model('MGs') 44 | # 第一阶段 45 | Us_t = model.addVars(T_set, vtype=GRB.BINARY, name='Us_t') 46 | Um_t = model.addVars(T_set, vtype=GRB.BINARY, name='Um_t') 47 | # 第二阶段 48 | Pg_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pg_t') 49 | Ps_ch_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Ps_ch_t') 50 | Ps_dis_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Ps_dis_t') 51 | Pdr_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pdr_t') 52 | Pdr1_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pdr1_t') 53 | Pdr2_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pdr2_t') 54 | Pbuy_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pbuy_t') 55 | Psell_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Psell_t') 56 | Ppv_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Ppv_t') 57 | Pl_t = model.addVars(T_set,lb=-GRB.INFINITY, name='Pl_t') 58 | # 不确定 59 | B_pv_t = model.addVars(T_set,vtype=GRB.BINARY,name='B_pv_t') 60 | B_l_t = model.addVars(T_set,vtype=GRB.BINARY,name='B_l_t') 61 | 62 | model.addConstrs((Pg_t[t]>=PG_min for t in T_set),name='min-Pg_t') 63 | model.addConstrs((Pg_t[t]<=PG_max for t in T_set),name='max-Pg_t') 64 | model.addConstrs((Pdr_t[t]<=D_DR_max for t in T_set),name='max-Pdr_t') 65 | model.addConstrs((Pdr_t[t]>=D_DR_min for t in T_set),name='min-Pdr_t') 66 | model.addConstrs((Pdr1_t[t]>=0 for t in T_set),name='Pdr1_t') 67 | model.addConstrs((Pdr2_t[t]>=0 for t in T_set),name='Pdr2_t') 68 | model.addConstrs((Pbuy_t[t]>=0 for t in T_set),name='Pbuy_t') 69 | model.addConstrs((Psell_t[t]>=0 for t in T_set),name='Psell_t') 70 | model.addConstrs((Ppv_t[t]>=0 for t in T_set),name='Ppv_t') 71 | model.addConstrs((Pl_t[t]>=0 for t in T_set),name='Pl_t') 72 | model.addConstrs((Ps_dis_t[t]>=0 for t in T_set),name='Ppv_t') 73 | model.addConstrs((Ps_ch_t[t]>=0 for t in T_set),name='Pl_t') 74 | 75 | 76 | # 添加约束 77 | # 可控分布式电源 78 | obj1 = quicksum((a*Pg_t[t]+b) for t in T_set) 79 | # 储能 80 | obj2 = quicksum(K_S*(Ps_dis_t[t]/eta+Ps_ch_t[t]*eta) for t in T_set) 81 | model.addConstrs( 82 | (Ps_dis_t[t] <= Us_t[t]*PS_max for t in T_set), name='Ps_disminmax') 83 | model.addConstrs((Ps_ch_t[t] <= (1-Us_t[t]) * 84 | PS_max for t in T_set), name='Ps_disminmax') 85 | 86 | model.addConstr(eta*quicksum(Ps_ch_t[t] for t in T_set) - 87 | 1/eta*quicksum(Ps_dis_t[t] for t in T_set) == 0, name='capacity') 88 | 89 | model.addConstrs((ES0+eta*quicksum(Ps_ch_t[t]*dT for t in range(0, t_hat))-1/eta*quicksum( 90 | Ps_dis_t[t]*dT for t in range(0, t_hat)) >= ES_min for t_hat in T_set), name='SOCmin') 91 | model.addConstrs((ES0+eta*quicksum(Ps_ch_t[t]*dT for t in range(0, t_hat))-1/eta*quicksum( 92 | Ps_dis_t[t]*dT for t in range(0, t_hat)) <= ES_max for t_hat in T_set), name='SOCmax') 93 | # 需求响应负荷 94 | obj3 = quicksum(K_DR*(Pdr1_t[t]+Pdr2_t[t]) for t in T_set) 95 | 96 | model.addConstr(quicksum(Pdr_t[t] for t in T_set)==D_DR) 97 | model.addConstrs((Pdr_t[t]-P_DR0[t]+Pdr1_t[t]-Pdr2_t[t]==0 for t in T_set),name='PDRauc') 98 | 99 | # 电网交互功率 100 | obj4 = quicksum(lambda1[t]*(Pbuy_t[t]-Psell_t[t]) for t in T_set) 101 | model.addConstrs((Pbuy_t[t]-Psell_t[t]==Ps_ch_t[t]+Pdr_t[t]+Pl_t[t]-Pg_t[t]-Ps_dis_t[t]-Ppv_t[t] for t in T_set),name='Pbalance') 102 | # Importance!!! 因为Pl和Ppv为不确定参数,所以相当于变量 103 | model.addConstrs((Pbuy_t[t]<=Um_t[t]*PM_max for t in T_set),name='PMbuy') 104 | model.addConstrs((Psell_t[t]<=(1-Um_t[t])*PM_max for t in T_set),name='PMsell') 105 | 106 | # 不确定参数 107 | model.addConstrs((Ppv_t[t]==P_PV0[t]-B_pv_t[t]*delta_u_PV for t in T_set),name='uncertainty-1') 108 | model.addConstrs((Pl_t[t]==P_L0[t]+B_l_t[t]*delta_u_L for t in T_set),name='uncertainty-2') 109 | model.addConstr(quicksum(B_pv_t[t]for t in T_set)<=tau_PV,name='B-1') 110 | model.addConstr(quicksum(B_l_t[t]for t in T_set)<=tau_L,name='B-2') 111 | 112 | # 设置目标函数 113 | model.setObjective(obj1+obj2+obj3+obj4,GRB.MINIMIZE) 114 | 115 | # 优化 116 | model.optimize() 117 | # model.update() 118 | 119 | 120 | --------------------------------------------------------------------------------