Skip to content

Complete Pipeline — Build a Hub Location Model from Scratch

This guide builds a single-allocation hub location problem using the core library.

The complete, runnable code is at examples/full_pipeline.py.

What We're Building

The classical Uncapacitated Hub Location Problem (UHLP):

  • Choose \(p\) nodes to be hubs
  • Assign every node to exactly one hub
  • Minimize total transport cost (with inter-hub discount \(\alpha\))
  • Flow goes: origin → first hub → second hub → destination

The Code

"""Complete Pipeline: Building a Hub Location Model from Scratch.

Runnable example: single-allocation uncapacitated hub location problem
built with the core library.

"""

from gurobipy import GRB, quicksum

from bilevelpy import (
    BaseModel, BaseModelSolution, Constraint, DataCol,
    Dataset, ModelMetaData, ModelSolver, SolutionRegistry,
    Variable, VariableMetaData,
)
from bilevelpy.data.builder import DatasetBuilder
from bilevelpy.data.loaders import HLPLoader
from bilevelpy.data.processor import HLPNodeSelector, HLPCostScaling

# ---------------------------------------------------------------------------
# Step 1: Load and prepare data
# ---------------------------------------------------------------------------

dataset = (
    DatasetBuilder()
    .pipe(HLPLoader(Dataset.CAB25)) # we load the CAB25 Dataset
    .pipe(HLPNodeSelector(n_nodes=10)) # we select 10 nodes from the dataset
    .pipe(HLPCostScaling(scaling_factor=100)) # divide costs by 100
    .build()
)

nodes = list(dataset[DataCol.NODE_ID].values)
print(f"Nodes: {nodes}")


# ---------------------------------------------------------------------------
# Step 2: Variable — binary x[i,k] for hub allocation
# ---------------------------------------------------------------------------

class AllocationVariable(Variable):
    r"""Binary: $x_{ik} = 1$ if node $i$ assigned to hub $k$.

    $x_{kk} = 1$ means node $k$ is open as a hub.
    """

    var_metadata = VariableMetaData(
        value="x",
        display_name="Hub Allocation",
        identifiers=[DataCol.START_NODE, DataCol.END_NODE],
    )

    def build(self, model: "BaseModel"):
        nodes = list(model.data[DataCol.NODE_ID].values)
        return model.model.addVars(nodes, nodes, vtype=GRB.BINARY, name="x")


# ---------------------------------------------------------------------------
# Step 3: Constraints
# ---------------------------------------------------------------------------

class ExactHubsConstraint(Constraint):
    r"""Exactly $p$ hubs: $\sum_{k \in V} x_{kk} = p$."""

    required_vars = [AllocationVariable]

    def build(self, model: "BaseModel", **kwargs):
        n_hubs = kwargs["n_hubs"]
        nodes = list(model.data[DataCol.NODE_ID].values)
        x = model.vars[AllocationVariable]
        model.add_constr(
            quicksum(x[k, k] for k in nodes) == n_hubs, name="exact_hubs"
        )


class SingleAssignmentConstraint(Constraint):
    r"""Every node to exactly one hub: $\sum_k x_{ik} = 1$."""

    required_vars = [AllocationVariable]

    def build(self, model: "BaseModel", **kwargs):
        nodes = list(model.data[DataCol.NODE_ID].values)
        x = model.vars[AllocationVariable]
        for i in nodes:
            model.add_constr(
                quicksum(x[i, k] for k in nodes) == 1, name=f"assign_{i}"
            )


class HubLinkConstraint(Constraint):
    r"""Can only assign to open hubs: $x_{ik} \leq x_{kk}$."""

    required_vars = [AllocationVariable]

    def build(self, model: "BaseModel", **kwargs):
        nodes = list(model.data[DataCol.NODE_ID].values)
        x = model.vars[AllocationVariable]
        for i in nodes:
            for k in nodes:
                model.add_constr(x[i, k] <= x[k, k], name=f"link_{i}_{k}")


# ---------------------------------------------------------------------------
# Step 4: Model — quadratic objective with inter-hub discount α
# ---------------------------------------------------------------------------

@SolutionRegistry.register_for(BaseModelSolution)
class HubLocationModel(BaseModel):
    """Single-allocation uncapacitated hub location problem."""

    model_metadata = ModelMetaData(value="UHLP", display_name="Uncapacitated HLP")

    def __init__(self, n_hubs: int, alpha: float, data):
        super().__init__(data)
        self._n_hubs = n_hubs
        self._alpha = alpha
        self.build(
            variables=[AllocationVariable],
            constraints=[
                ExactHubsConstraint,
                SingleAssignmentConstraint,
                HubLinkConstraint,
            ],
            n_hubs=n_hubs,
        )

    def _set_objective(self, **kwargs):
        r"""Minimize $\sum w_{ij} x_{ik} x_{jm} (c_{ik} + \alpha c_{km} + c_{mj})$."""
        costs = self.data[DataCol.COST_NODE_TO_NODE]
        weights = self.data[DataCol.WEIGHTS_NODE_TO_NODE]
        nodes = list(self.data[DataCol.NODE_ID].values)
        x = self.vars[AllocationVariable]

        obj = quicksum(
            weights[i, j]
            * x[i, k]
            * x[j, m]
            * (costs[i, k] + self._alpha * costs[k, m] + costs[m, j])
            for i in nodes
            for j in nodes
            for k in nodes
            for m in nodes
        )
        return obj, GRB.MINIMIZE


# ---------------------------------------------------------------------------
# Step 5: Solve and inspect
# ---------------------------------------------------------------------------

model = HubLocationModel(n_hubs=3, alpha=0.5, data=dataset)
solution = ModelSolver(model).solve()

print(f"Objective: {solution.solution_metadata.objective_value:.2f}")
print(f"Time: {solution.solution_metadata.solving_time:.2f}s")
print(f"Optimal: {solution.solution_metadata.is_optimal}")

x = solution.solution_data[AllocationVariable]
open_hubs = [k for (i, k), val in x.items() if i == k and val > 0.5]
print(f"Open hubs: {open_hubs}")

for (i, k), val in x.items():
    if val > 0.5 and i != k:
        print(f"  Node {i} → Hub {k}")

What We Built — Summary

Component Lines What it does
AllocationVariable ~15 Binary \(x_{ik}\) over all node pairs
ExactHubsConstraint ~12 \(\sum x_{kk} = p\)
SingleAssignmentConstraint ~12 \(\sum_k x_{ik} = 1\)
HubLinkConstraint ~12 \(x_{ik} \leq x_{kk}\)
HubLocationModel ~30 Orchestrates build phases + objective
dataset pipeline ~8 Load + prepare CAB25 data
Total ~90 lines Complete working HLP solver

Variables, constraints, and the objective are independent — you can swap, add, or change them without touching the rest of the model. Dependencies are checked automatically.