1. Knapsack
import numpy as np
import gurobipy as gp
from gurobipy import GRB
def generate_knapsack(num_items):
# Fix seed value
rng = np.random.default_rng(seed=0)
# Item values, weights
values = rng.uniform(low=1, high=25, size=num_items)
weights = rng.uniform(low=5, high=100, size=num_items)
# Knapsack capacity
capacity = 0.7 * weights.sum()
return values, weights, capacity
def solve_knapsack_model(values, weights, capacity):
num_items = len(values)
# Turn values and weights numpy arrays to dict
values_dict = {i: value for i, value in enumerate(values)}
weights_dict = {i: weight for i, weight in enumerate(weights)}
with gp.Env() as env:
with gp.Model(name="knapsack", env=env) as model:
# Define decision variables using the Model.addVars() method
x = model.addVars(num_items, vtype=GRB.BINARY, name="x")
# Define objective function using the Model.setObjective() method
# Build the LinExpr using the tupledict.prod() method
model.setObjective(x.prod(values_dict), sense=GRB.MAXIMIZE)
# Define capacity constraint using the Model.addConstr() method
model.addConstr(x.prod(weights_dict) <= capacity, name="knapsack_capacity")
model.optimize()
data = generate_knapsack(10000)
solve_knapsack_model(*data)
Here is the log of the execution of this program:
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[arm] - Darwin 24.2.0 24C101) CPU model: Apple M1 Pro Thread count: 8 physical cores, 8 logical processors, using up to 8 threads Optimize a model with 1 rows, 10000 columns and 10000 nonzeros Model fingerprint: 0xa2a6b3bf Variable types: 0 continuous, 10000 integer (10000 binary) Coefficient statistics: Matrix range [5e+00, 1e+02] Objective range [1e+00, 2e+01] Bounds range [1e+00, 1e+00] RHS range [4e+05, 4e+05] Found heuristic solution: objective 90594.480679 Presolve time: 0.01s Presolved: 1 rows, 10000 columns, 10000 nonzeros Variable types: 0 continuous, 10000 integer (10000 binary) Root relaxation: objective 1.190206e+05, 1 iterations, 0.00 seconds (0.00 work units) Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 119020.617 0 1 90594.4807 119020.617 31.4% - 0s H 0 0 119005.88462 119020.617 0.01% - 0s H 0 0 119015.22254 119020.617 0.00% - 0s Explored 1 nodes (1 simplex iterations) in 0.03 seconds (0.02 work units) Thread count was 8 (of 8 available processors) Solution count 3: 119015 119006 90594.5 Optimal solution found (tolerance 1.00e-04) Best objective 1.190152225370e+05, best bound 1.190206174693e+05, gap 0.0045%
2. Portfolio Optimization
import json
import pandas as pd
import numpy as np
import gurobipy as gp
from gurobipy import GRB
with open("data/portfolio-example.json", "r") as f:
data = json.load(f)
n = data["num_assets"]
sigma = np.array(data["covariance"])
mu = np.array(data["expected_return"])
mu_0 = data["target_return"]
k = data["portfolio_max_size"]
with gp.Model("portfolio") as model:
x = model.addVars(n, ub=1, name="x")
y = model.addVars(n, vtype=GRB.BINARY, name="y")
risk = gp.quicksum(x[i] * sigma[i, j] * x[j] for i in range(n) for j in range(n))
model.setObjective(risk)
expected_return = gp.quicksum(mu[i] * x[i] for i in range(n))
model.addConstr(expected_return >= mu_0, name="return")
model.addConstr(x.sum() == 1, name="budget")
model.addConstr(y.sum() <= k, name="cardinality")
model.addConstrs((x[i] <= y[i] for i in range(n)), name="is_allocated")
model.optimize()
# Write the solution into a DataFrame
portfolio = [var.X for var in model.getVars() if "x" in var.VarName]
risk = model.ObjVal
expected_return = model.getRow(model.getConstrByName("return")).getValue()
df = pd.DataFrame(
data=portfolio + [risk, expected_return],
index=[f"asset_{i}" for i in range(n)] + ["risk", "return"],
columns=["Portfolio"],
)
print(df)
Here is the log of the execution of this program:
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[arm] - Darwin 24.2.0 24C101) CPU model: Apple M1 Pro Thread count: 8 physical cores, 8 logical processors, using up to 8 threads Optimize a model with 23 rows, 40 columns and 100 nonzeros Model fingerprint: 0x755216b2 Model has 210 quadratic objective terms Variable types: 20 continuous, 20 integer (20 binary) Coefficient statistics: Matrix range [1e-04, 1e+00] Objective range [0e+00, 0e+00] QObjective range [7e-06, 4e-03] Bounds range [1e+00, 1e+00] RHS range [1e-04, 2e+01] Found heuristic solution: objective 0.0000960 Presolve time: 0.00s Presolved: 23 rows, 40 columns, 100 nonzeros Presolved model has 210 quadratic objective terms Variable types: 20 continuous, 20 integer (20 binary) Root relaxation: objective 6.741224e-05, 38 iterations, 0.00 seconds (0.00 work units) Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 0.00007 0 11 0.00010 0.00007 29.8% - 0s H 0 0 0.0000674 0.00007 0.00% - 0s 0 0 0.00007 0 11 0.00007 0.00007 0.00% - 0s Explored 1 nodes (38 simplex iterations) in 0.01 seconds (0.00 work units) Thread count was 8 (of 8 available processors) Solution count 2: 6.74122e-05 9.60284e-05 Optimal solution found (tolerance 1.00e-04) Best objective 6.741224163915e-05, best bound 6.741224163915e-05, gap 0.0000% Portfolio asset_0 0.000000 asset_1 0.000000 asset_2 0.000000 asset_3 0.000000 asset_4 0.006812 asset_5 0.034594 asset_6 0.000000 asset_7 0.000000 asset_8 0.409840 asset_9 0.026759 asset_10 0.000000 asset_11 0.027722 asset_12 0.039911 asset_13 0.030221 asset_14 0.038511 asset_15 0.000000 asset_16 0.184718 asset_17 0.000000 asset_18 0.126673 asset_19 0.074241 risk 0.000067 return 0.000104
3. Termination Criteria
from functools import partial
import gurobipy as gp
from gurobipy import GRB
class CallbackData:
def __init__(self):
self.last_gap_change_time = -GRB.INFINITY
self.last_gap = GRB.INFINITY
def callback(model, where, *, cbdata):
if where != GRB.Callback.MIP:
return
if model.cbGet(GRB.Callback.MIP_SOLCNT) == 0:
return
best = model.cbGet(GRB.Callback.MIP_OBJBST)
bound = model.cbGet(GRB.Callback.MIP_OBJBND)
gap = abs((bound - best) / best)
time = model.cbGet(GRB.Callback.RUNTIME)
if gap < cbdata.last_gap - epsilon_to_compare_gap:
# The gap has decreased, store new gap and reset the time
cbdata.last_gap = gap
cbdata.last_gap_change_time = time
print(f"Storing new gap: {gap}, {time}")
return
if time - cbdata.last_gap_change_time > max_time_between_gap_updates:
# No change since too long
print("It's been too long...")
model.terminate()
with gp.read("data/mkp.mps.bz2") as model:
# Global variables used in the callback function
max_time_between_gap_updates = 15
epsilon_to_compare_gap = 1e-4
# Initialize data passed to the callback function
callback_data = CallbackData()
callback_func = partial(callback, cbdata=callback_data)
model.optimize(callback_func)
Here is the log of the execution of this program:
Read MPS format model from file data/mkp.mps.bz2 Reading time = 0.04 seconds mkp_bip: 30 rows, 500 columns, 15000 nonzeros Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[arm] - Darwin 24.2.0 24C101) CPU model: Apple M1 Pro Thread count: 8 physical cores, 8 logical processors, using up to 8 threads Optimize a model with 30 rows, 500 columns and 15000 nonzeros Model fingerprint: 0xcbe37219 Variable types: 0 continuous, 500 integer (500 binary) Coefficient statistics: Matrix range [1e+00, 1e+03] Objective range [4e+02, 1e+03] Bounds range [1e+00, 1e+00] RHS range [1e+05, 1e+05] Found heuristic solution: objective 177106.00000 Presolve time: 0.02s Presolved: 30 rows, 500 columns, 15000 nonzeros Variable types: 0 continuous, 500 integer (500 binary) Storing new gap: 1.0921143270132012, 0.016685009002685547 Root relaxation: objective 2.164011e+05, 112 iterations, 0.00 seconds (0.01 work units) Storing new gap: 0.22187277675516356, 0.019383907318115234 Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 216401.065 0 25 177106.000 216401.065 22.2% - 0s H 0 0 209908.00000 216401.065 3.09% - 0s H 0 0 213709.00000 216401.065 1.26% - 0s H 0 0 215225.00000 216401.065 0.55% - 0s H 0 0 215436.00000 216401.065 0.45% - 0s H 0 0 215510.00000 216401.065 0.41% - 0s H 0 0 215624.00000 216401.065 0.36% - 0s Storing new gap: 0.0035756687567246688, 0.10831308364868164 0 0 216395.201 0 25 215624.000 216395.201 0.36% - 0s H 0 0 215635.00000 216395.201 0.35% - 0s H 0 2 215645.00000 216395.201 0.35% - 0s 0 2 216395.201 0 25 215645.000 216395.201 0.35% - 0s H 85 88 215666.00000 216395.201 0.34% 8.8 0s Storing new gap: 0.003380226832231321, 0.29430699348449707 H 162 166 215707.00000 216395.201 0.32% 9.4 0s Storing new gap: 0.0031895116987394937, 0.33049607276916504 H 2099 1932 215727.00000 216389.474 0.31% 7.0 0s Storing new gap: 0.0030686933021828516, 0.9242510795593262 H20129 16350 215731.00000 216372.312 0.30% 8.0 2s Storing new gap: 0.0029620221479527745, 2.383863925933838 41136 31672 216209.291 61 22 215731.000 216367.041 0.29% 8.1 5s H50272 34121 215821.00000 216367.041 0.25% 8.1 5s Storing new gap: 0.0025298742939750996, 5.620049953460693 137609 94670 216027.180 88 21 215821.000 216348.997 0.24% 8.5 10s Storing new gap: 0.002427937967111634, 11.783638954162598 232901 178512 216148.976 76 22 215821.000 216341.790 0.24% 8.8 15s 329701 262798 216036.548 99 18 215821.000 216337.190 0.24% 9.0 20s 429310 349889 216128.018 73 23 215821.000 216333.650 0.24% 9.1 25s It's been too long... Cutting planes: Gomory: 7 Explored 469770 nodes (4286693 simplex iterations) in 26.80 seconds (74.85 work units) Thread count was 8 (of 8 available processors) Solution count 10: 215821 215731 215727 ... 215436 Solve interrupted Best objective 2.158210000000e+05, best bound 2.163320000000e+05, gap 0.2368% User-callback calls 942664, time in user-callback 1.06 sec
4. Unit Commitment Problem
import gurobipy as gp
from gurobipy import GRB
# 24 Hour Load Forecast (MW)
load_forecast = [
4, 4, 4, 4, 4, 4, 6, 6,
12, 12, 12, 12, 12, 4, 4, 4,
4, 16, 16, 16, 16, 6.5, 6.5, 6.5,
]
# solar energy forecast (MW)
solar_forecast = [
0, 0, 0, 0, 0, 0, 0.5, 1.0,
1.5, 2.0, 2.5, 3.5, 3.5, 2.5, 2.0, 1.5,
1.0, 0.5, 0, 0, 0, 0, 0, 0,
]
# global number of time intervals
nTimeIntervals = len(load_forecast)
# thermal units
thermal_units = ["gen1", "gen2", "gen3"]
# thermal units' costs (a + b*p + c*p^2), (startup and shutdown costs)
thermal_units_cost, a, b, c, sup_cost, sdn_cost = gp.multidict(
{
"gen1": [5.0, 0.5, 1.0, 2, 1],
"gen2": [5.0, 0.5, 0.5, 2, 1],
"gen3": [5.0, 3.0, 2.0, 2, 1],
}
)
# thernal units operating limits
thermal_units_limits, pmin, pmax = gp.multidict(
{"gen1": [1.5, 5.0], "gen2": [2.5, 10.0], "gen3": [1.0, 3.0]}
)
# thermal units dynamic data (initial commitment status)
thermal_units_dyn_data, init_status = gp.multidict(
{"gen1": [0], "gen2": [0], "gen3": [0]}
)
def show_results():
obj_val_s = model.ObjVal
print(f" OverAll Cost = {round(obj_val_s, 2)} ")
print("\n")
print("%5s" % "time", end=" ")
for t in range(nTimeIntervals):
print("%4s" % t, end=" ")
print("\n")
for g in thermal_units:
print("%5s" % g, end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % thermal_units_out_power[g, t].X, end=" ")
print("\n")
print("%5s" % "Solar", end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % solar_forecast[t], end=" ")
print("\n")
print("%5s" % "Load", end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % load_forecast[t], end=" ")
print("\n")
with gp.Env() as env, gp.Model(env=env) as model:
# add variables for thermal units (power and statuses for commitment, startup and shutdown)
thermal_units_out_power = model.addVars(
thermal_units, range(nTimeIntervals), name="thermal_units_out_power"
)
thermal_units_startup_status = model.addVars(
thermal_units,
range(nTimeIntervals),
vtype=GRB.BINARY,
name="thermal_unit_startup_status",
)
thermal_units_shutdown_status = model.addVars(
thermal_units,
range(nTimeIntervals),
vtype=GRB.BINARY,
name="thermal_unit_shutdown_status",
)
thermal_units_comm_status = model.addVars(
thermal_units, range(nTimeIntervals), vtype=GRB.BINARY, name="thermal_unit_comm_status"
)
# define objective function as an empty quadratic construct and add terms
obj_fun_expr = gp.QuadExpr(0)
for t in range(nTimeIntervals):
for g in thermal_units:
obj_fun_expr.add(
a[g] * thermal_units_comm_status[g, t]
+ b[g] * thermal_units_out_power[g, t]
+ c[g] * thermal_units_out_power[g, t] * thermal_units_out_power[g, t]
+ sup_cost[g] * thermal_units_startup_status[g, t]
+ sdn_cost[g] * thermal_units_shutdown_status[g, t]
)
model.setObjective(obj_fun_expr)
# Power balance equations
for t in range(nTimeIntervals):
model.addConstr(
gp.quicksum(thermal_units_out_power[g, t] for g in thermal_units)
== load_forecast[t] - solar_forecast[t],
name="power_balance_" + str(t),
)
# Thermal units logical constraints
for t in range(nTimeIntervals):
for g in thermal_units:
if t == 0:
model.addConstr(
thermal_units_comm_status[g, t] - init_status[g]
== thermal_units_startup_status[g, t]
- thermal_units_shutdown_status[g, t],
name="logical1_" + g + "_" + str(t),
)
else:
model.addConstr(
thermal_units_comm_status[g, t]
- thermal_units_comm_status[g, t - 1]
== thermal_units_startup_status[g, t]
- thermal_units_shutdown_status[g, t],
name="logical1_" + g + "_" + str(t),
)
model.addConstr(
thermal_units_startup_status[g, t] + thermal_units_shutdown_status[g, t]
<= 1,
name="logical2_" + g + "_" + str(t),
)
# Thermal units physical constraints, using indicator constraints
for t in range(nTimeIntervals):
for g in thermal_units:
model.addConstr(
(thermal_units_comm_status[g, t] == 1) >>
(thermal_units_out_power[g, t] >= pmin[g])
)
model.addConstr(
(thermal_units_comm_status[g, t] == 1) >>
(thermal_units_out_power[g, t] <= pmax[g])
)
model.addConstr(
(thermal_units_comm_status[g, t] == 0) >>
(thermal_units_out_power[g, t] == 0.0)
)
model.optimize()
show_results()
Here is the log of the execution of this program:
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[arm] - Darwin 24.2.0 24C101) CPU model: Apple M1 Pro Thread count: 8 physical cores, 8 logical processors, using up to 8 threads Optimize a model with 168 rows, 288 columns and 501 nonzeros Model fingerprint: 0xa1375406 Model has 72 quadratic objective terms Model has 216 simple general constraints 216 INDICATOR Variable types: 72 continuous, 216 integer (216 binary) Coefficient statistics: Matrix range [1e+00, 1e+00] Objective range [5e-01, 5e+00] QObjective range [1e+00, 4e+00] Bounds range [1e+00, 1e+00] RHS range [1e+00, 2e+01] GenCon rhs range [1e+00, 1e+01] GenCon coe range [1e+00, 1e+00] Presolve added 1 rows and 0 columns Presolve removed 0 rows and 125 columns Presolve time: 0.01s Presolved: 185 rows, 171 columns, 473 nonzeros Presolved model has 43 quadratic objective terms Variable types: 51 continuous, 120 integer (120 binary) Found heuristic solution: objective 1007.6964286 Found heuristic solution: objective 907.5714286 Root relaxation: objective 8.586741e+02, 287 iterations, 0.00 seconds (0.00 work units) Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 858.67410 0 44 907.57143 858.67410 5.39% - 0s H 0 0 894.0714286 858.67410 3.96% - 0s H 0 0 888.7380952 858.67410 3.38% - 0s H 0 0 887.9047619 858.67410 3.29% - 0s 0 0 861.09989 0 48 887.90476 861.09989 3.02% - 0s 0 0 861.09989 0 35 887.90476 861.09989 3.02% - 0s 0 0 865.42295 0 37 887.90476 865.42295 2.53% - 0s 0 0 866.16295 0 37 887.90476 866.16295 2.45% - 0s 0 0 868.53847 0 41 887.90476 868.53847 2.18% - 0s 0 0 869.53572 0 42 887.90476 869.53572 2.07% - 0s 0 0 871.70239 0 42 887.90476 871.70239 1.82% - 0s 0 0 871.70239 0 44 887.90476 871.70239 1.82% - 0s 0 0 871.70239 0 44 887.90476 871.70239 1.82% - 0s 0 0 871.70239 0 44 887.90476 871.70239 1.82% - 0s 0 0 871.70239 0 44 887.90476 871.70239 1.82% - 0s 0 0 871.70239 0 46 887.90476 871.70239 1.82% - 0s 0 2 871.70239 0 46 887.90476 871.70239 1.82% - 0s Cutting planes: Cover: 2 Implied bound: 1 Clique: 1 MIR: 1 Flow cover: 7 Explored 52 nodes (1017 simplex iterations) in 0.05 seconds (0.03 work units) Thread count was 8 (of 8 available processors) Solution count 5: 887.905 888.738 894.071 ... 1007.7 Optimal solution found (tolerance 1.00e-04) Best objective 8.879047619048e+02, best bound 8.879047619048e+02, gap 0.0000% OverAll Cost = 887.9 time 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 gen1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 3.5 3.3 3.2 2.8 2.8 1.5 2.0 0.0 0.0 4.6 4.8 4.8 4.8 2.2 2.2 2.2 gen2 4.0 4.0 4.0 4.0 4.0 4.0 5.5 5.0 7.0 6.7 6.3 5.7 5.7 0.0 0.0 2.5 3.0 9.2 9.5 9.5 9.5 4.3 4.3 4.3 gen3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.7 1.8 1.8 1.8 0.0 0.0 0.0 Solar 0.0 0.0 0.0 0.0 0.0 0.0 0.5 1.0 1.5 2.0 2.5 3.5 3.5 2.5 2.0 1.5 1.0 0.5 0.0 0.0 0.0 0.0 0.0 0.0 Load 4.0 4.0 4.0 4.0 4.0 4.0 6.0 6.0 12.0 12.0 12.0 12.0 12.0 4.0 4.0 4.0 4.0 16.0 16.0 16.0 16.0 6.5 6.5 6.5
5. Unit Commitment Problem (matrix API)
import gurobipy as gp
from gurobipy import GRB
import numpy as np
# 24 Hour Load Forecast (MW)
load_forecast = [
4, 4, 4, 4, 4, 4, 6, 6,
12, 12, 12, 12, 12, 4, 4, 4,
4, 16, 16, 16, 16, 6.5, 6.5, 6.5,
]
# solar energy forecast (MW)
solar_forecast = [
0, 0, 0, 0, 0, 0, 0.5, 1.0,
1.5, 2.0, 2.5, 3.5, 3.5, 2.5, 2.0, 1.5,
1.0, 0.5, 0, 0, 0, 0, 0, 0,
]
# global number of time intervals
nTimeIntervals = len(load_forecast)
# thermal units
thermal_units = ["gen1", "gen2", "gen3"]
# thermal units' costs (a + b*p + c*p^2), (startup and shutdown costs)
thermal_units_cost, a, b, c, sup_cost, sdn_cost = gp.multidict(
{
"gen1": [5.0, 0.5, 1.0, 2, 1],
"gen2": [5.0, 0.5, 0.5, 2, 1],
"gen3": [5.0, 3.0, 2.0, 2, 1],
}
)
# thernal units operating limits
thermal_units_limits, pmin, pmax = gp.multidict(
{"gen1": [1.5, 5.0], "gen2": [2.5, 10.0], "gen3": [1.0, 3.0]}
)
# thermal units dynamic data (initial commitment status)
thermal_units_dyn_data, init_status = gp.multidict(
{"gen1": [0], "gen2": [0], "gen3": [0]}
)
# We need np.array instances instead of dict instances to use
# the matrix API
def dict_to_array(keys, d):
return np.array([d[k] for k in keys])
a = dict_to_array(thermal_units, a)
b = dict_to_array(thermal_units, b)
c = dict_to_array(thermal_units, c)
sup_cost = dict_to_array(thermal_units, sup_cost)
sdn_cost = dict_to_array(thermal_units, sdn_cost)
pmin = dict_to_array(thermal_units, pmin)
pmax = dict_to_array(thermal_units, pmax)
init_status = dict_to_array(thermal_units, init_status)
# We also need to turn to integer indices instead of strings
units_to_index = {u: idx for idx, u in enumerate(thermal_units)}
nb_units = len(thermal_units)
def show_results():
obj_val_s = model.ObjVal
print(f" OverAll Cost = {round(obj_val_s, 2)} ")
print("\n")
print("%5s" % "time", end=" ")
for t in range(nTimeIntervals):
print("%4s" % t, end=" ")
print("\n")
for g in thermal_units:
print("%5s" % g, end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % thermal_units_out_power[units_to_index[g], t].X, end=" ")
print("\n")
print("%5s" % "Solar", end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % solar_forecast[t], end=" ")
print("\n")
print("%5s" % "Load", end=" ")
for t in range(nTimeIntervals):
print("%4.1f" % load_forecast[t], end=" ")
print("\n")
with gp.Env() as env, gp.Model(env=env) as model:
# add variables for thermal units (power and statuses for commitment, startup and shutdown)
thermal_units_out_power = model.addMVar(
(nb_units, nTimeIntervals), name="thermal_units_out_power"
)
thermal_units_startup_status = model.addMVar(
(nb_units, nTimeIntervals),
vtype=GRB.BINARY,
name="thermal_unit_startup_status",
)
thermal_units_shutdown_status = model.addMVar(
(nb_units, nTimeIntervals),
vtype=GRB.BINARY,
name="thermal_unit_shutdown_status",
)
thermal_units_comm_status = model.addMVar(
(nb_units, nTimeIntervals), vtype=GRB.BINARY, name="thermal_unit_comm_status"
)
# define objective function as an empty quadratic construct and add terms
model.setObjective((a[:, None] * thermal_units_comm_status).sum() +
(b[:, None] * thermal_units_out_power).sum() +
(c[:, None] * (thermal_units_out_power * thermal_units_out_power)).sum() +
(sup_cost[:, None] * thermal_units_startup_status).sum() +
(sdn_cost[:, None] * thermal_units_shutdown_status).sum())
# Power balance equations
required_power = np.array([l - s for l, s in zip(load_forecast, solar_forecast) ])
model.addConstr(
thermal_units_out_power.sum(axis=0) == required_power,
name="power_balance"
)
# Thermal units logical constraints
# Initial condition (t = 0)
model.addConstr(
thermal_units_comm_status[:, 0] - init_status ==
thermal_units_startup_status[:, 0] - thermal_units_shutdown_status[:, 0],
name="logical1_initial"
)
# Remaining time intervals (t > 0)
model.addConstr(
thermal_units_comm_status[:, 1:] - thermal_units_comm_status[:, :-1]
== thermal_units_startup_status[:, 1:] - thermal_units_shutdown_status[:, 1:],
name="logical1_remaining"
)
model.addConstr(
thermal_units_startup_status + thermal_units_shutdown_status <= 1,
name="logical2"
)
# Thermal units physical constraints, using indicator constraints
for g in range(len(thermal_units)):
model.addGenConstrIndicator(thermal_units_comm_status[g, :], True,
thermal_units_out_power[g, :] >= pmin[g],
name="physical_min")
model.addGenConstrIndicator(thermal_units_comm_status[g, :], True,
thermal_units_out_power[g, :] <= pmax[g],
name="physical_max")
model.addGenConstrIndicator(thermal_units_comm_status[g, :], False,
thermal_units_out_power[g, :] == 0.0,
name="physical_off")
model.optimize()
show_results()
Here is the log of the execution of this program:
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[arm] - Darwin 24.2.0 24C101) CPU model: Apple M1 Pro Thread count: 8 physical cores, 8 logical processors, using up to 8 threads Optimize a model with 168 rows, 288 columns and 501 nonzeros Model fingerprint: 0x3b9885a9 Model has 72 quadratic objective terms Model has 216 simple general constraints 216 INDICATOR Variable types: 72 continuous, 216 integer (216 binary) Coefficient statistics: Matrix range [1e+00, 1e+00] Objective range [5e-01, 5e+00] QObjective range [1e+00, 4e+00] Bounds range [1e+00, 1e+00] RHS range [1e+00, 2e+01] GenCon rhs range [1e+00, 1e+01] GenCon coe range [1e+00, 1e+00] Presolve added 1 rows and 0 columns Presolve removed 0 rows and 125 columns Presolve time: 0.01s Presolved: 183 rows, 170 columns, 468 nonzeros Presolved model has 43 quadratic objective terms Variable types: 50 continuous, 120 integer (120 binary) Found heuristic solution: objective 972.4464286 Found heuristic solution: objective 949.4464286 Root relaxation: objective 8.562274e+02, 297 iterations, 0.00 seconds (0.00 work units) Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 856.22741 0 47 949.44643 856.22741 9.82% - 0s H 0 0 888.6964286 856.22741 3.65% - 0s H 0 0 887.9047619 856.22741 3.57% - 0s 0 0 859.55567 0 49 887.90476 859.55567 3.19% - 0s 0 0 859.55567 0 40 887.90476 859.55567 3.19% - 0s 0 0 860.62679 0 43 887.90476 860.62679 3.07% - 0s 0 0 861.04305 0 43 887.90476 861.04305 3.03% - 0s 0 0 863.11702 0 40 887.90476 863.11702 2.79% - 0s 0 0 863.60301 0 40 887.90476 863.60301 2.74% - 0s 0 0 864.38939 0 42 887.90476 864.38939 2.65% - 0s 0 0 865.38905 0 42 887.90476 865.38905 2.54% - 0s 0 0 865.80613 0 42 887.90476 865.80613 2.49% - 0s 0 0 865.80613 0 42 887.90476 865.80613 2.49% - 0s 0 0 865.81082 0 42 887.90476 865.81082 2.49% - 0s 0 2 865.81082 0 42 887.90476 865.81082 2.49% - 0s Cutting planes: Learned: 1 Cover: 2 MIR: 1 Flow cover: 2 Explored 147 nodes (1455 simplex iterations) in 0.05 seconds (0.03 work units) Thread count was 8 (of 8 available processors) Solution count 4: 887.905 888.696 949.446 972.446 Optimal solution found (tolerance 1.00e-04) Best objective 8.879047619048e+02, best bound 8.879047606600e+02, gap 0.0000% OverAll Cost = 887.9 time 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 gen1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 3.5 3.3 3.2 2.8 2.8 1.5 2.0 0.0 0.0 4.6 4.8 4.8 4.8 2.2 2.2 2.2 gen2 4.0 4.0 4.0 4.0 4.0 4.0 5.5 5.0 7.0 6.7 6.3 5.7 5.7 0.0 0.0 2.5 3.0 9.2 9.5 9.5 9.5 4.3 4.3 4.3 gen3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.7 1.8 1.8 1.8 0.0 0.0 0.0 Solar 0.0 0.0 0.0 0.0 0.0 0.0 0.5 1.0 1.5 2.0 2.5 3.5 3.5 2.5 2.0 1.5 1.0 0.5 0.0 0.0 0.0 0.0 0.0 0.0 Load 4.0 4.0 4.0 4.0 4.0 4.0 6.0 6.0 12.0 12.0 12.0 12.0 12.0 4.0 4.0 4.0 4.0 16.0 16.0 16.0 16.0 6.5 6.5 6.5