You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MIPLearn/miplearn/problems/uc.py

209 lines
7.3 KiB

# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from dataclasses import dataclass
from math import pi
from typing import List, Optional, Union
import gurobipy as gp
import numpy as np
from gurobipy import GRB, quicksum
from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen
from miplearn.io import read_pkl_gz
from miplearn.solvers.gurobi import GurobiModel
@dataclass
class UnitCommitmentData:
demand: np.ndarray
min_power: np.ndarray
max_power: np.ndarray
min_uptime: np.ndarray
min_downtime: np.ndarray
cost_startup: np.ndarray
cost_prod: np.ndarray
cost_prod_quad: np.ndarray
cost_fixed: np.ndarray
class UnitCommitmentGenerator:
def __init__(
self,
n_units: rv_frozen = randint(low=1_000, high=1_001),
n_periods: rv_frozen = randint(low=72, high=73),
max_power: rv_frozen = uniform(loc=50, scale=450),
min_power: rv_frozen = uniform(loc=0.5, scale=0.25),
cost_startup: rv_frozen = uniform(loc=0, scale=10_000),
cost_prod: rv_frozen = uniform(loc=0, scale=50),
cost_prod_quad: rv_frozen = uniform(loc=0, scale=0),
cost_fixed: rv_frozen = uniform(loc=0, scale=1_000),
min_uptime: rv_frozen = randint(low=2, high=8),
min_downtime: rv_frozen = randint(low=2, high=8),
cost_jitter: rv_frozen = uniform(loc=0.75, scale=0.5),
demand_jitter: rv_frozen = uniform(loc=0.9, scale=0.2),
fix_units: bool = False,
) -> None:
self.n_units = n_units
self.n_periods = n_periods
self.max_power = max_power
self.min_power = min_power
self.cost_startup = cost_startup
self.cost_prod = cost_prod
self.cost_prod_quad = cost_prod_quad
self.cost_fixed = cost_fixed
self.min_uptime = min_uptime
self.min_downtime = min_downtime
self.cost_jitter = cost_jitter
self.demand_jitter = demand_jitter
self.fix_units = fix_units
self.ref_data: Optional[UnitCommitmentData] = None
def generate(self, n_samples: int) -> List[UnitCommitmentData]:
def _sample() -> UnitCommitmentData:
if self.ref_data is None:
T = self.n_periods.rvs()
G = self.n_units.rvs()
# Generate unit parameteres
max_power = self.max_power.rvs(G)
min_power = max_power * self.min_power.rvs(G)
max_power = max_power
min_uptime = self.min_uptime.rvs(G)
min_downtime = self.min_downtime.rvs(G)
cost_startup = self.cost_startup.rvs(G)
cost_prod = self.cost_prod.rvs(G)
cost_prod_quad = self.cost_prod_quad.rvs(G)
cost_fixed = self.cost_fixed.rvs(G)
capacity = max_power.sum()
# Generate periodic demand in the range [0.4, 0.8] * capacity, with a peak every 12 hours.
demand = np.sin([i / 6 * pi for i in range(T)])
demand *= uniform(loc=0, scale=1).rvs(T)
demand -= demand.min()
demand /= demand.max() / 0.4
demand += 0.4
demand *= capacity
else:
T, G = len(self.ref_data.demand), len(self.ref_data.max_power)
demand = self.ref_data.demand * self.demand_jitter.rvs(T)
min_power = self.ref_data.min_power
max_power = self.ref_data.max_power
min_uptime = self.ref_data.min_uptime
min_downtime = self.ref_data.min_downtime
cost_startup = self.ref_data.cost_startup * self.cost_jitter.rvs(G)
cost_prod = self.ref_data.cost_prod * self.cost_jitter.rvs(G)
cost_prod_quad = self.ref_data.cost_prod_quad * self.cost_jitter.rvs(G)
cost_fixed = self.ref_data.cost_fixed * self.cost_jitter.rvs(G)
data = UnitCommitmentData(
demand.round(2),
min_power.round(2),
max_power.round(2),
min_uptime,
min_downtime,
cost_startup.round(2),
cost_prod.round(2),
cost_prod_quad.round(4),
cost_fixed.round(2),
)
if self.ref_data is None and self.fix_units:
self.ref_data = data
return data
return [_sample() for _ in range(n_samples)]
def build_uc_model_gurobipy(data: Union[str, UnitCommitmentData]) -> GurobiModel:
"""
Models the unit commitment problem according to equations (1)-(5) of:
Bendotti, P., Fouilhoux, P. & Rottner, C. The min-up/min-down unit
commitment polytope. J Comb Optim 36, 1024-1058 (2018).
https://doi.org/10.1007/s10878-018-0273-y
"""
if isinstance(data, str):
data = read_pkl_gz(data)
assert isinstance(data, UnitCommitmentData)
T = len(data.demand)
G = len(data.min_power)
D = data.demand
Pmin, Pmax = data.min_power, data.max_power
L = data.min_uptime
l = data.min_downtime
model = gp.Model()
is_on = model.addVars(G, T, vtype=GRB.BINARY, name="is_on")
switch_on = model.addVars(G, T, vtype=GRB.BINARY, name="switch_on")
prod = model.addVars(G, T, name="prod")
# Objective function
model.setObjective(
quicksum(
is_on[g, t] * data.cost_fixed[g]
+ switch_on[g, t] * data.cost_startup[g]
+ prod[g, t] * data.cost_prod[g]
+ prod[g, t] * prod[g, t] * data.cost_prod_quad[g]
for g in range(G)
for t in range(T)
)
)
# Eq 1: Minimum up-time constraint: If unit g is down at time t, then it
# cannot have start up during the previous L[g] periods.
model.addConstrs(
(
quicksum(switch_on[g, k] for k in range(t - L[g] + 1, t + 1)) <= is_on[g, t]
for g in range(G)
for t in range(L[g] - 1, T)
),
name="eq_min_uptime",
)
# Eq 2: Minimum down-time constraint: Symmetric to the minimum-up constraint.
model.addConstrs(
(
quicksum(switch_on[g, k] for k in range(t - l[g] + 1, t + 1))
<= 1 - is_on[g, t - l[g] + 1]
for g in range(G)
for t in range(l[g] - 1, T)
),
name="eq_min_downtime",
)
# Eq 3: Ensures that if unit g start up at time t, then the start-up variable
# must be one.
model.addConstrs(
(
switch_on[g, t] >= is_on[g, t] - is_on[g, t - 1]
for g in range(G)
for t in range(1, T)
),
name="eq_startup",
)
# Eq 4: Ensures that demand is satisfied at each time period.
model.addConstrs(
(quicksum(prod[g, t] for g in range(G)) >= D[t] for t in range(T)),
name="eq_demand",
)
# Eq 5: Sets the bounds to the quantity of power produced by each unit.
model.addConstrs(
(Pmin[g] * is_on[g, t] <= prod[g, t] for g in range(G) for t in range(T)),
name="eq_prod_lb",
)
model.addConstrs(
(prod[g, t] <= Pmax[g] * is_on[g, t] for g in range(G) for t in range(T)),
name="eq_prod_ub",
)
model.update()
return GurobiModel(model)