Principal Developer
Software Developer
2025-01-15
Application | Phenomenon | Nonlinearity |
---|---|---|
Finance | Risk | quadratic (convex) |
Truss topology | Physical Forces | quadratic (convex) |
Pooling (petrochemical, mining, agriculture) | Mixing Products | quadratic nonconvex |
electricity distribution (ACOPF) | Alternating Current | \(\sin\) and \(\cos\) (can be made quadratic) |
machine learning | — | logistic function, \(\tanh\) |
chemical engineering | Chemical Reactions | anything |
mechanical enginering design | Equations of Motion | ratios, angles, derivatives |
… all without leaving your Pythonic armchair!
In general, we aim to solve the following problem form:
\[ \begin{aligned} & \min f(x)\\ & \text{s.t:}\\ & g_i(x) \le 0, i = 1,\ldots, m \\ & x_j \in \mathbb Z, j \in J\\ & l \le x \le u \end{aligned} \]
gurobipy
12.0LinExpr
, QuadExpr
gurobipy
automatically promotes expressions to NLExpr
whenever it is necessary to store them.
Any Python operator:
+
-
*
/
**
On any combination of types:
Var
LinExpr
QuadExpr
NLExpr
Nonlinear functions live in the nlfunc
module:
sin
cos
tan
exp
sqrt
log
log2
log10
logistic
square
MNLExpr
is to NLExpr
as MLinExpr
is to LinExpr
Follows numpy broadcasting rules:
In Gurobi, nonlinear constraints are implemented as general constraints. They always take the form
\[ y = f(x) \]
with \(y\) being the resultant variable, and \(x\) being the argument variables.
To add a nonlinear inequality constraint \(l \le f(x) \le u\) do: \[ y = f(x), \; l \le y \le u \]
To minimize a nonlinear function do: \[ \min y \quad \text{s.t.} \; y = f(x), \; -\infty \le y \le \infty \]
\[ \begin{aligned} & \min && -32.174 (\\ & && 255 \log\left(\frac{0.03 + x_2 + x_3 + x_4}{0.03 + 0.09 x_2 + x_3 + x_4}\right) +\\ & && 280 \log\left(\frac{0.03 + x_3 + x_4}{0.03 + 0.07 x_3 + x_4}\right) +\\ & && 290 \log\left(\frac{0.03 + x_4}{0.03 + 0.13 x_4}\right)\\ & && )\\ & \text{s.t.} && 20 (x_2 + x_3 + x_4 - 1)^2 = 0\\ & && x_2, x_3, x_4 \ge 0\\ \end{aligned} \]
\[ \begin{aligned} & \min && -32.174 (\\ & && 255 \log\left(\frac{0.03 + x_2 + x_3 + x_4}{0.03 + 0.09 x_2 + x_3 + x_4}\right) +\\ & && 280 \log\left(\frac{0.03 + x_3 + x_4}{0.03 + 0.07 x_3 + x_4}\right) +\\ & && 290 \log\left(\frac{0.03 + x_4}{0.03 + 0.13 x_4}\right)\\ & && )\\ \end{aligned} \]
Introduce an unbounded variable to model the objective using a constraint:
objvar = model.addVar(lb=-float('inf'), name="objvar")
nle = -32.174 * (
255 * nlfunc.log((0.03 + x2 + x3 + x4) / (0.03 + 0.09 * x2 + x3 + x4))
+ 280 * nlfunc.log((0.03 + x3 + x4) / (0.03 + 0.07 * x3 + x4))
+ 290 * nlfunc.log((0.03 + x4) / (0.03 + 0.13 * x4))
)
model.addGenConstrNL(objvar, nle)
model.setObjective(objvar, sense=GRB.MINIMIZE)
Introduce a fixed variable to model \(f(x) = c\)
\[ \begin{aligned} & \text{s.t.} && 20 (x_2 + x_3 + x_4 - 1)^2 = 0\\ \end{aligned} \]
Note
This could also be written directly as a quadratic constraint (recommended).
model.addConstr(((-1) + x2 + x3 + x4) ** 2 == 0)
Gurobi will handle either form via presolve reductions.
\ LP format - for model browsing. Use MPS format to capture full model detail.
Minimize
0 x2 + 0 x3 + 0 x4 + objvar + 0 resvar
Subject To
Bounds
objvar free
resvar = 0
General Constraints
\ objvar = -32.174 * (((255 * log((0.03 + x2 + x3 + x4) / (0.03 + (0.09 *
\ x2) + x3 + x4))) + (280 * log((0.03 + x3 + x4) / (0.03 + (0.07 * x3) +
\ x4)))) + (290 * log((0.03 + x4) / (0.03 + (0.13 * x4)))))
GC0: objvar = NL : ( MULTIPLY , -1 , -1 ) ( CONSTANT , -32.174 , 0 )
( PLUS , -1 , 0 ) ( PLUS , -1 , 2 ) ( MULTIPLY , -1 , 3 )
( CONSTANT , 255 , 4 ) ( LOG , -1 , 4 ) ( DIVIDE , -1 , 6 )
( PLUS , -1 , 7 ) ( CONSTANT , 0.03 , 8 ) ( VARIABLE , x2 , 8 )
( VARIABLE , x3 , 8 ) ( VARIABLE , x4 , 8 ) ( PLUS , -1 , 7 )
( CONSTANT , 0.03 , 13 ) ( MULTIPLY , -1 , 13 ) ( CONSTANT , 0.09 , 15 )
( VARIABLE , x2 , 15 ) ( VARIABLE , x3 , 13 ) ( VARIABLE , x4 , 13 )
( MULTIPLY , -1 , 3 ) ( CONSTANT , 280 , 20 ) ( LOG , -1 , 20 )
( DIVIDE , -1 , 22 ) ( PLUS , -1 , 23 ) ( CONSTANT , 0.03 , 24 )
( VARIABLE , x3 , 24 ) ( VARIABLE , x4 , 24 ) ( PLUS , -1 , 23 )
( CONSTANT , 0.03 , 28 ) ( MULTIPLY , -1 , 28 ) ( CONSTANT , 0.07 , 30 )
( VARIABLE , x3 , 30 ) ( VARIABLE , x4 , 28 ) ( MULTIPLY , -1 , 2 )
( CONSTANT , 290 , 34 ) ( LOG , -1 , 34 ) ( DIVIDE , -1 , 36 )
( PLUS , -1 , 37 ) ( CONSTANT , 0.03 , 38 ) ( VARIABLE , x4 , 38 )
( PLUS , -1 , 37 ) ( CONSTANT , 0.03 , 41 ) ( MULTIPLY , -1 , 41 )
( CONSTANT , 0.13 , 43 ) ( VARIABLE , x4 , 43 )
\ resvar = 20 * sqr(-1 + x2 + x3 + x4)
GC1: resvar = NL : ( MULTIPLY , -1 , -1 ) ( CONSTANT , 20 , 0 )
( SQUARE , -1 , 0 ) ( PLUS , -1 , 2 ) ( CONSTANT , -1 , 3 )
( VARIABLE , x2 , 3 ) ( VARIABLE , x3 , 3 ) ( VARIABLE , x4 , 3 )
End
Optimize a model with 0 rows, 5 columns and 0 nonzeros
Model fingerprint: 0x4a8801ad
Model has 2 general nonlinear constraints (7 nonlinear terms)
Variable types: 5 continuous, 0 integer (0 binary)
...
Added 15 variables to disaggregate expressions.
Presolve time: 0.00s
Presolved: 83 rows, 27 columns, 165 nonzeros
Presolved model has 12 bilinear constraint(s)
Presolved model has 6 nonlinear constraint(s)
You can model NL expressions naturally in gurobipy. Presolve will transform them to suit Gurobi.
...
Warning: Model contains variables with very large bounds participating
in nonlinear terms.
Presolve was not able to compute smaller bounds for these variables.
Consider bounding these variables or reformulating the model.
Important
Mind the warnings! Bounds are always advisable in MINLP.
...
Solving non-convex MINLP
Variable types: 27 continuous, 0 integer (0 binary)
Root relaxation: objective -7.354055e+04, 25 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 -73540.546 0 14 - -73540.546 - - 0s
0 0 -68838.434 0 11 - -68838.434 - - 0s
0 0 -56834.224 0 15 - -56834.224 - - 0s
H 0 0 -26272.51449 -56834.224 116% - 0s
0 2 -56834.224 0 15 -26272.514 -56834.224 116% - 0s
Explored 597 nodes (1851 simplex iterations) in 0.04 seconds (0.02 work units)
...
Optimal solution found (tolerance 1.00e-04)
Best objective -2.627251448732e+04, best bound -2.627276807630e+04, gap 0.0010%
Relate velocities at gear pitch diameter:
\[ \begin{aligned} \omega_d T_d & = - \omega_a T_a & \\ \omega_b T_b & = - \omega_f T_f & \\ \omega_a & = \omega_b \\ \end{aligned} \]
Find the compound gear ratio:
\[ \begin{aligned} \frac{\omega_d}{\omega_f} & = \frac{\omega_a T_a}{-T_d} \frac{-T_f}{\omega_b T_b} \\ & = \frac{T_f T_a}{T_b T_d} = 6.931 \\ \end{aligned} \]
This is a nonlinear relationship in our integer design variables
Model each gear by its number of teeth
Add gear diameter restrictions to avoid collisions
\[ T_a \le T_b + T_f \]
gurobipy
modelgears = ["d", "a", "b", "f"]
min_teeth = 12
max_teeth = 60
target_ratio = 6.931
model = gp.Model(env=env)
T = model.addVars(
gears,
lb=min_teeth, ub=max_teeth,
vtype=GRB.INTEGER,
name="T",
)
gear_ratio = (T["f"] * T["a"]) / (T["b"] * T["d"]) # NLExpr
z = model.addVar()
model.addGenConstrNL(z, (gear_ratio - target_ratio) ** 2)
model.setObjective(z, sense=GRB.MINIMIZE)
model.addConstr(T["a"] <= T["b"] + T["f"])
Added 4 variables to disaggregate expressions.
Presolve time: 0.00s
Presolved: 16 rows, 10 columns, 44 nonzeros
Presolved model has 4 bilinear constraint(s)
...
Explored 37728 nodes (86875 simplex iterations) in 0.20 seconds (0.05 work units)
Thread count was 11 (of 11 available processors)
Solution count 20: 6.23269e-09 6.23269e-09 6.23269e-09 ... 2.29353e-07
No other solutions better than 2.29353e-07
Power system modelled as a connected network with:
\[ P_i = v_i \sum_{j=1}^N v_j \left( G_{ij} \cos(\theta_i - \theta_j) + B_{ij} \sin(\theta_i - \theta_j) \right) \]
\[ Q_i = v_i \sum_{j=1}^N v_j \left( G_{ij} \sin(\theta_i - \theta_j) + B_{ij} \cos(\theta_i - \theta_j) \right) \]
import gurobipy as gp
from gurobipy import GRB
from gurobipy import nlfunc
env = gp.Env()
model = gp.Model("OptimalPowerFlow", env=env)
P = model.addMVar(N, name="P", lb=P_lb, ub=P_ub) # Real power for buses
Q = model.addMVar(N, name="Q", lb=Q_lb, ub=Q_ub) # Reactive power for buses
v = model.addMVar(N, name="v", lb=v_lb, ub=v_ub) # Voltage magnitude at buses
# Voltage angle at buses
theta = model.addMVar(N, name="theta", lb=theta_lb, ub=theta_ub).reshape(N, 1)
<MLinExpr (4, 4)>
array([[ theta[0] + -1.0 theta[0], theta[0] + -1.0 theta[1],
theta[0] + -1.0 theta[2], theta[0] + -1.0 theta[3]],
[ theta[1] + -1.0 theta[0], theta[1] + -1.0 theta[1],
theta[1] + -1.0 theta[2], theta[1] + -1.0 theta[3]],
[ theta[2] + -1.0 theta[0], theta[2] + -1.0 theta[1],
theta[2] + -1.0 theta[2], theta[2] + -1.0 theta[3]],
[ theta[3] + -1.0 theta[0], theta[3] + -1.0 theta[1],
theta[3] + -1.0 theta[2], theta[3] + -1.0 theta[3]]])
\[ G_{ij} \cos(\theta_i - \theta_j) \]
<MNLExpr (4, 4)>
array([[ 1.7647 * cos(theta[0] + -1.0 * theta[0]),
-0.5882 * cos(theta[0] + -1.0 * theta[1]),
0.0 * cos(theta[0] + -1.0 * theta[2]),
-1.1765 * cos(theta[0] + -1.0 * theta[3])],
[ -0.5882 * cos(theta[1] + -1.0 * theta[0]),
1.5611 * cos(theta[1] + -1.0 * theta[1]),
-0.3846 * cos(theta[1] + -1.0 * theta[2]),
-0.5882 * cos(theta[1] + -1.0 * theta[3])],
[ 0.0 * cos(theta[2] + -1.0 * theta[0]),
-0.3846 * cos(theta[2] + -1.0 * theta[1]),
1.5611 * cos(theta[2] + -1.0 * theta[2]),
-1.1765 * cos(theta[2] + -1.0 * theta[3])],
[ -1.1765 * cos(theta[3] + -1.0 * theta[0]),
-0.5882 * cos(theta[3] + -1.0 * theta[1]),
-1.1765 * cos(theta[3] + -1.0 * theta[2]),
2.9412 * cos(theta[3] + -1.0 * theta[3])]])
\[ \sum_{j=1}^N v_j G_{ij} \cos(\theta_i - \theta_j) \]
1.7647 * cos(theta[0] + -1.0 * theta[0]) * v[0] + -0.5882 * cos(theta[0] + -1.0 * theta[1]) * v[1] + 0.0 * cos(theta[0] + -1.0 * theta[2]) * v[2] + -1.1765 * cos(theta[0] + -1.0 * theta[3]) * v[3]
\[ P_i = v_i \sum_{j=1}^N v_j \left( G_{ij} \cos(\theta_i - \theta_j) + B_{ij} \sin(\theta_i - \theta_j) \right) \]
Single call adds power balance constraints for the entire network!
gurobipy
Add mixed integer nonlinear global optimization to your toolbox!
©️ Gurobi Optimization – gurobi.github.io/slides/