MILP
Overview
The MILP function solves mixed-integer linear programs by wrapping scipy.optimize.milp. It minimizes a linear objective subject to linear equality and inequality constraints while supporting continuous, integer, semi-continuous, and semi-integer decision variables. This example function is provided as-is without any representation of accuracy.
Usage
To call the function in Excel:
=MILP(c, [integrality], [bounds_lower], [bounds_upper], [a_ub], [b_ub], [a_eq], [b_eq])c(2D list, required): Objective coefficients as a single row or column vector.integrality(2D list, optional): Variable integrality codes. Valid options per entry:0(continuous),1(integer),2(semi-continuous),3(semi-integer).bounds_lower(2D list, optional): Lower bounds for each variable. Use a single row with one value per variable.bounds_upper(2D list, optional): Upper bounds for each variable. Use a single row with one value per variable.a_ub(2D list, optional): Inequality constraint coefficients fora_ub * x <= b_ub.b_ub(2D list, optional): Right-hand sides for inequality constraints.a_eq(2D list, optional): Equality constraint coefficients fora_eq * x = b_eq.b_eq(2D list, optional): Right-hand sides for equality constraints.
The function returns a single-row 2D list containing the optimal variable values followed by the objective value, or an error message string if the problem is infeasible or input validation fails.
Examples
Example 1: Integer Assignment with All Parameters
Inputs:
| c | integrality | bounds_lower | bounds_upper | a_ub | b_ub | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -1 | 1 | 1 | 0 | 0 | 5 | 5 | -1 | 1 | 1 | ||
| 3 | 2 | 12 | ||||||||||
| 2 | 3 | 12 |
Excel formula:
=MILP({0,-1}, {1,1}, {0,0}, {5,5}, {-1,1;3,2;2,3}, {1;12;12})Expected output:
| x₁ | x₂ | Objective |
|---|---|---|
| 2.000 | 2.000 | -2.000 |
This example demonstrates using non-default values for all optional parameters.
Example 2: Bounded Production Mix
Inputs:
| c | integrality | bounds_lower | bounds_upper | a_ub | b_ub | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 1 | 1 | 0 | 0 | 4 | 3 | -1 | -1 | -3 |
Excel formula:
=MILP({1,2}, {1,1}, {0,0}, {4,3}, {-1,-1}, {-3})Expected output:
| x₁ | x₂ | Objective |
|---|---|---|
| 3.000 | 0.000 | 3.000 |
Example 3: Mixed Continuous and Integer Variables
Inputs:
| c | integrality | a_eq | b_eq | bounds_lower | bounds_upper | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 3 | 2 | 0 | 1 | 1 | 1 | 5 | 0 | 0 | 5 | 5 |
Excel formula:
=MILP({3,2}, {0,1}, , , , , {1,1}, {5}, {0,0}, {5,5})Expected output:
| x₁ | x₂ | Objective |
|---|---|---|
| 0.000 | 5.000 | 10.000 |
Example 4: Continuous Variables with Default Bounds
Inputs:
| c | a_ub | b_ub | |||||
|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 2 | 0 | 4 | |
| 0 | 1 | 1 | 3 |
Excel formula:
=MILP({1,1,1}, , , , {1,2,0;0,1,1}, {4;3})Expected output:
| x₁ | x₂ | x₃ | Objective |
|---|---|---|---|
| 0.000 | 0.000 | 0.000 | 0.000 |
Python Code
import math
from typing import List, Optional, Union
import numpy as np
from scipy.optimize import Bounds, LinearConstraint, milp as scipy_milp
def milp(
c: Union[List[List[float]], float], # Can be 2D list or scalar
integrality: Optional[Union[List[List[int]], int]] = None, # Can be 2D list or scalar
bounds_lower: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
bounds_upper: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
a_ub: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
b_ub: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
a_eq: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
b_eq: Optional[Union[List[List[float]], float]] = None, # Can be 2D list or scalar
):
"""
Solve a mixed-integer linear program using scipy.optimize.milp.
Wrapper for scipy.optimize.milp function (see: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html).
Args:
c: 2D list representing the objective coefficients (row or column vector).
integrality: 2D list matching the length of c that specifies variable integrality (0=continuous, 1=integer, 2=semi-continuous, 3=semi-integer).
bounds_lower: 2D list providing lower bounds for each variable.
bounds_upper: 2D list providing upper bounds for each variable.
a_ub: 2D list for inequality constraint coefficients (A_ub x <= b_ub).
b_ub: 2D list for inequality constraint limits.
a_eq: 2D list for equality constraint coefficients (A_eq x == b_eq).
b_eq: 2D list for equality constraint limits.
Returns:
list[list[float]] or str: [[x1, x2, ..., optimal_value]] if successful, otherwise an error message string.
This example function is provided as-is without any representation of accuracy.
"""
# Validate and flatten objective coefficients
# Normalize scalar inputs (Excel passes [[x]] as x) to 2D lists
def normalize_to_2d_list(value):
if not isinstance(value, list):
return [[value]]
return value
c = normalize_to_2d_list(c)
if integrality is not None:
integrality = normalize_to_2d_list(integrality)
if bounds_lower is not None:
bounds_lower = normalize_to_2d_list(bounds_lower)
if bounds_upper is not None:
bounds_upper = normalize_to_2d_list(bounds_upper)
if a_ub is not None:
a_ub = normalize_to_2d_list(a_ub)
if b_ub is not None:
b_ub = normalize_to_2d_list(b_ub)
if a_eq is not None:
a_eq = normalize_to_2d_list(a_eq)
if b_eq is not None:
b_eq = normalize_to_2d_list(b_eq)
try:
c_vec = np.array(c, dtype=float).flatten()
except Exception:
return "Invalid input: c must be a 2D list of numeric values."
if c_vec.size == 0:
return "Invalid input: c must contain at least one coefficient."
n_vars = c_vec.size
# Helper to extract 1D arrays from potential 2D lists
def _to_array(values, name):
if values is None:
return None
try:
arr = np.array(values, dtype=float)
except Exception:
raise ValueError(f"Invalid input: {name} must contain numeric values.")
return arr
# Integrality handling
integrality_arr = None
if integrality is not None:
try:
integrality_arr = np.array(integrality, dtype=int).flatten()
except Exception:
return "Invalid input: integrality must be integers."
if integrality_arr.size != n_vars:
return "Invalid input: integrality length must match number of variables."
if not np.all(np.isin(integrality_arr, [0, 1, 2, 3])):
return "Invalid input: integrality values must be 0 (continuous), 1 (integer), 2 (semi-continuous), or 3 (semi-integer)."
# Bounds handling
lower_arr = None
upper_arr = None
try:
if bounds_lower is not None:
lower_arr = _to_array(bounds_lower, "bounds_lower").flatten()
if bounds_upper is not None:
upper_arr = _to_array(bounds_upper, "bounds_upper").flatten()
except ValueError as exc:
return str(exc)
if lower_arr is not None and lower_arr.size != n_vars:
return "Invalid input: bounds_lower must provide one value per variable."
if upper_arr is not None and upper_arr.size != n_vars:
return "Invalid input: bounds_upper must provide one value per variable."
if lower_arr is not None and upper_arr is not None:
if np.any(lower_arr > upper_arr):
return "Invalid input: each lower bound must be less than or equal to the corresponding upper bound."
bounds_obj = None
if lower_arr is not None or upper_arr is not None:
# SciPy requires both vectors; if one missing, fill with defaults
if lower_arr is None:
lower_arr = np.zeros(n_vars)
if upper_arr is None:
upper_arr = np.full(n_vars, math.inf)
bounds_obj = Bounds(lower_arr, upper_arr)
# Constraint processing helper
def _build_linear_constraint(matrix, vector, sense):
if matrix is None and vector is None:
return None
if matrix is None or vector is None:
return f"Invalid input: {sense} constraints require both matrix and vector."
mat = _to_array(matrix, f"{sense} matrix")
vec = _to_array(vector, f"{sense} vector").flatten()
if mat.ndim == 1:
mat = mat.reshape(1, -1)
if mat.shape[1] != n_vars:
return f"Invalid input: {sense} matrix must have {n_vars} columns."
if vec.size != mat.shape[0]:
return f"Invalid input: {sense} vector length must equal number of rows in the matrix."
if sense == "inequality":
return LinearConstraint(mat, -np.inf, vec)
return LinearConstraint(mat, vec, vec)
constraints = []
for sense, matrix, vector in (
("inequality", a_ub, b_ub),
("equality", a_eq, b_eq),
):
constraint = _build_linear_constraint(matrix, vector, sense)
if isinstance(constraint, str):
return constraint
if constraint is not None:
constraints.append(constraint)
if len(constraints) == 0:
constraints = None
try:
result = scipy_milp(
c_vec,
integrality=integrality_arr,
bounds=bounds_obj,
constraints=constraints,
)
except Exception as exc:
return f"Error during MILP solving: {exc}"
if not result.success:
message = result.message if hasattr(result, "message") else "MILP solver did not succeed."
return f"MILP solving failed: {message}"
solution = result.x
if solution is None:
return "MILP solving failed: no solution returned."
try:
solution_list = list(map(float, solution))
except Exception:
return "Error converting MILP solution to floats."
objective = float(result.fun) if result.fun is not None else float(np.dot(c_vec, solution))
return [solution_list + [objective]]