diff --git a/notebooks/03/facility-location.ipynb b/notebooks/03/facility-location.ipynb index 8f2e3e4c..a2cc3b8d 100644 --- a/notebooks/03/facility-location.ipynb +++ b/notebooks/03/facility-location.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -1715,7 +1715,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.10" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/notebooks/04/dina_model.pdf b/notebooks/04/dina_model.pdf deleted file mode 100644 index 9f38e8fa..00000000 Binary files a/notebooks/04/dina_model.pdf and /dev/null differ diff --git a/notebooks/04/dina_model_basic.pdf b/notebooks/04/dina_model_basic.pdf deleted file mode 100644 index eed1bad5..00000000 Binary files a/notebooks/04/dina_model_basic.pdf and /dev/null differ diff --git a/notebooks/04/dina_times.pdf b/notebooks/04/dina_times.pdf deleted file mode 100644 index 8966115e..00000000 Binary files a/notebooks/04/dina_times.pdf and /dev/null differ diff --git a/notebooks/04/dinner-seat-allocation.ipynb b/notebooks/04/dinner-seat-allocation.ipynb index 94959a8d..dd01365f 100644 --- a/notebooks/04/dinner-seat-allocation.ipynb +++ b/notebooks/04/dinner-seat-allocation.ipynb @@ -76,20 +76,20 @@ "source": [ "## Problem description\n", "\n", - "Assume that you are organizing a wedding dinner at which your objective is to have guests from different families mingle with each other. One way to do this is to seat people at tables so that no more people than a given threshold $k$ from the same family sit at the same table. How could we solve a problem like this? \n", + "Assume that we are organizing a wedding dinner and our goal is to have guests from different families mingle with each other as much as possible. One way to do this is to seat people at tables so that no more people than a given threshold $k_{\\max}$ from the same family sit at the same table. How could we solve a problem like this? \n", "\n", - "First, we need the problem data -- for each family $f$ we need to know the number of its members $m_f$, and for each table $t$ we need to know its capacity $c_t$. Using these data and the tools we have learned so far, we can formulate this problem as a LO problem.\n", + "First, we need the problem data. For each family $f \\in F$ we need to know the number of its members $m_f$, and for each table $t \\in T$ we need to know its capacity $c_t$. Using these data and the tools we have learned so far, we can formulate this problem as a LO problem.\n", "\n", - "If we do not care about the specific people, but only about the number of people in a given family, then we can use variable $x_{ft}$ to determine the number of people in family $f$ who will sit at table $t$. In the problem formulation, we were not given any objective function, since our goal is to find a feasible seating arrangement. For this reason, we can set the objective function to a constant value, say $0$, and, in this way, do not differentiate between the various feasible solutions. \n", + "If we are not concerned about specific individuals, but only about the number of people in a given family, then we can use variable $x_{ft}$ to determine the number of people in family $f$ who will sit at table $t$. In the problem formulation, we were not given any objective function, since our goal is to find a feasible seating arrangement. For this reason, we can set the objective function to a constant value, say $0$, and, in this way, do not differentiate between the various feasible solutions. \n", "\n", - "The mathematical formulation of this seating problem is:\n", + "The mathematical formulation of this seating allocation problem is:\n", "\n", "$$\n", "\\begin{align*}\n", " \\min \\quad & 0\\\\\n", - " \\text{s.t.} \\quad & \\sum\\limits_{f} x_{ft} \\leq c_t & \\forall \\, t \\in T \\\\\n", - " & \\sum\\limits_{t} x_{ft} = m_f & \\forall \\, f \\in F \\\\\n", - " & 0 \\leq x_{ft} \\leq k.\n", + " \\text{s.t.} \\quad & \\sum\\limits_{f} x_{ft} \\leq c_t && \\forall \\, t \\in T \\\\\n", + " & \\sum\\limits_{t} x_{ft} = m_f && \\forall \\, f \\in F \\\\\n", + " & 0 \\leq x_{ft} \\leq k_{\\max}.\n", "\\end{align*}\n", "$$\n", "\n", @@ -109,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 2, "metadata": { "id": "g3Jo-pwb4ltx" }, @@ -122,8 +122,8 @@ "import pandas as pd\n", "\n", "\n", - "def table_seat(members, capacity, k, domain=pyo.NonNegativeReals):\n", - " m = pyo.ConcreteModel(\"Dina's seat plan\")\n", + "def seat_allocation(members, capacity, kmax, domain=pyo.NonNegativeReals):\n", + " m = pyo.ConcreteModel(\"Seating arrangement\")\n", "\n", " m.F = pyo.Set(initialize=range(len(members)))\n", " m.T = pyo.Set(initialize=range(len(capacity)))\n", @@ -131,7 +131,7 @@ " m.M = pyo.Param(m.F, initialize=members)\n", " m.C = pyo.Param(m.T, initialize=capacity)\n", "\n", - " m.x = pyo.Var(m.F, m.T, bounds=(0, k), domain=domain)\n", + " m.x = pyo.Var(m.F, m.T, bounds=(0, kmax), domain=domain)\n", " m.dummy = pyo.Var(bounds=(0, 1), initialize=0)\n", "\n", " @m.Objective(sense=pyo.minimize)\n", @@ -146,7 +146,9 @@ " def seat(m, f):\n", " return pyo.quicksum(m.x[f, t] for t in m.T) == m.M[f]\n", "\n", - " return m\n", + " results = SOLVER.solve(m)\n", + "\n", + " return results, m\n", "\n", "\n", "def get_solution(model):\n", @@ -159,15 +161,16 @@ " return df.round(5)\n", "\n", "\n", - "def report(model, results, type=int):\n", - " print(f\"Solver status: {results.solver.status}\")\n", - " print(f\"Termination condition: {results.solver.termination_condition}\")\n", + "def report(model, results, type=int, verbose=False):\n", + " if verbose:\n", + " print(f\"Solver status: {results.solver.status}\")\n", + " print(f\"Termination condition: {results.solver.termination_condition}\")\n", " if results.solver.termination_condition == \"optimal\":\n", " soln = get_solution(model).astype(type)\n", " display(soln)\n", - " print(f\"objective: {pyo.value(seatplan.goal)}\")\n", - " print(f\"places at table: {list(soln.sum(axis=0))}\")\n", - " print(f\"members seated: {list(soln.sum(axis=1))}\")" + " print(f\"objective: {pyo.value(model.goal)}\")\n", + " print(f\"places at table: {list(soln.sum(axis=0).round(1))}\")\n", + " print(f\"members seated: {list(soln.sum(axis=1).round(1))}\")" ] }, { @@ -179,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 3, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -200,16 +203,6 @@ "outputId": "5d75d95c-4176-4d59-f4fa-4d1d27adbdf4" }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 6.78 ms, sys: 8.16 ms, total: 14.9 ms\n", - "Wall time: 100 ms\n", - "Solver status: ok\n", - "Termination condition: optimal\n" - ] - }, { "data": { "text/html": [ @@ -249,48 +242,48 @@ " \n", " \n", " 0\n", - " 2.0\n", - " 3.0\n", - " 1.0\n", " 0.0\n", + " 1.0\n", + " 2.0\n", " 0.0\n", + " 3.0\n", " \n", " \n", " 1\n", - " 2.0\n", - " 0.0\n", " 3.0\n", " 0.0\n", + " 2.0\n", + " 0.0\n", " 3.0\n", " \n", " \n", " 2\n", - " 2.0\n", " 0.0\n", " 0.0\n", " 0.0\n", + " 2.0\n", " 0.0\n", " \n", " \n", " 3\n", - " 0.0\n", " 2.0\n", " 3.0\n", - " 1.0\n", " 3.0\n", + " 0.0\n", + " 1.0\n", " \n", " \n", " 4\n", - " 1.0\n", - " 3.0\n", " 3.0\n", " 3.0\n", " 3.0\n", + " 2.0\n", + " 2.0\n", " \n", " \n", " 5\n", - " 1.0\n", " 0.0\n", + " 1.0\n", " 0.0\n", " 0.0\n", " 0.0\n", @@ -302,12 +295,12 @@ "text/plain": [ " table 0 table 1 table 2 table 3 table 4\n", "family \n", - "0 2.0 3.0 1.0 0.0 0.0\n", - "1 2.0 0.0 3.0 0.0 3.0\n", - "2 2.0 0.0 0.0 0.0 0.0\n", - "3 0.0 2.0 3.0 1.0 3.0\n", - "4 1.0 3.0 3.0 3.0 3.0\n", - "5 1.0 0.0 0.0 0.0 0.0" + "0 0.0 1.0 2.0 0.0 3.0\n", + "1 3.0 0.0 2.0 0.0 3.0\n", + "2 0.0 0.0 0.0 2.0 0.0\n", + "3 2.0 3.0 3.0 0.0 1.0\n", + "4 3.0 3.0 3.0 2.0 2.0\n", + "5 0.0 1.0 0.0 0.0 0.0" ] }, "metadata": {}, @@ -324,12 +317,9 @@ } ], "source": [ - "seatplan = table_seat(\n", - " members=[6, 8, 2, 9, 13, 1], \n", - " capacity=[8, 8, 10, 4, 9], \n", - " k=3\n", + "results, seatplan = seat_allocation(\n", + " members=[6, 8, 2, 9, 13, 1], capacity=[8, 8, 10, 4, 9], kmax=3\n", ")\n", - "%time results = SOLVER.solve(seatplan)\n", "report(seatplan, results, type=float)" ] }, @@ -346,23 +336,31 @@ "source": [ "## Minimize the maximum group size\n", "\n", - "Our objective was that we make members of different families mingle as much as possible. Is $k = 3$ the lowest possible number for which a feasible table allocation exists or can we make the tables even more diverse by bringing this number down?\n", + "Our objective was that we make members of different families mingle as much as possible. Is $k_{\\max} = 3$ the lowest possible number for which a feasible table allocation exists or can we make the tables even more diverse by bringing this number down?\n", + "\n", + "In order to find out, we can make $k=k_{\\max}$ a decision variable and change the objective function as to minimize $k$, obtaining the following problem:\n", "\n", - "In order to find out, we change the objective function and try to minimize $k$, obtaining the following problem:" + "$$\n", + "\\begin{align*}\n", + " \\min \\quad & k \\\\\n", + " \\text{s.t.} \\quad & \\sum_{f} x_{ft} \\leq c_t && \\forall \\, t \\in T \\nonumber \\\\\n", + " & \\sum_{t} x_{ft} = m_f && \\forall \\, f \\in F \\nonumber \\\\\n", + " & 0 \\leq x_{ft} \\leq k && \\forall f \\in F, t \\in T \\nonumber \\\\\n", + " & k \\geq 0. \\nonumber\n", + "\\end{align*}\n", + "$$" ] }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 4, "metadata": { "id": "GJxtvN9gS2xW" }, "outputs": [], "source": [ - "def table_seat_minimize_max_group_at_table(\n", - " members, capacity, domain=pyo.NonNegativeReals\n", - "):\n", - " m = pyo.ConcreteModel(\"Dina's seat plan\")\n", + "def seat_allocation_minimize_group_size(members, capacity, domain=pyo.NonNegativeReals):\n", + " m = pyo.ConcreteModel(\"Seating arrangement minimizing the maximum group size\")\n", "\n", " m.F = pyo.Set(initialize=range(len(members)))\n", " m.T = pyo.Set(initialize=range(len(capacity)))\n", @@ -389,7 +387,9 @@ " def bound(m, f, t):\n", " return m.x[f, t] <= m.k\n", "\n", - " return m" + " results = SOLVER.solve(m)\n", + "\n", + " return results, m" ] }, { @@ -401,7 +401,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 5, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -422,16 +422,6 @@ "outputId": "9056e7b0-1a7e-4017-e31b-ecbffa4a312b" }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 8.45 ms, sys: 8.99 ms, total: 17.4 ms\n", - "Wall time: 42.7 ms\n", - "Solver status: ok\n", - "Termination condition: optimal\n" - ] - }, { "data": { "text/html": [ @@ -472,34 +462,34 @@ " \n", " 0\n", " 2.6\n", - " 2.2\n", " 0.0\n", + " 2.2\n", " 0.0\n", " 1.2\n", " \n", " \n", " 1\n", - " 1.0\n", - " 2.6\n", + " 0.2\n", " 1.8\n", - " 0.0\n", " 2.6\n", + " 1.4\n", + " 2.0\n", " \n", " \n", " 2\n", " 0.0\n", " 0.0\n", - " 2.0\n", " 0.0\n", " 0.0\n", + " 2.0\n", " \n", " \n", " 3\n", - " 1.8\n", - " 0.6\n", " 2.6\n", - " 1.4\n", " 2.6\n", + " 2.6\n", + " 0.0\n", + " 1.2\n", " \n", " \n", " 4\n", @@ -512,10 +502,10 @@ " \n", " 5\n", " 0.0\n", - " 0.0\n", " 1.0\n", " 0.0\n", " 0.0\n", + " 0.0\n", " \n", " \n", "\n", @@ -524,12 +514,12 @@ "text/plain": [ " table 0 table 1 table 2 table 3 table 4\n", "family \n", - "0 2.6 2.2 0.0 0.0 1.2\n", - "1 1.0 2.6 1.8 0.0 2.6\n", - "2 0.0 0.0 2.0 0.0 0.0\n", - "3 1.8 0.6 2.6 1.4 2.6\n", + "0 2.6 0.0 2.2 0.0 1.2\n", + "1 0.2 1.8 2.6 1.4 2.0\n", + "2 0.0 0.0 0.0 0.0 2.0\n", + "3 2.6 2.6 2.6 0.0 1.2\n", "4 2.6 2.6 2.6 2.6 2.6\n", - "5 0.0 0.0 1.0 0.0 0.0" + "5 0.0 1.0 0.0 0.0 0.0" ] }, "metadata": {}, @@ -541,17 +531,14 @@ "text": [ "objective: 2.6\n", "places at table: [8.0, 8.0, 10.0, 4.0, 9.0]\n", - "members seated: [6.000000000000001, 8.0, 2.0, 9.0, 13.0, 1.0]\n" + "members seated: [6.0, 8.0, 2.0, 9.0, 13.0, 1.0]\n" ] } ], "source": [ - "seatplan = table_seat_minimize_max_group_at_table(\n", - " members=[6, 8, 2, 9, 13, 1], \n", - " capacity=[8, 8, 10, 4, 9], \n", - " domain=pyo.NonNegativeReals \n", + "results, seatplan = seat_allocation_minimize_group_size(\n", + " members=[6, 8, 2, 9, 13, 1], capacity=[8, 8, 10, 4, 9], domain=pyo.NonNegativeReals\n", ")\n", - "%time results = SOLVER.solve(seatplan)\n", "report(seatplan, results, type=float)" ] }, @@ -561,12 +548,12 @@ "source": [ "Unfortunately, this solution is no longer integer. Mathematically, this is because the \"structure\" that previously ensured integer solutions at no extra cost has been lost as a result of making $k$ a decision variable. To find the solution to this problem we need to impose that the variables are integers.\n", "\n", - "Using an MILO solver such as `cbc` or `highs`, we recover the original optimal value $k = 3$." + "Using an MILO solver such as `cbc` or `highs` and specifying the desired domain for the variables using `domain=pyo.NonNegativeIntegers`, we can recover the original optimal value $k = 3$." ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -587,16 +574,6 @@ "outputId": "7e40163d-ddd3-4a07-9009-fd40d68b52c0" }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 8.39 ms, sys: 8.11 ms, total: 16.5 ms\n", - "Wall time: 57.9 ms\n", - "Solver status: ok\n", - "Termination condition: optimal\n" - ] - }, { "data": { "text/html": [ @@ -636,34 +613,34 @@ " \n", " \n", " 0\n", - " 2\n", - " 2\n", - " 0\n", + " 3\n", " 0\n", " 2\n", + " 0\n", + " 1\n", " \n", " \n", " 1\n", - " 1\n", - " 3\n", - " 2\n", " 0\n", - " 2\n", + " 3\n", + " 3\n", + " 1\n", + " 1\n", " \n", " \n", " 2\n", " 0\n", " 0\n", - " 2\n", " 0\n", " 0\n", + " 2\n", " \n", " \n", " 3\n", " 2\n", + " 3\n", " 2\n", - " 2\n", - " 1\n", + " 0\n", " 2\n", " \n", " \n", @@ -677,10 +654,10 @@ " \n", " 5\n", " 0\n", - " 0\n", " 1\n", " 0\n", " 0\n", + " 0\n", " \n", " \n", "\n", @@ -689,12 +666,12 @@ "text/plain": [ " table 0 table 1 table 2 table 3 table 4\n", "family \n", - "0 2 2 0 0 2\n", - "1 1 3 2 0 2\n", - "2 0 0 2 0 0\n", - "3 2 2 2 1 2\n", + "0 3 0 2 0 1\n", + "1 0 3 3 1 1\n", + "2 0 0 0 0 2\n", + "3 2 3 2 0 2\n", "4 3 1 3 3 3\n", - "5 0 0 1 0 0" + "5 0 1 0 0 0" ] }, "metadata": {}, @@ -711,12 +688,11 @@ } ], "source": [ - "seatplan = table_seat_minimize_max_group_at_table(\n", - " members=[6, 8, 2, 9, 13, 1], \n", - " capacity=[8, 8, 10, 4, 9], \n", - " domain=pyo.NonNegativeIntegers\n", + "results, seatplan = seat_allocation_minimize_group_size(\n", + " members=[6, 8, 2, 9, 13, 1],\n", + " capacity=[8, 8, 10, 4, 9],\n", + " domain=pyo.NonNegativeIntegers,\n", ")\n", - "%time results = SOLVER.solve(seatplan)\n", "report(seatplan, results, type=int)" ] }, @@ -727,23 +703,41 @@ "## Minimize number of tables" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us fix again to $k_{\\max} = 3$ the maximum number of family members that can be seated at the same table and focus on minimizing the number of tables used. Let us introduce a binary variable $y_t$ that is equal to 1 if table $t$ is used and 0 otherwise. We can thus consider a new optimization problem in which we minimize the sum of $y_t$ over $t=1,\\ldots,T$. The mathematical formulation of this new problem is:\n", + "\n", + "$$\n", + "\\begin{align*}\n", + " \\min \\quad & \\sum_{t} y_t\\\\\n", + " \\text{s.t.} \\quad & \\sum\\limits_{f} x_{ft} \\leq c_t \\cdot y_t && \\forall \\, t \\in T \\\\\n", + " & \\sum\\limits_{t} x_{ft} = m_f && \\forall \\, f \\in F \\\\\n", + " & 0 \\leq x_{ft} \\leq k_{\\max}.\n", + "\\end{align*}\n", + "$$\n", + "\n", + "We can implement it in Pyomo as follows." + ] + }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 7, "metadata": { "id": "9mHD-wjuuLg2" }, "outputs": [], "source": [ - "def table_seat_minimize_number_of_tables(\n", - " members, capacity, k, domain=pyo.NonNegativeReals\n", + "def seat_allocation_minimize_tables(\n", + " members, capacity, kmax, domain=pyo.NonNegativeReals\n", "):\n", - " m = pyo.ConcreteModel(\"Dina's seat plan\")\n", + " m = pyo.ConcreteModel(\"Seating arrangement minimizing the number of tables\")\n", " m.F = pyo.Set(initialize=range(len(members)))\n", " m.T = pyo.Set(initialize=range(len(capacity)))\n", " m.M = pyo.Param(m.F, initialize=members)\n", " m.C = pyo.Param(m.T, initialize=capacity)\n", - " m.x = pyo.Var(m.F, m.T, bounds=(0, k), domain=domain)\n", + " m.x = pyo.Var(m.F, m.T, bounds=(0, kmax), domain=domain)\n", " m.y = pyo.Var(m.T, domain=pyo.Binary)\n", "\n", " @m.Objective(sense=pyo.minimize)\n", @@ -758,24 +752,16 @@ " def seat(m, f):\n", " return pyo.quicksum(m.x[f, t] for t in m.T) == m.M[f]\n", "\n", - " return m" + " results = SOLVER.solve(m)\n", + "\n", + " return results, m" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 8, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 8.46 ms, sys: 9.26 ms, total: 17.7 ms\n", - "Wall time: 46.3 ms\n", - "Solver status: ok\n", - "Termination condition: optimal\n" - ] - }, { "data": { "text/html": [ @@ -815,48 +801,48 @@ " \n", " \n", " 0\n", - " 3\n", - " 0\n", - " 3\n", " 0\n", + " 2\n", + " 1\n", " 0\n", + " 3\n", " \n", " \n", " 1\n", - " 3\n", - " 2\n", - " 0\n", " 0\n", + " 1\n", + " 3\n", + " 1\n", " 3\n", " \n", " \n", " 2\n", + " 2\n", + " 0\n", " 0\n", " 0\n", - " 1\n", - " 1\n", " 0\n", " \n", " \n", " 3\n", - " 0\n", " 3\n", " 3\n", - " 0\n", " 3\n", + " 0\n", + " 0\n", " \n", " \n", " 4\n", - " 1\n", " 3\n", + " 1\n", " 3\n", " 3\n", " 3\n", " \n", " \n", " 5\n", - " 1\n", " 0\n", + " 1\n", " 0\n", " 0\n", " 0\n", @@ -868,12 +854,12 @@ "text/plain": [ " table 0 table 1 table 2 table 3 table 4\n", "family \n", - "0 3 0 3 0 0\n", - "1 3 2 0 0 3\n", - "2 0 0 1 1 0\n", - "3 0 3 3 0 3\n", - "4 1 3 3 3 3\n", - "5 1 0 0 0 0" + "0 0 2 1 0 3\n", + "1 0 1 3 1 3\n", + "2 2 0 0 0 0\n", + "3 3 3 3 0 0\n", + "4 3 1 3 3 3\n", + "5 0 1 0 0 0" ] }, "metadata": {}, @@ -890,13 +876,12 @@ } ], "source": [ - "seatplan = table_seat_minimize_number_of_tables(\n", - " members=[6, 8, 2, 9, 13, 1], \n", - " capacity=[8, 8, 10, 4, 9], \n", - " k=3, \n", - " domain=pyo.NonNegativeIntegers\n", + "results, seatplan = seat_allocation_minimize_tables(\n", + " members=[6, 8, 2, 9, 13, 1],\n", + " capacity=[8, 8, 10, 4, 9],\n", + " kmax=3,\n", + " domain=pyo.NonNegativeIntegers,\n", ")\n", - "%time results = SOLVER.solve(seatplan)\n", "report(seatplan, results, type=int)" ] }, @@ -913,39 +898,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "However, using an MILO solver is not necessarily the best approach for problems like this. Many real-life situations (assigning people to work/course groups) require solving really large problems. There are existing algorithms that can leverage the special **network structure** of the problem at hand and scale better than LO solvers. To see this we can visualize the seating problem using a graph where:\n", + "However, using an MILO solver is not necessarily the best approach for problems like this. Many real-life situations (e.g., assigning people to groups/teams) require solving really large problems. There are existing algorithms that can leverage the special **network structure** of the problem at hand and scale better than LO solvers. \n", "\n", - "* the nodes on the left-hand side stand for the families and the numbers next to them provide the family size\n", - "* the nodes on the left-hand side stand for the tables and the numbers next to them provide the table size\n", + "To illustrate this, we first visualize the seating problem using a graph where:\n", + "* the nodes on the left-hand side stand for the families and the numbers next to them provide the family size;\n", + "* the nodes on the left-hand side stand for the tables and the numbers next to them provide the table size;\n", "* each left-to-right arrow stand comes with a number denoting the capacity of arc $(f, t)$ -- how many people of family $f$ can be assigned to table $t$.\n", "\n", - "![](dina_model_basic.png)" + "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If we see each family as a place of supply (people) and tables as places of demand (people), then we can see our original problem as literally sending people from families $f$ to tables $t$ so that everyone is assigned to some table, the tables' capacities are respected, and no table gets more than $k = 3$ members of the same family.\n", + "If we think of each family as a \"supply of individuals\" and each table as a \"demand of individuals\", then we can rephrase our original task as the problem of sending people from families $f$ to tables $t$ so that everyone is assigned to some table, the tables' capacities are respected, and no table gets more than $k_{\\max} = 3$ members of the same family.\n", + "\n", + "$$\n", + "\\begin{align*}\n", + " \\min \\quad & \\sum_{t, f} x_{ft}\\\\\n", + " \\text{s.t.} \\quad & \\sum\\limits_{f} x_{ft} \\leq c_t && \\forall \\, t \\in T \\\\\n", + " & \\sum\\limits_{t} x_{ft} = m_f && \\forall \\, f \\in F \\\\\n", + " & 0 \\leq x_{ft} \\leq k_{\\max}.\n", + "\\end{align*}\n", + "$$\n", + "\n", "\n", "A Pyomo version of this model is given in the next cell. After that we will show how to reformulate the calculation using network algorithms." ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 9, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 7.69 ms, sys: 8.7 ms, total: 16.4 ms\n", - "Wall time: 47.7 ms\n", - "Solver status: ok\n", - "Termination condition: optimal\n" - ] - }, { "data": { "text/html": [ @@ -985,18 +971,18 @@ " \n", " \n", " 0\n", - " 1\n", - " 2\n", - " 1\n", " 0\n", - " 2\n", + " 3\n", + " 0\n", + " 0\n", + " 3\n", " \n", " \n", " 1\n", " 1\n", + " 1\n", " 3\n", " 0\n", - " 1\n", " 3\n", " \n", " \n", @@ -1010,24 +996,24 @@ " \n", " 3\n", " 3\n", - " 0\n", " 3\n", + " 2\n", + " 1\n", " 0\n", - " 3\n", " \n", " \n", " 4\n", " 3\n", + " 1\n", " 3\n", " 3\n", " 3\n", - " 1\n", " \n", " \n", " 5\n", + " 1\n", " 0\n", " 0\n", - " 1\n", " 0\n", " 0\n", " \n", @@ -1038,12 +1024,12 @@ "text/plain": [ " table 0 table 1 table 2 table 3 table 4\n", "family \n", - "0 1 2 1 0 2\n", - "1 1 3 0 1 3\n", + "0 0 3 0 0 3\n", + "1 1 1 3 0 3\n", "2 0 0 2 0 0\n", - "3 3 0 3 0 3\n", - "4 3 3 3 3 1\n", - "5 0 0 1 0 0" + "3 3 3 2 1 0\n", + "4 3 1 3 3 3\n", + "5 1 0 0 0 0" ] }, "metadata": {}, @@ -1060,35 +1046,40 @@ } ], "source": [ - "def table_seat_maximize_members_flow_to_tables(members, capacity, k, domain=pyo.NonNegativeReals):\n", - " m = pyo.ConcreteModel(\"Dina's seat plan\")\n", + "def seating_allocation_maximize_flow_to_tables(\n", + " members, capacity, kmax, domain=pyo.NonNegativeReals\n", + "):\n", + " m = pyo.ConcreteModel(\"Seating arrangement as network problem\")\n", " m.F = pyo.Set(initialize=range(len(members)))\n", " m.T = pyo.Set(initialize=range(len(capacity)))\n", " m.M = pyo.Param(m.F, initialize=members)\n", - " m.C = pyo.Param( m.T, initialize=capacity)\n", - " m.x = pyo.Var(m.F, m.T, bounds=(0, k), domain=domain)\n", - " \n", + " m.C = pyo.Param(m.T, initialize=capacity)\n", + " m.x = pyo.Var(m.F, m.T, bounds=(0, kmax), domain=domain)\n", + "\n", " @m.Objective(sense=pyo.maximize)\n", " def goal(m):\n", " return pyo.quicksum(m.x[f, t] for f in m.F for t in m.T)\n", - " \n", - " @m.Constraint(m.T) \n", + "\n", + " @m.Constraint(m.T)\n", " def capacity(m, t):\n", - " return pyo.quicksum(m.x[f,t] for f in m.F ) <= m.C[t]\n", - " \n", + " return pyo.quicksum(m.x[f, t] for f in m.F) <= m.C[t]\n", + "\n", " @m.Constraint(m.F)\n", " def seat(m, f):\n", - " return pyo.quicksum(m.x[f,t] for t in m.T ) == m.M[f]\n", + " return pyo.quicksum(m.x[f, t] for t in m.T) == m.M[f]\n", + "\n", + " results = SOLVER.solve(m)\n", "\n", - " return m\n", + " return results, m\n", "\n", - "seatplan = table_seat_maximize_members_flow_to_tables(\n", - " members=[6, 8, 2, 9, 13, 1], \n", - " capacity=[8, 8, 10, 4, 9], \n", - " k=3, \n", - " domain=pyo.NonNegativeIntegers\n", + "\n", + "results, seatplan = seating_allocation_maximize_flow_to_tables(\n", + " members=[6, 8, 2, 9, 13, 1],\n", + " capacity=[8, 8, 10, 4, 9],\n", + " kmax=3,\n", + " domain=pyo.NonNegativeIntegers,\n", ")\n", - "%time results = SOLVER.solve(seatplan)\n", + "\n", "report(seatplan, results, type=int)" ] }, @@ -1098,89 +1089,14 @@ "source": [ "By adding two more nodes to the graph above, we can formulate the problem as a slightly different flow problem where all the data is formulated as the arc capacity, see figure below. In a network like this, we can imagine a problem of sending resources from the _root node_ \"door\" to the _sink node_ \"seat\", subject to the restriction that for any node apart from $s$ and $t$, the sum of incoming and outgoing flows are equal (_balance constraint_). If there exists a flow in this new graph that respects the arc capacities and the sum of outgoing flows at $s$ is equal to $\\sum_{f \\in F} m_f = 39$, it means that there exists a family-to-table assignment that meets our requirements.\n", "\n", - "![](dina_model.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "aSJ7UOokVfnu" - }, - "outputs": [], - "source": [ - "def model_as_network(members, capacity, k):\n", - " # create lists of families and tables\n", - " families = [f\"f{i}\" for i in range(len(members))]\n", - " tables = [f\"t{j}\" for j in range(len(capacity))]\n", - "\n", - " # create digraphy object\n", - " G = nx.DiGraph()\n", - "\n", - " # add edges\n", - " G.add_edges_from([\"door\", f, {\"capacity\": n}] for f, n in zip(families, members))\n", - " G.add_edges_from([(f, t) for f in families for t in tables], capacity=k)\n", - " G.add_edges_from([t, \"seat\", {\"capacity\": n}] for t, n in zip(tables, capacity))\n", - "\n", - " return G" + "" ] }, { "cell_type": "code", "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "members = [6, 8, 2, 9, 13, 1]\n", - "capacity = [8, 8, 10, 4, 9]\n", - "\n", - "G = model_as_network(members, capacity=[8, 8, 10, 4, 9], k=3)\n", - "\n", - "labels = {(e[0], e[1]): e[2] for e in G.edges(data=\"capacity\")}" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "vRHT74fEV6KX" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 1.03 ms, sys: 53 µs, total: 1.09 ms\n", - "Wall time: 1.09 ms\n" - ] - } - ], - "source": [ - "%time flow_value, flow_dict = nx.maximum_flow(G, 'door', 'seat')" - ] - }, - { - "cell_type": "code", - "execution_count": 52, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 206 - }, - "executionInfo": { - "elapsed": 214, - "status": "ok", - "timestamp": 1643283766270, - "user": { - "displayName": "Joaquim Gromicho", - "photoUrl": "https://lh3.googleusercontent.com/a/default-user=s64", - "userId": "14375950305363805729" - }, - "user_tz": -60 - }, - "id": "JL9vgcULV-TG", - "outputId": "15de4861-1a77-485e-e91f-c03f8d55a917", - "tags": [] + "id": "aSJ7UOokVfnu" }, "outputs": [ { @@ -1271,15 +1187,38 @@ "t4 3 3 2 0 1 0" ] }, - "execution_count": 52, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "families = [f\"f{i:.0f}\" for i in range(len(members))]\n", - "tables = [f\"t{j:.0f}\" for j in range(len(capacity))]\n", + "def model_as_network(members, capacity, k):\n", + " # create lists of families and tables\n", + " families = [f\"f{i}\" for i in range(len(members))]\n", + " tables = [f\"t{j}\" for j in range(len(capacity))]\n", + "\n", + " # create digraphy object\n", + " G = nx.DiGraph()\n", "\n", + " # add edges\n", + " G.add_edges_from([\"door\", f, {\"capacity\": n}] for f, n in zip(families, members))\n", + " G.add_edges_from([(f, t) for f in families for t in tables], capacity=k)\n", + " G.add_edges_from([t, \"seat\", {\"capacity\": n}] for t, n in zip(tables, capacity))\n", + "\n", + " return G\n", + "\n", + "\n", + "members = [6, 8, 2, 9, 13, 1]\n", + "capacity = [8, 8, 10, 4, 9]\n", + "G = model_as_network(members, capacity, k=3)\n", + "\n", + "# we solve the maximum flow problem using the networkx function\n", + "flow_value, flow_dict = nx.maximum_flow(G, \"door\", \"seat\")\n", + "\n", + "# we parse and table the solution\n", + "families = [f\"f{i}\" for i in range(len(members))]\n", + "tables = [f\"t{j}\" for j in range(len(capacity))]\n", "pd.DataFrame(flow_dict).loc[tables, families].astype(\"int\")" ] }, @@ -1287,15 +1226,30 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Even for this very small example, we see that network algorithms generate a solution significantly faster." + "Even for this very small example, we see that network algorithms generate a solution significantly faster than using the MILO formulation above." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 401 µs, sys: 4 µs, total: 405 µs\n", + "Wall time: 408 µs\n", + "CPU times: user 3.66 ms, sys: 4.57 ms, total: 8.23 ms\n", + "Wall time: 15.1 ms\n" + ] + } + ], + "source": [ + "%time flow_value, flow_dict = nx.maximum_flow(G, 'door', 'seat')\n", + "\n", + "%time results, seatplan = seating_allocation_maximize_flow_to_tables(members=[6, 8, 2, 9, 13, 1], capacity=[8, 8, 10, 4, 9], kmax=3, domain=pyo.NonNegativeIntegers)" + ] } ], "metadata": { @@ -1314,7 +1268,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/notebooks/04/dina_times.xlsx b/notebooks/04/dinner-seat-allocation_runtimes.xlsx similarity index 100% rename from notebooks/04/dina_times.xlsx rename to notebooks/04/dinner-seat-allocation_runtimes.xlsx diff --git a/notebooks/04/dina_model.png b/notebooks/04/seating_flow_model.png similarity index 100% rename from notebooks/04/dina_model.png rename to notebooks/04/seating_flow_model.png diff --git a/notebooks/04/dina_model_basic.png b/notebooks/04/seating_model_basic.png similarity index 100% rename from notebooks/04/dina_model_basic.png rename to notebooks/04/seating_model_basic.png diff --git a/notebooks/04/tableseat_1.py b/notebooks/04/tableseat_1.py deleted file mode 100644 index d562f1f8..00000000 --- a/notebooks/04/tableseat_1.py +++ /dev/null @@ -1,21 +0,0 @@ -def TableSeat( members, capacity, k, domain=pyo.NonNegativeReals ): - m = pyo.ConcreteModel("Dina's seat plan") - m.F = pyo.Set( initialize=range( len(members) ) ) - m.T = pyo.Set( initialize=range( len(capacity) ) ) - m.M = pyo.Param( m.F, initialize=members ) - m.C = pyo.Param( m.T, initialize=capacity ) - m.x = pyo.Var( m.F, m.T, bounds=(0,k), domain=domain ) - - @m.Objective( sense=pyo.maximize ) - def goal(m): - return 0 - - @m.Constraint( m.T ) - def capacity( m, t ): - return pyo.quicksum( m.x[f,t] for f in m.F ) <= m.C[t] - - @m.Constraint( m.F ) - def seat( m, f ): - return pyo.quicksum( m.x[f,t] for t in m.T ) == m.M[f] - - return m