9.6 Integer programming in software

Software 9.6.1 (Branch and bound). A depth-first branch and bound on Example 9.3.2, with each subproblem’s LP solved by linprog and printed with its bounds; then SciPy’s milp, which calls the HiGHS branch-and-cut solver:

>>> import numpy as np
>>> from scipy.optimize import linprog, milp, LinearConstraint, Bounds
>>> c = np.array([5., 4])                  # max 5 x1 + 4 x2
>>> A = np.array([[6., 4], [1, 2]]); b = np.array([24., 6])
>>> best = {'z': -np.inf, 'x': None}
>>> def branch(bounds, depth=0):
...     r = linprog(-c, A_ub=A, b_ub=b, bounds=bounds, method='highs')
...     tag = '  ' * depth + str([(lo, hi) for lo, hi in bounds])
...     if r.status != 0: print(tag, 'infeasible'); return
...     z, x = -r.fun, r.x
...     if z <= best['z'] + 1e-9: print(tag, f"bound {z:.2f}: pruned"); return
...     frac = [j for j in range(2) if abs(x[j] - round(x[j])) > 1e-9]
...     if not frac:
...         best.update(z=z, x=x + 0.0)
...         print(tag, f"integer {x + 0.0}, z = {z:.0f}"); return
...     j = frac[0]
...     print(tag, f"LP {np.round(x, 3)}, z = {z:.2f}, branch x{j+1}")
...     lo, hi = bounds[j]
...     down = list(bounds); down[j] = (lo, np.floor(x[j]))
...     up = list(bounds); up[j] = (np.ceil(x[j]), hi)
...     branch(down, depth + 1); branch(up, depth + 1)
...
>>> branch([(0, None), (0, None)])
[(0, None), (0, None)] LP [3.  1.5], z = 21.00, branch x2
  [(0, None), (0, 1.0)] LP [3.333 1.   ], z = 20.67, branch x1
    [(0, 3.0), (0, 1.0)] integer [3. 1.], z = 19
    [(4.0, None), (0, 1.0)] integer [4. 0.], z = 20
  [(0, None), (2.0, None)] bound 18.00: pruned
>>> best['z'], best['x']
(20.0, array([4., 0.]))
>>> r = milp(-c, constraints=LinearConstraint(A, ub=b), integrality=[1, 1])
>>> r.x, -r.fun                            # SciPy's branch and cut (HiGHS)
(array([4., 0.]), 20.0)

Reading the output. Five nodes reproduce the tree of Figure 9: the root, the node \(x_2 \leq 1\), its two integer children, and the pruned node \(x_2 \geq 2\). Both methods find \((4, 0)\) with \(z = 20\).

MATLAB/Octave: intlinprog(-c, [1 2], A, b).

Software 9.6.2 (A Gomory cut). The \(x_2\) row of the optimal tableau of Example 9.3.2, computed as row \(2\) of \(B^{-1}[A \mid I]\); its fractional parts on the slacks; the cut rewritten in \(x_1\), \(x_2\); and the LP with the cut added:

>>> from fractions import Fraction as F
>>> B = np.array([[6., 4], [1, 2]]); Binv = np.linalg.inv(B)
>>> row_x2 = Binv[1] @ np.hstack([A, np.eye(2)])   # x2 row: coefficients
>>> rhs = Binv[1] @ b
>>> [str(F(v).limit_denominator(100)) for v in row_x2], str(F(rhs))
(['0', '1', '-1/8', '3/4'], '3/2')
>>> frac = lambda v: v - np.floor(v)  # cut: sum frac(a_j) s_j >= frac(b)
>>> cut = frac(row_x2[2:]); cut, frac(rhs)  # on the slacks s1, s2
(array([0.875, 0.75 ]), 0.5)
>>> g = -(cut @ A); h = frac(rhs) - cut @ b       # g x >= h
>>> g, h
(array([-6., -5.]), -25.0)
>>> r = linprog(-c, A_ub=np.vstack([A, -g]), b_ub=np.r_[b, -h],
...             method='highs')
>>> r.x, -r.fun                            # the cut moves the LP optimum
(array([3.33333333, 1.        ]), 20.666666666666668)

Reading the output. The row is \(x_2 - \frac 18s_1 + \frac 34s_2 = \frac 32\); the cut \(0.875s_1 + 0.75s_2 \geq 0.5\) becomes \(-6x_1 - 5x_2 \geq -25\), that is \(6x_1 + 5x_2 \leq 25\). With it the LP optimum moves from \((3, 1.5)\) to \((3.33, 1)\), and the bound falls from \(21\) to \(20.67\); further cuts would be needed to reach \(20\).

MATLAB/Octave: B ∖ [A eye(2)] gives the tableau rows.

Software 9.6.3 (Inspectors and goals). The inspector problem of Example 1.2.2 as an IP; then a goal programme with variables \(x_1, x_2\) and deviations \(d_1^\mp \) (profit \(40x_1 + 30x_2\), goal at least \(1000\)) and \(d_2^\mp \) (labour \(3x_1 + 4x_2\), goal at most \(60\) hours), with capacity \(x_1 + x_2 \leq 30\), under two weightings:

>>> pieces = LinearConstraint([[320, 240]], lb=2000)    # pieces a day
>>> r = milp([68.8, 68.0], constraints=pieces,
...          integrality=[1, 1], bounds=Bounds([0, 0], [9, 11]))
>>> r.x, round(r.fun, 1)              # the inspectors of Section 1, as an IP
(array([4., 3.]), 479.2)
>>> Ae = [[40, 30, 1, -1, 0, 0],           # 40 x1 + 30 x2 + d1- - d1+ = 1000
...       [3, 4, 0, 0, 1, -1]]             # 3 x1 + 4 x2 + d2- - d2+ = 60
>>> for name, w in (('profit first ', [0, 0, 1000, 0, 0, 1]),
...                 ('labour x 20  ', [0, 0, 1, 0, 0, 20])):
...     r = linprog(w, A_eq=Ae, b_eq=[1000, 60], A_ub=[[1, 1, 0, 0, 0, 0]],
...                 b_ub=[30], method='highs')        # and x1 + x2 <= 30
...     print(name, np.round(r.x, 2))
...
profit first  [25.  0.  0.  0.  0. 15.]
labour x 20   [ 20.   0. 200.   0.   0.   0.]

Reading the output. The inspector IP gives \(4\) Grade 1 and \(3\) Grade 2 inspectors at Rs. 479.2, the answer found by enumeration in Section 1. In the goal programme the two goals conflict. With profit given priority, \(x_1 = 25\) meets the profit goal exactly and overruns labour by \(15\) hours (\(d_2^+ = 15\)). With an hour weighted as Rs. 20 of profit, \(x_1 = 20\) meets the labour goal and falls Rs. 200 short of the profit goal. The model does not choose between them; the weights, set by the decision maker, do.

MATLAB/Octave: intlinprog and linprog with the same matrices.

Questions on this section

Stuck on something here? Ask below and it stays attached to this topic.