Skip to content

EX 10 — Tools of a CNC machine

Class: BIP · Links: disaggregated activation · Script: python/ex10_tools.py
Difficulty: ★★☆☆☆ · Time: 30–45 min

Open in Colab

One of the fifteen numerical models, of the location and covering family, with a pointer to the selection family.

EX 10

A CNC machine can perform five operations; each one requires a subset of six tools and, if performed, yields a profit:

Operation Required tools Profit (€)
1 1, 3, 4, 5 2000
2 1, 2, 6 1500
3 1, 2, 3, 5 1800
4 2, 4, 5 1700
5 5, 6 800

The magazine holds at most \(k = 4\) tools at a time. The configuration of maximum profit is wanted.

Model

With \(x_i = 1\) if operation \(i\) is performed (\(5\) binaries), \(y_j = 1\) if tool \(j\) is loaded (\(6\) binaries), and \(T_i\) the set of tools required by operation \(i\):

\[ \begin{array}{rrrrrrrrrrrr c l} \max & 2000x_1 & +1500x_2 & +1800x_3 & +1700x_4 & +800x_5 & & & & & & & & \\ \text{subject to} & & & & & & y_1 & +y_2 & +y_3 & +y_4 & +y_5 & +y_6 & \le & 4\\ & x_1 & & & & & -y_1 & & & & & & \le & 0\\ & x_1 & & & & & & & -y_3 & & & & \le & 0\\ & x_1 & & & & & & & & -y_4 & & & \le & 0\\ & x_1 & & & & & & & & & -y_5 & & \le & 0\\ & & x_2 & & & & -y_1 & & & & & & \le & 0\\ & & x_2 & & & & & -y_2 & & & & & \le & 0\\ & & x_2 & & & & & & & & & -y_6 & \le & 0\\ & & & x_3 & & & -y_1 & & & & & & \le & 0\\ & & & x_3 & & & & -y_2 & & & & & \le & 0\\ & & & x_3 & & & & & -y_3 & & & & \le & 0\\ & & & x_3 & & & & & & & -y_5 & & \le & 0\\ & & & & x_4 & & & -y_2 & & & & & \le & 0\\ & & & & x_4 & & & & & -y_4 & & & \le & 0\\ & & & & x_4 & & & & & & -y_5 & & \le & 0\\ & & & & & x_5 & & & & & -y_5 & & \le & 0\\ & & & & & x_5 & & & & & & -y_6 & \le & 0\\ & x_1, & x_2, & x_3, & x_4, & x_5 & & & & & & & \in & \{0, 1\}\\ & & & & & & y_1, & y_2, & y_3, & y_4, & y_5, & y_6 & \in & \{0, 1\} \end{array} \]

With \(n = 5\) operations, \(m = 6\) tools and magazine \(k = 4\): one magazine constraint and \(\sum_i |T_i| = 16\) link constraints. It is disaggregated activation read the other way round: here the operation is the object, the tools are the resources, and the operation needs all of its tools — a conjunction in the consequent, which by the table of chapter 2 costs one constraint per tool.

The model of the instance, written out in full:

Constructive heuristic: the primal bound

It is a maximisation. Constructive heuristic by decreasing profit: the tool set of each operation is loaded, if the magazine allows it.

  • Operation 1 (\(2000\), tools \(1, 3, 4, 5\)): \(4\) new tools are needed and the magazine reaches exactly \(4\): it runs.
  • Operations 3, 4, 2, 5: each would need at least one more tool, and the magazine is full: they are discarded.

\(\mathit{LB} = 2000\).

LP relaxation and dual: the dual bound

Relaxing to \(x_i, y_j \ge 0\), with \(\alpha \ge 0\) for the magazine constraint and \(\beta_{ij} \ge 0\) for each link constraint:

\[ \begin{array}{rrrrrrrrrrrrrrrrrr c l} \min & 4\alpha & & & & & & & & & & & & & & & & & & \\ \text{subject to} & & \beta_{11} & +\beta_{13} & +\beta_{14} & +\beta_{15} & & & & & & & & & & & & & \ge & 2000\\ & & & & & & \beta_{21} & +\beta_{22} & +\beta_{26} & & & & & & & & & & \ge & 1500\\ & & & & & & & & & \beta_{31} & +\beta_{32} & +\beta_{33} & +\beta_{35} & & & & & & \ge & 1800\\ & & & & & & & & & & & & & \beta_{42} & +\beta_{44} & +\beta_{45} & & & \ge & 1700\\ & & & & & & & & & & & & & & & & \beta_{55} & +\beta_{56} & \ge & 800\\ & -\alpha & +\beta_{11} & & & & +\beta_{21} & & & +\beta_{31} & & & & & & & & & \le & 0\\ & -\alpha & & & & & & +\beta_{22} & & & +\beta_{32} & & & +\beta_{42} & & & & & \le & 0\\ & -\alpha & & +\beta_{13} & & & & & & & & +\beta_{33} & & & & & & & \le & 0\\ & -\alpha & & & +\beta_{14} & & & & & & & & & & +\beta_{44} & & & & \le & 0\\ & -\alpha & & & & +\beta_{15} & & & & & & & +\beta_{35} & & & +\beta_{45} & +\beta_{55} & & \le & 0\\ & -\alpha & & & & & & & +\beta_{26} & & & & & & & & & +\beta_{56} & \le & 0\\ & \alpha & & & & & & & & & & & & & & & & & \ge & 0\\ & & \beta_{11}, & \beta_{13}, & \beta_{14}, & \beta_{15}, & \beta_{21}, & \beta_{22}, & \beta_{26}, & \beta_{31}, & \beta_{32}, & \beta_{33}, & \beta_{35}, & \beta_{42}, & \beta_{44}, & \beta_{45}, & \beta_{55}, & \beta_{56} & \ge & 0 \end{array} \]

It reads: \(\beta_{ij}\) is the share of the profit of operation \(i\) attributed to tool \(j\); the first group asks the shares to cover the whole profit, the second that no tool receives more than \(\alpha\); the objective pays \(\alpha\) for each of the \(k\) magazine slots.

The recipe. The profit of each operation is spread in equal parts over the tools it needs, \(\bar\beta_{ij} = p_i / |T_i|\); then \(\bar\alpha = \max_j \sum_{i : j \in T_i} \bar\beta_{ij}\).

\(\bar\beta_{ij} = p_i/\|T_i\|\) tool 1 tool 2 tool 3 tool 4 tool 5 tool 6
op. 1: \(2000/4 = 500\) 500 500 500 500
op. 2: \(1500/3 = 500\) 500 500 500
op. 3: \(1800/4 = 450\) 450 450 450 450
op. 4: \(1700/3\) \(1700/3\) \(1700/3\) \(1700/3\)
op. 5: \(800/2 = 400\) 400 400
total 1450 \(4550/3\) 950 \(3200/3\) \(5750/3\) 900

The critical tool is number \(5\), required by four operations out of five: hence \(\bar\alpha = 5750/3\) and \(\mathit{UB} = k\bar\alpha = 23000/3 \approx 7666.7\).

A dual solution is to be checked, not proposed

A choice that comes naturally is \(\bar\alpha = 2000\) with all \(\bar\beta_{ij} = 900\): the constraints on the operations are satisfied (\(2 \cdot 900 = 1800 \ge 800\) is the tightest case) and the value would be \(4 \cdot 2000 = 8000\). But that solution is not feasible. The second group asks \(\sum_{i : j \in T_i} \beta_{ij} \le \alpha\), and tool \(5\) serves four operations: it would receive \(4 \cdot 900 = 3600 > 2000\). The violation is \(1600\), and \(8000\) is not a bound.

This is why the course protocol does not ask to propose a dual solution but to check it: the script verifies every constraint, and an assert fails if the violation exceeds the tolerance. With the correct recipe the bound is \(23000/3 \approx 7666.7\), that is, better than the one hoped for by getting it wrong.

\(LB\) (constructive heuristic) \(z(\mathit{MILP})\) \(z(\mathit{LP})\) \(z(\mathit{LP}^+)\) \(UB\) (dual by hand) heuristic gap
2000 2500 5200 5200 \(23000/3\) \(20.0\%\)

The optimum loads tools \(2, 4, 5, 6\) and performs operations \(4\) (\(1700\)) and \(5\) (\(800\)). No four-tool configuration yields more: with \(\{1,3,4,5\}\) only operation 1 runs (\(2000\)), with \(\{1,2,3,5\}\) only operation 3 (\(1800\)).

The operation-tool matrix

Why the relaxation is so weak here

\(z(\mathit{LP}) = 5200\) against \(z(\mathit{MILP}) = 2500\): more than double. In the continuous problem one can load "a bit" of every tool — for instance \(y_j = 2/3\) for six tools, which respects \(\sum_j y_j \le 4\) — and run a fraction of every operation. The integrality of the magazine is everything: this is the case where hand-built bounds, however well constructed, are not enough and branch-and-bound is needed. One way to tighten is to add the valid inequalities \(\sum_{j \in T_i} y_j \ge |T_i| x_i\) (the aggregated form of the links).

Code

The complete script — which also checks the infeasibility of the natural dual recipe — is python/ex10_tools.py; the notebook is notebooks/ex10_tools.ipynb.

Show the complete script — python/ex10_tools.py (144 lines)
"""EX 10 -- CNC tools: operation selection with a limited magazine (family 8).

Disaggregated activation the other way round: an operation runs only if *all* of
its tools are loaded, and the magazine holds at most four. It is a maximisation,
so the heuristic gives the lower bound and the dual the upper one.

It comes naturally to propose alpha = 2000 with all multipliers at 900: that dual
solution is *not feasible*, because some tools serve more than two operations.
The recipe used here is different and it is checked.
"""
import gurobipy as gp
import pandas as pd
from gurobipy import GRB

from mip import (ammissibile, due_rilassamenti, frazione, nuovo_modello, registra_bound,
                 risolvi, valuta)
from stile import intestazione, plt, salva_dati, salva_figura
from esteso import salva_modello

R = range

# ---------- 1. MODEL AND INSTANCE ----------
intestazione("EX 10. CNC tools: which operations to run with at most four tools")
pr = [2000, 1500, 1800, 1700, 800]
T = [[0, 2, 3, 4], [0, 1, 5], [0, 1, 2, 4], [1, 3, 4], [4, 5]]
no, nu, K = 5, 6, 4
salva_dati(pd.DataFrame([{"operation": i + 1,
                          "tools": ", ".join(str(j + 1) for j in T[i]),
                          "profit": pr[i]} for i in R(no)]), "ex10_operazioni")


def modello(pr, T, K):
    no, nu = len(pr), max(max(t) for t in T) + 1
    m = nuovo_modello("cnc_tools")
    x = m.addVars(no, vtype=GRB.BINARY, name="x")
    y = m.addVars(nu, vtype=GRB.BINARY, name="y")
    m.setObjective(gp.quicksum(pr[i] * x[i] for i in R(no)), GRB.MAXIMIZE)
    m.addConstr(y.sum() <= K, name="magazine")
    for i in R(no):
        for j in T[i]:
            m.addConstr(x[i] - y[j] <= 0, name=f"link[{i},{j}]")
    return m, x, y


def duale(pr, T, K):
    """min K alpha;  sum_{j in T_i} beta_ij >= p_i;  sum_{i : j in T_i} beta_ij <= alpha;
    alpha, beta >= 0."""
    no, nu = len(pr), max(max(t) for t in T) + 1
    d = nuovo_modello("dual_cnc_tools")
    alpha = d.addVar(name="alpha")
    beta = d.addVars([(i, j) for i in R(no) for j in T[i]], name="beta")
    d.setObjective(K * alpha, GRB.MINIMIZE)
    d.addConstrs((gp.quicksum(beta[i, j] for j in T[i]) >= pr[i] for i in R(no)), name="rc_x")
    d.addConstrs((gp.quicksum(beta[i, j] for i in R(no) if j in T[i]) <= alpha for j in R(nu)),
                 name="rc_y")
    return d


m, x, y = modello(pr, T, K)
salva_modello(m, "ex10_primale")

# ---------- 2. CONSTRUCTIVE HEURISTIC (LOWER BOUND: IT IS A MAXIMISATION) ----------
carichi, eseguite = set(), []
for i in sorted(R(no), key=lambda i: -pr[i]):
    nuovi = set(T[i]) - carichi
    if len(carichi) + len(nuovi) <= K:
        carichi |= nuovi
        eseguite.append(i)
        print(f"  Operation {i + 1} (profit {pr[i]}, tools "
              + ", ".join(str(j + 1) for j in T[i])
              + f"): {len(nuovi)} new ones are needed, the magazine reaches {len(carichi)} <= {K}: it runs")
    else:
        print(f"  Operation {i + 1} (profit {pr[i]}): {len(nuovi)} new tools would be needed, "
              f"the magazine would reach {len(carichi) + len(nuovi)} > {K}: discarded")
lb = sum(pr[i] for i in eseguite)
sol_eur = {f"x[{i}]": 1 for i in eseguite} | {f"y[{j}]": 1 for j in carichi}
assert ammissibile(m, sol_eur)
print("  Heuristic solution: operations " + ", ".join(str(i + 1) for i in sorted(eseguite))
      + " with tools " + ", ".join(str(j + 1) for j in sorted(carichi))
      + f"   lb = {frazione(lb)}")

# ---------- 3. LP RELAXATION AND DUAL (UPPER BOUND) ----------
d = duale(pr, T, K)
salva_modello(d, "ex10_duale")
mano = {f"beta[{i},{j}]": pr[i] / len(T[i]) for i in R(no) for j in T[i]}
carico = {j: sum(mano[f"beta[{i},{j}]"] for i in R(no) if j in T[i]) for j in R(nu)}
mano["alpha"] = max(carico.values())
ub, viol = valuta(d, mano)
assert viol <= 1e-9, viol
print("  Dual by hand: beta_ij = p_i / |T_i| (the profit spread over the tools it needs)")
for i in R(no):
    print(f"    operation {i + 1}: {pr[i]} / {len(T[i])} = "
          f"{frazione(pr[i] / len(T[i]))} on each of its tools")
print("  Load of each tool: " + ", ".join(f"{j + 1}: {frazione(carico[j])}" for j in R(nu)))
utensile_critico = max(carico, key=carico.get)
print(f"  The largest is tool {utensile_critico + 1}, so alpha = "
      f"{frazione(mano['alpha'])} and ub = {K} alpha = {frazione(ub)}")
bozza = {f"beta[{i},{j}]": 900 for i in R(no) for j in T[i]} | {"alpha": 2000}
_, viol_bozza = valuta(d, bozza)
assert viol_bozza > 1e-6
peggiore = max(R(nu), key=lambda j: sum(900 for i in R(no) if j in T[i]))
print("  Check of the natural recipe (alpha = 2000, all beta = 900): NOT feasible,")
print(f"  largest violation {frazione(viol_bozza)}. Tool {peggiore + 1} serves "
      f"{sum(1 for i in R(no) if peggiore in T[i])} operations, so it receives "
      f"{sum(900 for i in R(no) if peggiore in T[i])} > 2000.")
zlp, zlpr, pi = due_rilassamenti(m, d)

# ---------- 4. MILP OPTIMUM AND BOUND TABLE ----------
z = risolvi(m)
op_ott = [i for i in R(no) if x[i].X > 0.5]
ut_ott = [j for j in R(nu) if y[j].X > 0.5]
print("  Optimal solution: operations " + ", ".join(str(i + 1) for i in op_ott)
      + " with tools " + ", ".join(str(j + 1) for j in ut_ott)
      + f"   z(MILP) = {frazione(z)}")
riga = registra_bound("EX 10 CNC tools", ub, lb, zlp, zlpr, z, senso="max")
salva_dati(pd.DataFrame([riga]), "ex10_bound")
assert lb <= z <= zlp + 1e-9 <= ub + 1e-9
print(f"  The sandwich: {frazione(lb)} <= z(MILP) = {frazione(z)} <= z(LP) = {frazione(zlp)} "
      f"<= ub = {frazione(ub)}")
print("  Here the relaxation is very weak: in the continuous problem one can load 'a bit'")
print("  of every tool and run fractions of all the operations.")

# ---------- 5. FIGURE ----------
fig, ax = plt.subplots(figsize=(7.0, 3.2))
for i in R(no):
    for j in R(nu):
        serve = j in T[i]
        colore = ("#0E7490" if i in op_ott else "#F4F6F7") if serve else "white"
        ax.add_patch(plt.Rectangle((j - 0.45, i - 0.4), 0.9, 0.8, facecolor=colore,
                                   edgecolor="#7F8C8D" if serve else "#E5E8E8", lw=0.8))
for j in ut_ott:
    ax.annotate("loaded", (j, no - 0.35), ha="center", va="bottom", fontsize=7.5,
                color="#C0392B")
ax.set_xlim(-0.6, nu - 0.4)
ax.set_ylim(-0.6, no + 0.1)
ax.set_xticks(R(nu))
ax.set_xticklabels([f"tool {j + 1}" for j in R(nu)], fontsize=8)
ax.set_yticks(R(no))
ax.set_yticklabels([f"op. {i + 1} ({pr[i]})" for i in R(no)], fontsize=8)
ax.set_title(f"EX 10: the operations run (teal) and the {K} tools loaded (z = {frazione(z)})")
ax.invert_yaxis()
ax.grid(False)
salva_figura(fig, "ex10_ottimo")
print("Done.")