Skip to content

Signal coverage with interference

Class: BIP · Links: if and only if (threshold + interference) · Script: python/fam08_3_coverage.py
Difficulty: ★★★★☆ · Time: 30–45 min

Open in Colab

Problem 8.3

An operator chooses at most \(k \in \mathbb{Z}_{\ge 1}\) locations, among \(m \in \mathbb{Z}_{\ge 1}\) candidates, to serve \(n \in \mathbb{Z}_{\ge 1}\) clients. \(s_{lc} \in \mathbb{Q}_{\ge 0}\) is the signal received by client \(c\) if \(l\) is installed. A client is covered if and only if the total signal is at least \(t \in \mathbb{Q}_{>0}\) and at most one location generates for it a signal \(\ge b \in \mathbb{Q}_{>0}\). \(p_c \in \mathbb{Q}_{>0}\) is the profit if covered. We want to maximize the total profit.

The problem in words. We decide which locations to install (at most \(k\)). The objective: maximum total profit. The constraints: a client is covered if and only if it receives enough signal and not too much interference; at most \(k\) installed locations.

Model

Data. \(m\), \(n\), \(s_{lc} \in \mathbb{Q}_{\ge 0}\), \(p_c \in \mathbb{Q}_{>0}\), threshold \(t\), interference limit \(b\), budget \(k\). For every client \(c\): \(\mathscr{L}_c = \{l : s_{lc} \ge b\}\).

Decision variables. \(m\) binaries \(x_l\) (location installed), \(n\) binaries \(y_c\) (client covered).

\[ \begin{aligned} \max ~~ \sum_{c=1}^{n} p_c\, y_c & & \\ \text{subject to} \quad -\sum_{l=1}^{m} s_{lc}\, x_l + t\, y_c &\le 0, & \forall c \in \{1, 2, \dots, n\}, \\ \sum_{l \in \mathscr{L}_c} x_l + (m-1)\, y_c &\le m, & \forall c \in \{1, 2, \dots, n\}, \\ \sum_{l=1}^{m} x_l &\le k, & & \\ x_l &\in \{0, 1\}, & \forall l \in \{1, 2, \dots, m\}, \\ y_c &\in \{0, 1\}, & \forall c \in \{1, 2, \dots, n\}. \end{aligned} \]
  • the objective maximizes total profit;
  • the first constraint links coverage and received signal (\(n\) constraints);
  • the second links coverage and interference (\(n\) constraints);
  • the third caps installed locations at \(k\) (one constraint).

The link: an if and only if. One direction — \(y_c=1 \Rightarrow\) signal \(\ge t\) and at most one strong location — is imposed directly by the two constraints. The other direction — if both conditions hold, the client is covered — is not imposed by the constraints (which also allow \(y_c=0\)), but follows from optimality: since \(p_c>0\) and \(y_c\) appears only in these two constraints, raising it to \(1\) remains feasible and raises the objective. The same pattern as problem 7.6.

The model in gurobipy

mod = gp.Model("coverage_interference")
x = mod.addVars(m, vtype=GRB.BINARY, name="x")
y = mod.addVars(n, vtype=GRB.BINARY, name="y")
mod.setObjective(gp.quicksum(p[c] * y[c] for c in range(n)), GRB.MAXIMIZE)
mod.addConstrs((-gp.quicksum(s[l][c] * x[l] for l in range(m)) + t * y[c] <= 0
                for c in range(n)), name="threshold")
mod.addConstrs((gp.quicksum(x[l] for l in L[c]) + (m - 1) * y[c] <= m
                for c in range(n)), name="interference")
mod.addConstr(x.sum() <= k, name="budget")

The instance

\(m=3\), \(n=5\), \(t=5\), \(b=4\), \(k=2\):

\(s_{lc}\) \(c=1\) \(c=2\) \(c=3\) \(c=4\) \(c=5\)
\(l=1\) 6 0 5 3 1
\(l=2\) 4 5 2 0 0
\(l=3\) 0 7 5 4 2
\(c=1\) \(c=2\) \(c=3\) \(c=4\) \(c=5\)
\(p_c\) 10 20 5 15 25

With \(b=4\): \(\mathscr{L}_1=\{1,2\}\), \(\mathscr{L}_2=\{2,3\}\), \(\mathscr{L}_3=\{1,3\}\), \(\mathscr{L}_4=\{3\}\), \(\mathscr{L}_5=\emptyset\).

The model written on the data of the instance:

\[ \begin{array}{rrrrrrrrr c l} \max & & & & 10y_1 & +20y_2 & +5y_3 & +15y_4 & +25y_5 & & \\ \text{subject to} & -6x_1 & -4x_2 & & +5y_1 & & & & & \le & 0\\ & & -5x_2 & -7x_3 & & +5y_2 & & & & \le & 0\\ & -5x_1 & -2x_2 & -5x_3 & & & +5y_3 & & & \le & 0\\ & -3x_1 & & -4x_3 & & & & +5y_4 & & \le & 0\\ & -x_1 & & -2x_3 & & & & & +5y_5 & \le & 0\\ & x_1 & +x_2 & & +2y_1 & & & & & \le & 3\\ & & x_2 & +x_3 & & +2y_2 & & & & \le & 3\\ & x_1 & & +x_3 & & & +2y_3 & & & \le & 3\\ & & & x_3 & & & & +2y_4 & & \le & 3\\ & & & & & & & & 2y_5 & \le & 3\\ & x_1 & +x_2 & +x_3 & & & & & & \le & 2\\ & x_1, & x_2, & x_3 & & & & & & \in & \{0, 1\}\\ & & & & y_1, & y_2, & y_3, & y_4, & y_5 & \in & \{0, 1\} \end{array} \]

Constructive heuristic: the primal bound

The first \(k\) locations open. Client 1: signal \(10\ge5\) but 2 strong locations (\(>1\)): not covered. Client 2: signal \(5\ge5\), 1 strong location: covered. Client 3: signal \(7\ge5\), 1 strong location: covered. Clients 4 and 5: insufficient signal: not covered. Value \(20+5=25\): \(z(\mathit{MILP}) \ge \mathit{LB} = 25\).

LP relaxation and dual: the dual bound

The dual of the linear relaxation, one variable per constraint of the primal:

\[ \begin{aligned} \min ~~ m \sum_{c=1}^{n} \lambda_c + k\, \mu & & \\ \text{subject to} \quad -\sum_{c=1}^{n} s_{lc}\, \pi_c + \sum_{c\, :\, l \in \mathscr{L}_c} \lambda_c + \mu &\ge 0, & \forall l \in \{1, 2, \dots, m\}, \\ t\, \pi_c + (m-1)\, \lambda_c &\ge p_c, & \forall c \in \{1, 2, \dots, n\}, \\ \pi_c &\ge 0, & \forall c \in \{1, 2, \dots, n\}, \\ \lambda_c &\ge 0, & \forall c \in \{1, 2, \dots, n\}, \\ \mu &\ge 0. & & \end{aligned} \]

The same dual, written on the data of the instance:

\[ \begin{array}{rrrrrrrrrrrr c l} \min & & & & & & 3\lambda_1 & +3\lambda_2 & +3\lambda_3 & +3\lambda_4 & +3\lambda_5 & +2\mu & & \\ \text{subject to} & -6\pi_1 & & -5\pi_3 & -3\pi_4 & -\pi_5 & +\lambda_1 & & +\lambda_3 & & & +\mu & \ge & 0\\ & -4\pi_1 & -5\pi_2 & -2\pi_3 & & & +\lambda_1 & +\lambda_2 & & & & +\mu & \ge & 0\\ & & -7\pi_2 & -5\pi_3 & -4\pi_4 & -2\pi_5 & & +\lambda_2 & +\lambda_3 & +\lambda_4 & & +\mu & \ge & 0\\ & 5\pi_1 & & & & & +2\lambda_1 & & & & & & \ge & 10\\ & & 5\pi_2 & & & & & +2\lambda_2 & & & & & \ge & 20\\ & & & 5\pi_3 & & & & & +2\lambda_3 & & & & \ge & 5\\ & & & & 5\pi_4 & & & & & +2\lambda_4 & & & \ge & 15\\ & & & & & 5\pi_5 & & & & & +2\lambda_5 & & \ge & 25\\ & \pi_1, & \pi_2, & \pi_3, & \pi_4, & \pi_5 & & & & & & & \ge & 0\\ & & & & & & \lambda_1, & \lambda_2, & \lambda_3, & \lambda_4, & \lambda_5 & & \ge & 0\\ & & & & & & & & & & & \mu & \ge & 0 \end{array} \]

With \(\bar\pi_c=0\), \(\bar\mu=0\) and \(\bar\lambda_c = p_c/(m-1) = p_c/2\):

\[ \bar\lambda_1=5,\ \bar\lambda_2=10,\ \bar\lambda_3=5/2,\ \bar\lambda_4=15/2,\ \bar\lambda_5=25/2, \]

of value \(m\sum_c\bar\lambda_c = 3\cdot75/2=225/2\). By weak duality (a maximisation problem: the heuristic gives the lower bound, the dual the upper bound), \(\mathit{LB}=25 \le z(\mathit{MILP}) \le z(\mathit{LP}) \le \mathit{UB}=225/2\).

What the solver says. \(z(\mathit{LP}) = 41925/646 \approx 64.9\), \(z(\mathit{LP}^+) = 125/2 = 62.5\). \(z(\mathit{MILP}) = 45\), with locations 1 and 3 installed and clients 1, 2, 4 covered (not 3 or 5): different from what the heuristic found. Heuristic gap \(44.4\%\).

\(LB\) \(UB\) (dual) \(z(\mathit{LP})\) \(z(\mathit{LP}^+)\) \(z(\mathit{MILP})\) heuristic gap
25 \(225/2\) \(41925/646\) \(125/2\) 45 \(44.4\%\)

Optimal solution

Additional considerations

  • Client 5 can never be covered: maximum signal \(1+0+2=3<5\) even opening all locations.
  • For clients with \(|\mathscr{L}_c|\le1\) (4 and 5) the interference constraint is redundant.

Additional modelling questions

8.3.1 — Conditional installation

Location 1 can only be installed if location 3 is also installed. How is this modelled? What is the new optimum?

A worked variant: guaranteed minimum coverage

By contract, at least \(3\) clients must be covered.

Add the linear constraint

\[ \sum_{c=1}^{n} y_c \ge 3 \]

(one linear constraint). On the instance the optimum of problem 8.3 already covers \(3\) clients, so the constraint is not binding and the optimum stays \(45\).

"At least three clients covered" adds \(\omega \le 0\) to the dual. It is best left at zero, and the arithmetic says why: lowering it forces every \(\lambda_c\) up by \(-\omega/(m-1)\), and in the objective those \(\lambda\) weigh more than the right-hand side saves. The heuristic, with few sites, tries every choice of \(k\) and keeps the best feasible one.

value what it is
\(\mathit{UB}\) \(\frac{225}{2}\) dual certificate built by hand
\(\mathit{LB}\) \(45\) heuristic solution
\(z(\mathit{LP})\) \(\frac{41925}{646}\) relaxation without the bounds
\(z(\mathit{LP}^+)\) \(\frac{125}{2}\) relaxation with the bounds
\(z(\mathit{MILP})\) \(45\) optimum of the MILP

Code

Full script — python/fam08_3_coverage.py (reproducible with python3 python/fam08_3_coverage.py from the python/ folder). Notebook — notebooks/fam08_3_coverage.ipynb — opens in Colab from the badge at the top of the page.

Show the complete script — python/fam08_3_coverage.py (227 lines)
"""Problem 8.3 -- Signal coverage with interference (maximum profit).

An "if and only if" as in scheduling problem 7.6: one direction (threshold +
interference => covered) is imposed by two families of link constraints; the
other direction (covered => conditions satisfied) follows from the objective.
"""
from itertools import combinations as combinazioni

import gurobipy as gp
import pandas as pd
from gurobipy import GRB

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

R = range

# ---------- 1. MODEL AND INSTANCE ----------

intestazione("3. Coverage with interference: signal threshold and at most one strong location")
s3 = [[6, 0, 5, 3, 1], [4, 5, 2, 0, 0], [0, 7, 5, 4, 2]]   # signal location l -> client c
p3 = [10, 20, 5, 15, 25]     # profit if client c is covered
t3, b3, k3 = 5, 4, 2         # signal threshold, interference limit, budget of locations
m, n = 3, 5
L3 = [[l for l in R(m) if s3[l][c] >= b3] for c in R(n)]   # L_c: "strong" locations for client c
salva_dati(pd.DataFrame([{"location": l + 1, "client": c + 1, "s": s3[l][c]}
                         for l in R(m) for c in R(n)]), "fam08_3_segnale")
salva_dati(pd.DataFrame({"client": R(1, n + 1), "p": p3}), "fam08_3_clienti")


def modello_3(s, p, t, b, k):
    m, n = len(s), len(p)
    L = [[l for l in R(m) if s[l][c] >= b] for c in R(n)]
    mod = nuovo_modello("coverage_interference")
    x = mod.addVars(m, vtype=GRB.BINARY, name="x")
    y = mod.addVars(n, vtype=GRB.BINARY, name="y")
    mod.setObjective(gp.quicksum(p[c] * y[c] for c in R(n)), GRB.MAXIMIZE)
    mod.addConstrs((-gp.quicksum(s[l][c] * x[l] for l in R(m)) + t * y[c] <= 0 for c in R(n)),
                   name="threshold")
    mod.addConstrs((gp.quicksum(x[l] for l in L[c]) + (m - 1) * y[c] <= m for c in R(n)),
                   name="interference")
    mod.addConstr(x.sum() <= k, name="budget")
    return mod, x, y, L


def duale_3(s, p, t, b, k):
    """min sum m lam_c + k mu;  -sum_c s_lc pi_c + sum_{c in C_l} lam_c + mu >= 0;
    t pi_c + (m-1) lam_c >= p_c;  pi,lam,mu >= 0."""
    m, n = len(s), len(p)
    L = [[l for l in R(m) if s[l][c] >= b] for c in R(n)]
    C = [[c for c in R(n) if l in L[c]] for l in R(m)]
    dl = nuovo_modello("duale_coverage")
    pi = dl.addVars(n, name="pi")
    lam = dl.addVars(n, name="lam")
    mu = dl.addVar(name="mu")
    dl.setObjective(m * lam.sum() + k * mu, GRB.MINIMIZE)
    dl.addConstrs((-gp.quicksum(s[l][c] * pi[c] for c in R(n)) + gp.quicksum(lam[c] for c in C[l]) + mu >= 0
                  for l in R(m)), name="rc_x")
    dl.addConstrs((t * pi[c] + (m - 1) * lam[c] >= p[c] for c in R(n)), name="rc_y")
    return dl


m3, x3, y3, L3m = modello_3(s3, p3, t3, b3, k3)
salva_modello(m3, "fam08_3_primale")

# ---------- 2. THE LP RELAXATION ----------
zlp3, zlp3r, _ = rilassamenti(m3)

# ---------- 3. THE DUAL OF THE RELAXATION (UPPER BOUND: IT IS A MAXIMUM) ----------

d3 = duale_3(s3, p3, t3, b3, k3)
salva_modello(d3, "fam08_3_duale")
mano = {"mu": 0.0}
mano.update({f"pi[{c}]": 0.0 for c in R(n)})
mano.update({f"lam[{c}]": p3[c] / 2 for c in R(n)})
ub3, viol = valuta(d3, mano)
assert viol <= 1e-9, viol
print("Hand-built dual solution: pi = 0, mu = 0, lam_c = p_c/2 = "
      + ", ".join(frazione(p3[c] / 2) for c in R(n)) + f"  ->  ub = {frazione(ub3)}")
dualita_forte(d3, zlp3)

# ---------- 4. CONSTRUCTIVE HEURISTIC (LOWER BOUND: IT IS A MAXIMUM) ----------

print("Heuristic: the first k locations are opened; a client is covered if the total")
print("signal reaches the threshold and at most one strong location reaches it.")


def euristica_3(s, p, t, b, k):
    m, n = len(s), len(p)
    x = [1 if l < k else 0 for l in R(m)]
    y, passi = [0] * n, []
    for c in R(n):
        ts = sum(s[l][c] for l in R(k))
        ni = sum(1 for l in R(k) if s[l][c] >= b)
        y[c] = 1 if (ts >= t and ni <= 1) else 0
        passi.append(f"Client {c + 1}: total signal = {ts}, strong locations = {ni}; "
                     f"{'covered' if y[c] else 'not covered'}.")
    return x, y, passi


xe, ye, passi = euristica_3(s3, p3, t3, b3, k3)
print(f"  The first k = {k3} locations are opened: x = {xe}.")
for i, s in enumerate(passi, 1):
    print(f"  Step {i}. {s}")
lb3 = sum(p3[c] * ye[c] for c in R(n))
print(f"  lb = {lb3}")

# ---------- 5. OPTIMAL SOLUTION OF THE MILP ----------

z3 = risolvi(m3)
print("Optimal solution of the MILP:")
stampa_soluzione(m3, solo_non_nulle=True)
riga = registra_bound("3 coverage", ub3, lb3, zlp3, zlp3r, z3, senso="max")
salva_dati(pd.DataFrame([riga]), "fam08_3_bound")

# ---------- 6. ADDITIONAL MODELLING QUESTIONS ----------

varianti = {}


def variante(nome, mod):
    z = risolvi(mod)
    print(f"  {nome:70s} z = {frazione(z)}")
    return z


# 3a: at least 3 clients must be covered
mod, x, y, L = modello_3(s3, p3, t3, b3, k3)
mod.addConstr(y.sum() >= 3, name="minimum_coverage")
varianti["3a"] = variante("3a. At least 3 clients covered (sum y_c >= 3)", mod)
# 3b: if location 1 is opened, location 3 must also be opened
mod, x, y, L = modello_3(s3, p3, t3, b3, k3)
mod.addConstr(x[0] <= x[2], name="1_implies_3")
varianti["3b"] = variante("3b. If location 1 opens, location 3 also opens (x_1 <= x_3)", mod)
salva_dati(pd.DataFrame({"variant": list(varianti), "z": list(varianti.values())}), "fam08_3_varianti")

# ---------- 7. THE SANDWICH ON THE VARIANT 3a ----------
intestazione("3a. The sandwich on the variant: at least 3 clients covered")


def modello_3a(s, p, t, b, k, minimo=3):
    mod_, xx, yy, LL = modello_3(s, p, t, b, k)
    mod_.addConstr(yy.sum() >= minimo, name="copertura_minima")
    return mod_, xx, yy, LL


def duale_3a(s, p, t, b, k, minimo=3):
    """To the dual of 8.3 one adds omega <= 0 for the constraint sum_c y_c >= minimo
    (a >= direction in a maximisation): it enters the objective with its
    right-hand side and the columns of the y_c."""
    mm, nn = len(s), len(p)
    L = [[l for l in R(mm) if s[l][c] >= b] for c in R(nn)]
    C = [[c for c in R(nn) if l in L[c]] for l in R(mm)]
    dl = nuovo_modello("duale_copertura_3a")
    pi = dl.addVars(nn, name="pi")
    lam = dl.addVars(nn, name="lam")
    mu = dl.addVar(name="mu")
    om = dl.addVar(lb=-GRB.INFINITY, ub=0.0, name="omega")
    dl.setObjective(mm * lam.sum() + k * mu + minimo * om, GRB.MINIMIZE)
    dl.addConstrs((-gp.quicksum(s[l][c] * pi[c] for c in R(nn))
                   + gp.quicksum(lam[c] for c in C[l]) + mu >= 0 for l in R(mm)), name="rc_x")
    dl.addConstrs((t * pi[c] + (mm - 1) * lam[c] + om >= p[c] for c in R(nn)), name="rc_y")
    return dl


m3a, x3a, y3a, L3a = modello_3a(s3, p3, t3, b3, k3)
salva_modello(m3a, "fam08_3a_primale")

# -- feasible heuristic: every choice of k sites is tried and the best is kept --
print("Constructive heuristic: the sites are few, so every choice of k sites is tried and")
print("the one covering at least 3 clients with the highest profit is kept.")
migliore_3a = None
for scelta in combinazioni(R(m), k3):
    cop = []
    for c in R(n):
        segnale = sum(s3[l][c] for l in scelta)
        forti = sum(1 for l in scelta if s3[l][c] >= b3)
        if segnale >= t3 and forti <= 1:
            cop.append(c)
    valore = sum(p3[c] for c in cop)
    print(f"  sites {[l + 1 for l in scelta]}: clients covered {[c + 1 for c in cop]}, "
          f"profit {valore}" + ("" if len(cop) >= 3 else "  (fewer than 3: not feasible)"))
    if len(cop) >= 3 and (migliore_3a is None or valore > migliore_3a[0]):
        migliore_3a = (valore, scelta, cop)
lb3a, sedi_3a, cop_3a = migliore_3a
sol_3a = ({f"x[{l}]": (1 if l in sedi_3a else 0) for l in R(m)}
          | {f"y[{c}]": (1 if c in cop_3a else 0) for c in R(n)})
assert ammissibile(m3a, sol_3a), "the heuristic solution of the variant must be feasible"
print(f"  the best is {[l + 1 for l in sedi_3a]}  ->  lb = {frazione(lb3a)}")

# -- dual certificate: omega stays at zero, and the arithmetic shows it --
d3a = duale_3a(s3, p3, t3, b3, k3)
salva_modello(d3a, "fam08_3a_duale")
mano_3a = {"mu": 0.0, "omega": 0.0}
mano_3a.update({f"pi[{c}]": 0.0 for c in R(n)})
mano_3a.update({f"lam[{c}]": p3[c] / (m - 1) for c in R(n)})
ub3a, viol_3a = valuta(d3a, mano_3a)
assert viol_3a <= 1e-9, viol_3a
print("Dual solution by hand: pi = 0, mu = 0 and lam_c = p_c/(m-1) as in the base problem.")
print("  The new omega is best left at zero: lowering it forces every lam_c up by")
print("  -omega/(m-1) for each of the n clients, and in the objective those m lam weigh")
print("  m*n/(m-1) times more than the right-hand side saves.")
print(f"  ->  ub = {frazione(ub3a)}")
zlp3a, zlp3ar, _ = due_rilassamenti(m3a, d3a)
z3a = risolvi(m3a)
riga_3a = registra_bound("3a at least 3 clients covered", ub3a, lb3a, zlp3a, zlp3ar, z3a, senso="max")
salva_dati(pd.DataFrame([riga_3a]), "fam08_3a_bound")
assert lb3a <= z3a <= zlp3a + 1e-9 <= ub3a + 1e-9

# ---------- 8. FIGURES ----------

fig, ax = plt.subplots(figsize=(7.2, 3.2))
ott_x = [l for l in R(m) if x3[l].X > 0.5]
larghezza = 0.6
for c in R(n):
    colore = "#1E8449" if y3[c].X > 0.5 else "#C0392B"
    ax.bar(c, p3[c], color=colore, width=larghezza)
    ax.text(c, p3[c] + 0.5, "covered" if y3[c].X > 0.5 else "not covered", ha="center", fontsize=8)
ax.set_xticks(R(n))
ax.set_xticklabels([f"client {c + 1}" for c in R(n)])
ax.set_ylabel("profit $p_c$")
ax.set_title(f"Coverage: optimal solution with open locations {[l + 1 for l in ott_x]} (z = {frazione(z3)})")
salva_figura(fig, "cap08_copertura_ottimo")
print("Fine.")