MILP

Overview

The MILP function solves mixed-integer linear programming problems, a class of mathematical optimization where some or all decision variables are constrained to take integer values. MILP extends standard linear programming by adding integrality constraints, making it suitable for problems involving discrete decisions such as scheduling, resource allocation, network design, and capital budgeting.

This implementation uses scipy.optimize.milp, which is a wrapper around the HiGHS optimization solver—a high-performance, open-source solver developed at the University of Edinburgh. HiGHS employs a branch-and-cut algorithm for MIP problems, combining the dual revised simplex method with cutting planes and presolve techniques to efficiently find optimal or near-optimal solutions.

The function solves problems of the form:

\min_{x} \quad c^T x subject to: b_l \leq A x \leq b_u, \quad l \leq x \leq u, \quad x_i \in \mathbb{Z} \; \forall i \in I

where c is the vector of objective coefficients, A is the constraint matrix, b_l and b_u are constraint bounds, l and u are variable bounds, and I is the set of indices for integer-constrained variables.

The function supports four types of variable integrality: continuous (0), integer (1), semi-continuous (2), and semi-integer (3). Semi-continuous and semi-integer variables must either equal zero or fall within their specified bounds—useful for modeling minimum lot sizes or on/off decisions.

For more details on the underlying algorithm, see the SciPy MILP documentation and the HiGHS GitHub repository. For background on integer programming concepts, see Integer programming on Wikipedia.

This example function is provided as-is without any representation of accuracy.

Excel Usage

=MILP(c, integrality, bounds_lower, bounds_upper, a_ub, b_ub, a_eq, b_eq)
  • c (list[list], required): Objective coefficients as a single row or column vector.
  • integrality (list[list], optional, default: null): Variable integrality codes (0=continuous, 1=integer, 2=semi-continuous, 3=semi-integer).
  • bounds_lower (list[list], optional, default: null): Lower bounds for each decision variable.
  • bounds_upper (list[list], optional, default: null): Upper bounds for each decision variable.
  • a_ub (list[list], optional, default: null): Inequality constraint coefficient matrix (A_ub x <= b_ub).
  • b_ub (list[list], optional, default: null): Right-hand side values for inequality constraints.
  • a_eq (list[list], optional, default: null): Equality constraint coefficient matrix (A_eq x = b_eq).
  • b_eq (list[list], optional, default: null): Right-hand side values for equality constraints.

Returns (list[list]): 2D list [[x1, x2, …, objective]], or error message string.

Example 1: Integer variables with multiple inequality constraints

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:

Result
2 2 -2
Example 2: Bounded integer optimization with production constraint

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:

Result
3 0 3
Example 3: Mixed continuous and integer variables with equality constraint

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:

Result
0 5 10
Example 4: Continuous LP with default bounds and inequality constraints

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:

Result
0 0 0 0

Python Code

Show Code
import math
import numpy as np
from scipy.optimize import Bounds, LinearConstraint
from scipy.optimize import milp as scipy_milp

def milp(c, integrality=None, bounds_lower=None, bounds_upper=None, a_ub=None, b_ub=None, a_eq=None, b_eq=None):
    """
    Solve a mixed-integer linear program using scipy.optimize.milp.

    See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        c (list[list]): Objective coefficients as a single row or column vector.
        integrality (list[list], optional): Variable integrality codes (0=continuous, 1=integer, 2=semi-continuous, 3=semi-integer). Default is None.
        bounds_lower (list[list], optional): Lower bounds for each decision variable. Default is None.
        bounds_upper (list[list], optional): Upper bounds for each decision variable. Default is None.
        a_ub (list[list], optional): Inequality constraint coefficient matrix (A_ub x <= b_ub). Default is None.
        b_ub (list[list], optional): Right-hand side values for inequality constraints. Default is None.
        a_eq (list[list], optional): Equality constraint coefficient matrix (A_eq x = b_eq). Default is None.
        b_eq (list[list], optional): Right-hand side values for equality constraints. Default is None.

    Returns:
        list[list]: 2D list [[x1, x2, ..., objective]], or error message string.
    """
    try:
        def to2d(x):
            return [[x]] if not isinstance(x, list) else x

        c = to2d(c)
        if integrality is not None:
            integrality = to2d(integrality)
        if bounds_lower is not None:
            bounds_lower = to2d(bounds_lower)
        if bounds_upper is not None:
            bounds_upper = to2d(bounds_upper)
        if a_ub is not None:
            a_ub = to2d(a_ub)
        if b_ub is not None:
            b_ub = to2d(b_ub)
        if a_eq is not None:
            a_eq = to2d(a_eq)
        if b_eq is not None:
            b_eq = to2d(b_eq)

        try:
            c_vec = np.array(c, dtype=float).flatten()
        except Exception:
            return "Error: c must be a 2D list of numeric values."

        if c_vec.size == 0:
            return "Error: c must contain at least one coefficient."

        n_vars = c_vec.size

        def _to_array(values, name):
            if values is None:
                return None
            try:
                arr = np.array(values, dtype=float)
            except Exception:
                return f"Error: {name} must contain numeric values."
            return arr

        integrality_arr = None
        if integrality is not None:
            try:
                integrality_arr = np.array(integrality, dtype=int).flatten()
            except Exception:
                return "Error: integrality must be integers."
            if integrality_arr.size != n_vars:
                return "Error: integrality length must match number of variables."
            if not np.all(np.isin(integrality_arr, [0, 1, 2, 3])):
                return "Error: integrality values must be 0 (continuous), 1 (integer), 2 (semi-continuous), or 3 (semi-integer)."

        lower_arr = None
        upper_arr = None
        if bounds_lower is not None:
            lower_arr = _to_array(bounds_lower, "bounds_lower")
            if isinstance(lower_arr, str):
                return lower_arr
            lower_arr = lower_arr.flatten()
        if bounds_upper is not None:
            upper_arr = _to_array(bounds_upper, "bounds_upper")
            if isinstance(upper_arr, str):
                return upper_arr
            upper_arr = upper_arr.flatten()

        if lower_arr is not None and lower_arr.size != n_vars:
            return "Error: bounds_lower must provide one value per variable."
        if upper_arr is not None and upper_arr.size != n_vars:
            return "Error: 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 "Error: 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:
            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)

        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"Error: {sense} constraints require both matrix and vector."
            mat = _to_array(matrix, f"{sense} matrix")
            if isinstance(mat, str):
                return mat
            vec = _to_array(vector, f"{sense} vector")
            if isinstance(vec, str):
                return vec
            vec = vec.flatten()
            if mat.ndim == 1:
                mat = mat.reshape(1, -1)
            if mat.shape[1] != n_vars:
                return f"Error: {sense} matrix must have {n_vars} columns."
            if vec.size != mat.shape[0]:
                return f"Error: {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

        result = scipy_milp(
            c_vec,
            integrality=integrality_arr,
            bounds=bounds_obj,
            constraints=constraints,
        )

        if not result.success:
            message = result.message if hasattr(result, "message") else "MILP solver did not succeed."
            return f"Error: {message}"

        solution = result.x
        if solution is None:
            return "Error: No solution returned."

        try:
            solution_list = list(map(float, solution))
        except Exception:
            return "Error: 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]]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Objective coefficients as a single row or column vector.
Variable integrality codes (0=continuous, 1=integer, 2=semi-continuous, 3=semi-integer).
Lower bounds for each decision variable.
Upper bounds for each decision variable.
Inequality constraint coefficient matrix (A_ub x <= b_ub).
Right-hand side values for inequality constraints.
Equality constraint coefficient matrix (A_eq x = b_eq).
Right-hand side values for equality constraints.