def unit_commitment_model(): model = ConcreteModel() # Define input files xlsx = pd.ExcelFile( f"{Path(__file__).parent.absolute()}/input/unit_commitment.xlsx", engine="openpyxl", ) system_demand = Helper.read_excel(xlsx, "SystemDemand") storage_systems = Helper.read_excel(xlsx, "StorageSystems") generators = Helper.read_excel(xlsx, "Generators") generator_step_size = Helper.read_excel(xlsx, "GeneratorStepSize") generator_step_cost = Helper.read_excel(xlsx, "GeneratorStepCost") pv_generation = Helper.read_excel(xlsx, "PVGeneration") # Define sets model.T = Set(ordered=True, initialize=system_demand.index) model.I = Set(ordered=True, initialize=generators.index) model.F = Set(ordered=True, initialize=generator_step_size.columns) model.S = Set(ordered=True, initialize=storage_systems.index) # Define parameters model.Pmax = Param(model.I, within=NonNegativeReals, mutable=True) model.Pmin = Param(model.I, within=NonNegativeReals, mutable=True) model.RU = Param(model.I, within=NonNegativeReals, mutable=True) model.RD = Param(model.I, within=NonNegativeReals, mutable=True) model.SUC = Param(model.I, within=NonNegativeReals, mutable=True) model.SDC = Param(model.I, within=NonNegativeReals, mutable=True) model.Pini = Param(model.I, within=NonNegativeReals, mutable=True) model.uini = Param(model.I, within=Binary, mutable=True) model.C = Param(model.I, model.F, within=NonNegativeReals, mutable=True) model.B = Param(model.I, model.F, within=NonNegativeReals, mutable=True) model.SystemDemand = Param(model.T, within=NonNegativeReals, mutable=True) model.Emissions = Param(model.I, within=NonNegativeReals, mutable=True) model.PV = Param(model.T, within=NonNegativeReals, mutable=True) model.ESS_Pmax = Param(model.S, within=NonNegativeReals, mutable=True) model.ESS_SOEmax = Param(model.S, within=NonNegativeReals, mutable=True) model.ESS_SOEini = Param(model.S, within=NonNegativeReals, mutable=True) model.ESS_Eff = Param(model.S, within=NonNegativeReals, mutable=True) # Give values to parameters of the generators for i in model.I: model.Pmin[i] = generators.loc[i, "Pmin"] model.Pmax[i] = generators.loc[i, "Pmax"] model.RU[i] = generators.loc[i, "RU"] model.RD[i] = generators.loc[i, "RD"] model.SUC[i] = generators.loc[i, "SUC"] model.SDC[i] = generators.loc[i, "SDC"] model.Pini[i] = generators.loc[i, "Pini"] model.uini[i] = generators.loc[i, "uini"] model.Emissions[i] = generators.loc[i, "Emissions"] for f in model.F: model.B[i, f] = generator_step_size.loc[i, f] model.C[i, f] = generator_step_cost.loc[i, f] # Add system demand and PV generation for t in model.T: model.SystemDemand[t] = system_demand.loc[t, "SystemDemand"] model.PV[t] = pv_generation.loc[t, "PVGeneration"] # Give values to ESS parameters for s in model.S: model.ESS_Pmax[s] = storage_systems.loc[s, "Power"] model.ESS_SOEmax[s] = storage_systems.loc[s, "Energy"] model.ESS_SOEini[s] = storage_systems.loc[s, "SOEini"] model.ESS_Eff[s] = storage_systems.loc[s, "Eff"] # Define decision variables model.P = Var(model.I, model.T, within=NonNegativeReals) model.Pres = Var(model.T, within=NonNegativeReals) model.b = Var(model.I, model.F, model.T, within=NonNegativeReals) model.u = Var(model.I, model.T, within=Binary) model.CSU = Var(model.I, model.T, within=NonNegativeReals) model.CSD = Var(model.I, model.T, within=NonNegativeReals) model.SOE = Var(model.S, model.T, within=NonNegativeReals) model.Pch = Var(model.S, model.T, within=NonNegativeReals) model.Pdis = Var(model.S, model.T, within=NonNegativeReals) model.u_ess = Var(model.S, model.T, within=Binary) # -------------------------------------- # Define the objective functions # -------------------------------------- def cost_objective(model): return sum( sum( sum(model.C[i, f] * model.b[i, f, t] for f in model.F) + model.CSU[i, t] + model.CSD[i, t] for i in model.I) for t in model.T) def emissions_objective(model): return sum( sum(model.P[i, t] * model.Emissions[i] for i in model.I) for t in model.T) def unmet_objective(model): return sum(model.Pres[t] for t in model.T) # -------------------------------------- # Define the regular constraints # -------------------------------------- def power_decomposition_rule1(model, i, t): return model.P[i, t] == sum(model.b[i, f, t] for f in model.F) def power_decomposition_rule2(model, i, f, t): return model.b[i, f, t] <= model.B[i, f] def power_min_rule(model, i, t): return model.P[i, t] >= model.Pmin[i] * model.u[i, t] def power_max_rule(model, i, t): return model.P[i, t] <= model.Pmax[i] * model.u[i, t] def ramp_up_rule(model, i, t): if model.T.ord(t) == 1: return model.P[i, t] - model.Pini[i] <= 60 * model.RU[i] if model.T.ord(t) > 1: return model.P[i, t] - model.P[i, model.T.prev(t)] <= 60 * model.RU[i] def ramp_down_rule(model, i, t): if model.T.ord(t) == 1: return (model.Pini[i] - model.P[i, t]) <= 60 * model.RD[i] if model.T.ord(t) > 1: return (model.P[i, model.T.prev(t)] - model.P[i, t]) <= 60 * model.RD[i] def start_up_cost(model, i, t): if model.T.ord(t) == 1: return model.CSU[i, t] >= model.SUC[i] * (model.u[i, t] - model.uini[i]) if model.T.ord(t) > 1: return model.CSU[i, t] >= model.SUC[i] * ( model.u[i, t] - model.u[i, model.T.prev(t)]) def shut_down_cost(model, i, t): if model.T.ord(t) == 1: return model.CSD[i, t] >= model.SDC[i] * (model.uini[i] - model.u[i, t]) if model.T.ord(t) > 1: return model.CSD[i, t] >= model.SDC[i] * ( model.u[i, model.T.prev(t)] - model.u[i, t]) def ESS_SOEupdate(model, s, t): if model.T.ord(t) == 1: return (model.SOE[s, t] == model.ESS_SOEini[s] + model.ESS_Eff[s] * model.Pch[s, t] - model.Pdis[s, t] / model.ESS_Eff[s]) if model.T.ord(t) > 1: return (model.SOE[s, t] == model.SOE[s, model.T.prev(t)] + model.ESS_Eff[s] * model.Pch[s, t] - model.Pdis[s, t] / model.ESS_Eff[s]) def ESS_SOElimit(model, s, t): return model.SOE[s, t] <= model.ESS_SOEmax[s] def ESS_Charging(model, s, t): return model.Pch[s, t] <= model.ESS_Pmax[s] * model.u_ess[s, t] def ESS_Discharging(model, s, t): return model.Pdis[s, t] <= model.ESS_Pmax[s] * (1 - model.u_ess[s, t]) def Balance(model, t): return model.PV[t] + sum(model.P[i, t] for i in model.I) + sum( model.Pdis[s, t] for s in model.S) == model.SystemDemand[t] - model.Pres[t] + sum( model.Pch[s, t] for s in model.S) def Pres_max(model, t): return model.Pres[t] <= 0.1 * model.SystemDemand[t] # -------------------------------------- # Add components to the model # -------------------------------------- # Add the constraints to the model model.power_decomposition_rule1 = Constraint( model.I, model.T, rule=power_decomposition_rule1) model.power_decomposition_rule2 = Constraint( model.I, model.F, model.T, rule=power_decomposition_rule2) model.power_min_rule = Constraint(model.I, model.T, rule=power_min_rule) model.power_max_rule = Constraint(model.I, model.T, rule=power_max_rule) model.start_up_cost = Constraint(model.I, model.T, rule=start_up_cost) model.shut_down_cost = Constraint(model.I, model.T, rule=shut_down_cost) model.ConSOEUpdate = Constraint(model.S, model.T, rule=ESS_SOEupdate) model.ConCharging = Constraint(model.S, model.T, rule=ESS_Charging) model.ConDischarging = Constraint(model.S, model.T, rule=ESS_Discharging) model.ConSOElimit = Constraint(model.S, model.T, rule=ESS_SOElimit) model.ConGenUp = Constraint(model.I, model.T, rule=ramp_up_rule) model.ConGenDown = Constraint(model.I, model.T, rule=ramp_down_rule) model.ConBalance = Constraint(model.T, rule=Balance) model.Pres_max = Constraint(model.T, rule=Pres_max) # Add the objective functions to the model using ObjectiveList(). Note # that the first index is 1 instead of 0! model.obj_list = ObjectiveList() model.obj_list.add(expr=cost_objective(model), sense=minimize) model.obj_list.add(expr=emissions_objective(model), sense=minimize) model.obj_list.add(expr=unmet_objective(model), sense=minimize) # By default deactivate all the objective functions for o in range(len(model.obj_list)): model.obj_list[o + 1].deactivate() return model
def create_model(self): """ Create and return the mathematical model. """ if options.DEBUG: logging.info("Creating model for day %d" % self.day_id) # Obtain the orders book book = self.orders complexOrders = self.complexOrders # Create the optimization model model = ConcreteModel() model.periods = Set(initialize=book.periods) maxPeriod = max(book.periods) model.bids = Set(initialize=range(len(book.bids))) model.L = Set(initialize=book.locations) model.sBids = Set(initialize=[ i for i in range(len(book.bids)) if book.bids[i].type == 'SB' ]) model.bBids = Set(initialize=[ i for i in range(len(book.bids)) if book.bids[i].type == 'BB' ]) model.cBids = RangeSet(len(complexOrders)) # Complex orders model.C = RangeSet(len(self.connections)) model.directions = RangeSet(2) # 1 == up, 2 = down TODO: clean # Variables model.xs = Var(model.sBids, domain=Reals, bounds=(0.0, 1.0)) # Single period bids acceptance model.xb = Var(model.bBids, domain=Binary) # Block bids acceptance model.xc = Var(model.cBids, domain=Binary) # Complex orders acceptance model.pi = Var(model.L * model.periods, domain=Reals, bounds=self.priceCap) # Market prices model.s = Var(model.bids, domain=NonNegativeReals) # Bids model.sc = Var(model.cBids, domain=NonNegativeReals) # complex orders model.complexVolume = Var(model.cBids, model.periods, domain=Reals) # Bids model.pi_lg_up = Var(model.cBids * model.periods, domain=NonNegativeReals) # Market prices model.pi_lg_down = Var(model.cBids * model.periods, domain=NonNegativeReals) # Market prices model.pi_lg = Var(model.cBids * model.periods, domain=Reals) # Market prices def flowBounds(m, c, d, t): capacity = self.connections[c - 1].capacity_up[t] if d == 1 else \ self.connections[c - 1].capacity_down[t] return (0, capacity) model.f = Var(model.C * model.directions * model.periods, domain=NonNegativeReals, bounds=flowBounds) model.u = Var(model.C * model.directions * model.periods, domain=NonNegativeReals) # Objective def primalObj(m): # Single period bids cost expr = summation( {i: book.bids[i].price * book.bids[i].volume for i in m.sBids}, m.xs) # Block bids cost expr += summation( { i: book.bids[i].price * sum(book.bids[i].volumes.values()) for i in m.bBids }, m.xb) return -expr if options.PRIMAL and not options.DUAL: model.obj = Objective(rule=primalObj, sense=maximize) def primalDualObj(m): return primalObj(m) + sum(1e-5 * m.xc[i] for i in model.cBids) if options.PRIMAL and options.DUAL: model.obj = Objective(rule=primalDualObj, sense=maximize) # Complex order constraint if options.PRIMAL and options.DUAL: model.deactivate_suborders = ConstraintList() for o in model.cBids: sub_ids = complexOrders[o - 1].ids curves = complexOrders[o - 1].curves for id in sub_ids: bid = book.bids[id] if bid.period <= complexOrders[o - 1].SSperiods and bid.price == \ curves[bid.period].bids[0].price: pass # This bid, first step of the cruve in the scheduled stop periods, is not automatically deactivated when MIC constraint is not satisfied else: model.deactivate_suborders.add( model.xs[id] <= model.xc[o]) # Ramping constraints for complex orders def complex_volume_def_rule(m, o, p): sub_ids = complexOrders[o - 1].ids return m.complexVolume[o, p] == sum(m.xs[i] * book.bids[i].volume for i in sub_ids if book.bids[i].period == p) if options.PRIMAL: model.complex_volume_def = Constraint(model.cBids, model.periods, rule=complex_volume_def_rule) def complex_lg_down_rule(m, o, p): if p + 1 > maxPeriod or complexOrders[o - 1].ramp_down == None: return Constraint.Skip else: return m.complexVolume[o, p] - m.complexVolume[o, p + 1] <= complexOrders[ o - 1].ramp_down * \ m.xc[o] if options.PRIMAL and options.APPLY_LOAD_GRADIENT: model.complex_lg_down = Constraint(model.cBids, model.periods, rule=complex_lg_down_rule) def complex_lg_up_rule(m, o, p): if p + 1 > maxPeriod or complexOrders[o - 1].ramp_up == None: return Constraint.Skip else: return m.complexVolume[o, p + 1] - m.complexVolume[ o, p] <= complexOrders[o - 1].ramp_up if options.PRIMAL and options.APPLY_LOAD_GRADIENT: model.complex_lg_up = Constraint( model.cBids, model.periods, rule=complex_lg_up_rule) # Balance constraint # Energy balance constraints balanceExpr = {l: {t: 0.0 for t in model.periods} for l in model.L} for i in model.sBids: # Simple bids bid = book.bids[i] balanceExpr[bid.location][bid.period] += bid.volume * model.xs[i] for i in model.bBids: # Block bids bid = book.bids[i] for t, v in bid.volumes.items(): balanceExpr[bid.location][t] += v * model.xb[i] def balanceCstr(m, l, t): export = 0.0 for c in model.C: if self.connections[c - 1].from_id == l: export += m.f[c, 1, t] - m.f[c, 2, t] elif self.connections[c - 1].to_id == l: export += m.f[c, 2, t] - m.f[c, 1, t] return balanceExpr[l][t] == export if options.PRIMAL: model.balance = Constraint(model.L * book.periods, rule=balanceCstr) # Surplus of single period bids def sBidSurplus(m, i): # For the "usual" step orders bid = book.bids[i] if i in self.plain_single_orders: return m.s[i] >= (m.pi[bid.location, bid.period] - bid.price) * bid.volume else: return Constraint.Skip if options.DUAL: model.sBidSurplus = Constraint(model.sBids, rule=sBidSurplus) # Surplus definition for complex suborders accounting for impact of load gradient condition if options.DUAL: model.complex_sBidSurplus = ConstraintList() for o in model.cBids: sub_ids = complexOrders[o - 1].ids l = complexOrders[o - 1].location for i in sub_ids: bid = book.bids[i] model.complex_sBidSurplus.add( model.s[i] >= (model.pi[l, bid.period] + model.pi_lg[o, bid.period] - bid.price) * bid.volume) def LG_price_def_rule(m, o, p): l = complexOrders[o - 1].location exp = 0 if options.APPLY_LOAD_GRADIENT: D = complexOrders[o - 1].ramp_down U = complexOrders[o - 1].ramp_up if D is not None: exp += (m.pi_lg_down[o, p - 1] if p > 1 else 0) - (m.pi_lg_down[o, p] if p < maxPeriod else 0) if U is not None: exp -= (m.pi_lg_up[o, p - 1] if p > 1 else 0) - (m.pi_lg_up[o, p] if p < maxPeriod else 0) return m.pi_lg[o, p] == exp if options.DUAL: model.LG_price_def = Constraint(model.cBids, model.periods, rule=LG_price_def_rule) # Surplus of block bids def bBidSurplus(m, i): bid = book.bids[i] bidVolume = -sum(bid.volumes.values()) bigM = (self.priceCap[1] - self.priceCap[0]) * bidVolume # FIXME tighten BIGM return m.s[i] + sum([ m.pi[bid.location, t] * -v for t, v in bid.volumes.items() ]) >= bid.cost * bidVolume + bigM * (1 - m.xb[i]) if options.DUAL: model.bBidSurplus = Constraint(model.bBids, rule=bBidSurplus) # Surplus of complex orders def cBidSurplus(m, o): complexOrder = complexOrders[o - 1] sub_ids = complexOrder.ids if book.bids[sub_ids[0]].volume > 0: # supply bigM = sum((self.priceCap[1] - book.bids[i].price) * book.bids[i].volume for i in sub_ids) else: bigM = sum((book.bids[i].price - self.priceCap[0]) * book.bids[i].volume for i in sub_ids) return m.sc[o] + bigM * (1 - m.xc[o]) >= sum(m.s[i] for i in sub_ids) if options.DUAL: model.cBidSurplus = Constraint(model.cBids, rule=cBidSurplus) # Surplus of complex orders def cBidSurplus_2(m, o): complexOrder = complexOrders[o - 1] expr = 0 for i in complexOrder.ids: bid = book.bids[i] if (bid.period <= complexOrder.SSperiods) and ( bid.price == complexOrder.curves[bid.period].bids[0].price): expr += m.s[i] return m.sc[o] >= expr if options.DUAL: model.cBidSurplus_2 = Constraint( model.cBids, rule=cBidSurplus_2) # MIC constraint def cMIC(m, o): complexOrder = complexOrders[o - 1] if complexOrder.FT == 0 and complexOrder.VT == 0: return Constraint.Skip expr = 0 bigM = complexOrder.FT for i in complexOrder.ids: bid = book.bids[i] if (bid.period <= complexOrder.SSperiods) and ( bid.price == complexOrder.curves[bid.period].bids[0].price): bigM += (bid.volume * (self.priceCap[1] - bid.price) ) # FIXME assumes order is supply expr += bid.volume * m.xs[i] * (bid.price - complexOrder.VT) return m.sc[o] + expr + bigM * (1 - m.xc[o]) >= complexOrder.FT if options.DUAL and options.PRIMAL: model.cMIC = Constraint(model.cBids, rule=cMIC) # Dual connections capacity def dualCapacity(m, c, t): exportPrices = 0.0 for l in m.L: if l == self.connections[c - 1].from_id: exportPrices += m.pi[l, t] elif l == self.connections[c - 1].to_id: exportPrices -= m.pi[l, t] return m.u[c, 1, t] - m.u[c, 2, t] + exportPrices == 0.0 if options.DUAL: model.dualCapacity = Constraint(model.C * model.periods, rule=dualCapacity) # Dual optimality def dualObj(m): dualObj = summation(m.s) + summation(m.sc) for o in m.cBids: sub_ids = complexOrders[o - 1].ids for id in sub_ids: dualObj -= m.s[ id] # Remove contribution of complex suborders which were accounted for in prevous summation over single bids if options.APPLY_LOAD_GRADIENT: ramp_down = complexOrders[o - 1].ramp_down ramp_up = complexOrders[o - 1].ramp_up for p in m.periods: if p == maxPeriod: continue if ramp_down is not None: dualObj += ramp_down * m.pi_lg_down[ o, p] # Add contribution of load gradient if ramp_up is not None: dualObj += ramp_up * m.pi_lg_up[ o, p] # Add contribution of load gradient for c in model.C: for t in m.periods: dualObj += self.connections[c - 1].capacity_up[t] * m.u[c, 1, t] dualObj += self.connections[c - 1].capacity_down[t] * m.u[c, 2, t] return dualObj if not options.PRIMAL: model.obj = Objective(rule=dualObj, sense=minimize) def primalEqualsDual(m): return primalObj(m) >= dualObj(m) if options.DUAL and options.PRIMAL: model.primalEqualsDual = Constraint(rule=primalEqualsDual) self.model = model