Vai al contenuto

EX 10 — Utensili di una macchina CNC

Classe: BIP · Legami: attivazione disaggregata · Script: python/ex10_utensili.py

Difficoltà: ★★☆☆☆ · Tempo: 30–45 min

Apri in Colab

Uno dei quindici modelli numerici, della famiglia localizzazione e copertura, con rimando alla famiglia della selezione.

EX 10

Una macchina CNC può eseguire cinque operazioni; ciascuna richiede un sottoinsieme di sei utensili e, se eseguita, rende un profitto:

Operazione Utensili richiesti Profitto (€)
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

Il magazzino tiene al più \(k = 4\) utensili alla volta. Si vuole la configurazione di profitto massimo.

Modello

Con \(x_i = 1\) se l'operazione \(i\) è eseguita (\(5\) binarie), \(y_j = 1\) se l'utensile \(j\) è caricato (\(6\) binarie), e \(T_i\) l'insieme degli utensili richiesti dall'operazione \(i\):

\[ \begin{array}{rrrrrrrrrrrr c l} \max & 2000x_1 & +1500x_2 & +1800x_3 & +1700x_4 & +800x_5 & & & & & & & & \\ \text{soggetto a} & & & & & & 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} \]

Con \(n = 5\) operazioni, \(m = 6\) utensili e magazzino \(k = 4\): un vincolo di magazzino e \(\sum_i |T_i| = 16\) vincoli di link. È l'attivazione disaggregata letta al rovescio: qui l'operazione è l'oggetto, gli utensili sono le risorse, e l'operazione richiede tutti i suoi utensili — una congiunzione nel conseguente, che per la tabella del capitolo 2 costa un vincolo per utensile.

Euristica costruttiva: il bound primale

È un massimo. Euristica costruttiva per profitto decrescente: si carica il corredo di ciascuna operazione, se il magazzino lo permette.

  • Operazione 1 (\(2000\), utensili \(1, 3, 4, 5\)): servono \(4\) utensili nuovi, il magazzino arriva esattamente a \(4\): si esegue.
  • Operazioni 3, 4, 2, 5: ciascuna richiederebbe almeno un utensile in più, e il magazzino è pieno: si scartano.

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

Rilassamento LP e duale: il bound duale

Rilassando a \(x_i, y_j \ge 0\), con \(\alpha \ge 0\) per il vincolo di magazzino e \(\beta_{ij} \ge 0\) per ciascun vincolo di link:

\[ \begin{array}{rrrrrrrrrrrrrrrrrr c l} \min & 4\alpha & & & & & & & & & & & & & & & & & & \\ \text{soggetto a} & & \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} \]

Si legge: \(\beta_{ij}\) è la parte del profitto dell'operazione \(i\) attribuita all'utensile \(j\); il primo gruppo chiede che le parti coprano tutto il profitto, il secondo che nessun utensile riceva più di \(\alpha\); l'obiettivo paga \(\alpha\) per ciascuno dei \(k\) posti in magazzino.

La ricetta. Si spalma il profitto di ogni operazione in parti uguali sugli utensili che le servono, \(\bar\beta_{ij} = p_i / |T_i|\); poi \(\bar\alpha = \max_j \sum_{i : j \in T_i} \bar\beta_{ij}\).

\(\bar\beta_{ij} = p_i/\|T_i\|\) ut. 1 ut. 2 ut. 3 ut. 4 ut. 5 ut. 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
totale 1450 \(4550/3\) 950 \(3200/3\) \(5750/3\) 900

L'utensile critico è il \(5\), richiesto da quattro operazioni su cinque: quindi \(\bar\alpha = 5750/3\) e \(\mathit{UB} = k\bar\alpha = 23000/3 \approx 7666{,}7\).

Una soluzione duale va verificata, non proposta

Una scelta che viene naturale è \(\bar\alpha = 2000\) con tutti i \(\bar\beta_{ij} = 900\): i vincoli sulle operazioni sono soddisfatti (\(2 \cdot 900 = 1800 \ge 800\) è il caso più stretto) e il valore sarebbe \(4 \cdot 2000 = 8000\). Ma quella soluzione non è ammissibile. Il secondo gruppo chiede \(\sum_{i : j \in T_i} \beta_{ij} \le \alpha\), e l'utensile \(5\) serve a quattro operazioni: riceverebbe \(4 \cdot 900 = 3600 > 2000\). La violazione è di \(1600\), e \(8000\) non è un bound.

È la ragione per cui il protocollo del corso non chiede di proporre una soluzione duale ma di verificarla: lo script controlla ogni vincolo, e un assert fallisce se la violazione supera la tolleranza. Con la ricetta corretta il bound è \(23000/3 \approx 7666{,}7\), cioè migliore di quello che si sperava di ottenere sbagliando.

\(LB\) (euristica costruttiva) \(z(\mathit{MILP})\) \(z(\mathit{LP})\) \(z(\mathit{LP}^+)\) \(UB\) (duale a mano) gap euristica
2000 2500 5200 5200 \(23000/3\) \(20{,}0\%\)

L'ottimo carica gli utensili \(2, 4, 5, 6\) ed esegue le operazioni \(4\) (\(1700\)) e \(5\) (\(800\)). Nessuna configurazione da quattro utensili rende di più: con \(\{1,3,4,5\}\) si esegue solo l'operazione 1 (\(2000\)), con \(\{1,2,3,5\}\) solo la 3 (\(1800\)).

La matrice operazioni-utensili

Perché qui il rilassamento è così debole

\(z(\mathit{LP}) = 5200\) contro \(z(\mathit{MILP}) = 2500\): più del doppio. Nel continuo si può caricare «un po'» di ogni utensile — per esempio \(y_j = 2/3\) per sei utensili, che rispetta \(\sum_j y_j \le 4\) — ed eseguire una frazione di ogni operazione. L'interezza del magazzino è tutto: è il caso in cui i bound a mano, per quanto ben costruiti, non bastano e serve il branch-and-bound. Un modo per rafforzare è aggiungere le disuguaglianze valide \(\sum_{j \in T_i} y_j \ge |T_i| x_i\) (la forma aggregata dei link).

Codice

Lo script completo — che verifica anche l'inammissibilità della ricetta duale della scelta spontanea — è python/ex10_utensili.py; il notebook è notebooks/ex10_utensili.ipynb.

Mostra lo script completo — python/ex10_utensili.py (152 righe)
"""EX 10 -- Utensili CNC: selezione delle operazioni con magazzino limitato (famiglia 8).

Attivazione disaggregata al rovescio: un'operazione si esegue solo se *tutti* i
suoi utensili sono caricati, e il magazzino ne tiene al piu' quattro. E' un
massimo, quindi l'euristica da' il lower bound e il duale l'upper.

Viene spontaneo proporre alpha = 2000 con tutti i moltiplicatori a 900:
quella soluzione duale *non e' ammissibile*, perche' alcuni utensili servono a
piu' di due operazioni. Qui la ricetta duale e' diversa e viene verificata.
"""
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. MODELLO E ISTANZA ----------
intestazione("EX 10. Utensili CNC: quali operazioni eseguire con al piu' quattro utensili")
pr = [2000, 1500, 1800, 1700, 800]            # profitto delle cinque operazioni
T = [[0, 2, 3, 4], [0, 1, 5], [0, 1, 2, 4], [1, 3, 4], [4, 5]]   # utensili richiesti (0-based)
no, nu, K = 5, 6, 4
salva_dati(pd.DataFrame([{"operazione": i + 1,
                          "utensili": ", ".join(str(j + 1) for j in T[i]),
                          "profitto": 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("utensili_cnc")
    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="magazzino")
    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("duale_utensili")
    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. EURISTICA COSTRUTTIVA (LOWER BOUND: E' UN MASSIMO) ----------
# euristica costruttiva: si scandiscono le operazioni per profitto decrescente e si carica il
# corredo di utensili di ciascuna, finche' il magazzino lo permette
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"  Operazione {i + 1} (profitto {pr[i]}, utensili "
              + ", ".join(str(j + 1) for j in T[i])
              + f"): ne servono {len(nuovi)} nuovi, il magazzino arriva a {len(carichi)} <= {K}: si esegue")
    else:
        print(f"  Operazione {i + 1} (profitto {pr[i]}): servirebbero {len(nuovi)} utensili nuovi, "
              f"il magazzino arriverebbe a {len(carichi) + len(nuovi)} > {K}: si scarta")
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(f"  Soluzione euristica: operazioni " + ", ".join(str(i + 1) for i in sorted(eseguite))
      + " con utensili " + ", ".join(str(j + 1) for j in sorted(carichi))
      + f"   lb = {frazione(lb)}")

# ---------- 3. RILASSAMENTO LP E DUALE (UPPER BOUND) ----------
d = duale(pr, T, K)
salva_modello(d, "ex10_duale")
# ricetta: si spalma il profitto di ogni operazione in parti uguali sui suoi utensili,
# e alpha e' il carico massimo che un utensile riceve
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("  Duale a mano: beta_ij = p_i / |T_i| (il profitto spalmato sugli utensili che servono)")
for i in R(no):
    print(f"    operazione {i + 1}: {pr[i]} / {len(T[i])} = "
          f"{frazione(pr[i] / len(T[i]))} su ciascuno dei suoi utensili")
print("  Carico di ciascun utensile: "
      + ", ".join(f"{j + 1}: {frazione(carico[j])}" for j in R(nu)))
utensile_critico = max(carico, key=carico.get)
print(f"  Il massimo e' l'utensile {utensile_critico + 1}, quindi alpha = "
      f"{frazione(mano['alpha'])} e ub = {K} alpha = {frazione(ub)}")
# la ricetta che viene spontanea NON e' ammissibile: si verifica
tentativo = {f"beta[{i},{j}]": 900 for i in R(no) for j in T[i]} | {"alpha": 2000}
_, viol_tentativo = valuta(d, tentativo)
assert viol_tentativo > 1e-6
peggiore = max(R(nu), key=lambda j: sum(900 for i in R(no) if j in T[i]))
print(f"  Controllo della ricetta spontanea (alpha = 2000, tutti i beta = 900): NON")
print(f"  ammissibile, violazione massima {frazione(viol_tentativo)}. L'utensile "
      f"{peggiore + 1} serve a "
      f"{sum(1 for i in R(no) if peggiore in T[i])} operazioni, quindi riceve "
      f"{sum(900 for i in R(no) if peggiore in T[i])} > 2000.")
zlp, zlpr, pi = due_rilassamenti(m, d)

# ---------- 4. OTTIMO DEL MILP E TABELLA DEI BOUND ----------
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(f"  Soluzione ottima: operazioni " + ", ".join(str(i + 1) for i in op_ott)
      + " con utensili " + ", ".join(str(j + 1) for j in ut_ott)
      + f"   z(MILP) = {frazione(z)}")
riga = registra_bound("EX 10 utensili CNC", 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"  Il sandwich: {frazione(lb)} <= z(MILP) = {frazione(z)} <= z(LP) = {frazione(zlp)} "
      f"<= ub = {frazione(ub)}")
print("  Qui il rilassamento e' molto debole: nel continuo si puo' caricare 'un po'' di")
print("  ogni utensile ed eseguire frazioni di tutte le operazioni.")

# ---------- 5. FIGURA ----------
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("caricato", (j, no - 0.35), ha="center", va="bottom", fontsize=7.5,
                color="#C0392B", rotation=0)
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"ut. {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: le operazioni eseguite (in teal) e i {K} utensili caricati "
             f"(z = {frazione(z)})")
ax.invert_yaxis()
ax.grid(False)
salva_figura(fig, "ex10_ottimo")
print("Fine.")