OpenJij/OpenJij

View on GitHub
docs/tutorial/en/optimization/integer_jobs.ipynb

Summary

Maintainability
Test Coverage
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Job Sequencing Problem with Integer Lengths\n",
    "Here we show how to solve the job sequencing problems with integer lengths using OpenJij, [JijModeling](https://www.ref.documentation.jijzept.com/jijmodeling/), and [JijModeling Transpiler](https://www.ref.documentation.jijzept.com/jijmodeling-transpiler/). This problem is also mentioned in 6.3. Job Sequencing with Integer Lengths in [Lucas, 2014, \"Ising formulations of many NP problems\"](https://doi.org/10.3389/fphy.2014.00005)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Overview of the Job Sequencing Problem with Integer Lengths\n",
    "\n",
    "We consider several computers and tasks with integer lengths (i.e., task 1 takes one hour to execute on a computer, task 2 takes three hours, and so on).\n",
    "When allocating these tasks to multiple computers to execute, the question is what combinations can be used to distribute the execution time of the computers without creating bias.\n",
    "We can obtain a leveled solution by minimizing the largest value.\n",
    "\n",
    "### Example\n",
    "\n",
    "As an example of this problem, consider the following situation.\n",
    "\n",
    "> Here are 10 tasks and 3 computers. \n",
    "> The length of each of the 10 tasks is 1, 2, ..., 10.\n",
    "> Our goal is to assign these tasks to the computers and minimize the maximum amount of time the tasks take.\n",
    "> In this case, one of the optimal solution is $\\{1, 2, 7, 8\\}, \\{3, 4, 5, 6\\}$ and $\\{9, 10\\}$, whose maximum execution time of computers is 19.\n",
    "\n",
    "![](../../../assets/integer_jobs_01_en.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Mathematical Model\n",
    "Next, we introduce $N$ tasks $\\{0, 1, ..., N-1\\}$ and list of the execution time $\\boldsymbol{L} = \\{L_0, L_1, ..., L_{N-1}\\}$. \n",
    "Given $M$ computers, the total execution time of the $j$ th computer to perform its assigned tasks is $A_j = \\sum_{i \\in V_j} L_i$ where $V_j$ is a set of assigned tasks to the $j$ th computer. Note that $A_j = \\sum_i L_i x_{ij}$.\n",
    "Finally, let us denote $x_{i, j}$ to be a binary variable which is 1 if the $i$ th task is assigned to the $j$ th computer, and 0 otherwise.\n",
    "\n",
    "#### Constraint\n",
    "Each task must be performed on one computer; for example, task 3 is not allowed to be executed on both computers 1 and 2. Also, it is not allowed that there is no computer that handles task 3.\n",
    "\n",
    "$$\n",
    "\\nonumber\n",
    "\\sum_{j=0}^{M-1} x_{i, j} = 1 \\quad (\\forall i \\in \\{ 0, 1, \\dots, N-1 \\})\n",
    "$$(1)\n",
    "\n",
    "#### Objective Function\n",
    "We consider the execution time of the $0$ th computer as the reference and minimize the difference between that and others.\n",
    "This reduces the execution time variability and the tasks are distributed equally.\n",
    "\n",
    "$$\n",
    "\\min \\quad \\sum_{j=1}^{M-1} (A_0 - A_j)^2\n",
    "$$(2)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Modeling by JijModeling\n",
    "\n",
    "Next, we show an implementation using JijModeling.\n",
    "We first define variables for the mathematical model described above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import jijmodeling as jm\n",
    "\n",
    "\n",
    "# defin variables\n",
    "L = jm.Placeholder('L', dim=1)\n",
    "N = L.shape[0].set_latex('N')\n",
    "M = jm.Placeholder('M')\n",
    "x = jm.Binary('x', shape=(N, M))\n",
    "i = jm.Element('i', (0, N))\n",
    "j = jm.Element('j', (0, M))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`L` is a one-dimensional array representing the execution time of each task.\n",
    "`N` denotes the number of tasks, and `M` is the number of computers.\n",
    "We define a two-dimensional binary variables `x`, and we set the subscripts `i` and `j` used in the mathematical model.\n",
    "\n",
    "### Constraint\n",
    "\n",
    "We implement the constraint in equation (1) as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# set problem\n",
    "problem = jm.Problem('Integer Jobs')\n",
    "# set constraint: job must be executed using a certain node\n",
    "problem += jm.Constraint('onehot', x[i, :]==1, forall=i)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`x[v, :]` implements `Sum(n, x[v, n])` in a concise way."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Objective Function\n",
    "\n",
    "Let us implement the objective function of equation (2).\n",
    "`Sum((j, j!=0), ...)` denotes taking the sum of all cases where j is not 0."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/latex": [
       "$$\\begin{alignat*}{4}\\text{Problem} & \\text{: Integer Jobs} \\\\\\min & \\quad \\sum_{ j = 0,\\ j \\neq 0 }^{ M - 1 } \\left( \\sum_{ i = 0 }^{ N - 1 } L_{i} \\cdot x_{i,0} - \\sum_{ i = 0 }^{ N - 1 } L_{i} \\cdot x_{i,j} \\right) ^ { 2 } \\\\\\text{s.t.} & \\\\& \\text{onehot} :\\\\ &\\quad \\quad \\sum_{ \\bar{i}_{1} = 0 }^{ M - 1 } x_{i,\\bar{i}_{1}} = 1,\\ \\forall i \\in \\left\\{ 0 ,\\ldots , N - 1 \\right\\} \\\\[8pt]& x_{i_{0},i_{1}} \\in \\{0, 1\\}\\end{alignat*}$$"
      ],
      "text/plain": [
       "<jijmodeling.problem.problem.Problem at 0x11214fb50>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set objective function: minimize difference between node 0 and others\n",
    "A_0 = jm.Sum(i, L[i]*x[i, 0])\n",
    "A_j = jm.Sum(i, L[i]*x[i, j])\n",
    "problem += jm.Sum((j, j!=0), (A_0 - A_j) ** 2)\n",
    "problem"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Instance\n",
    "\n",
    "Here we set the execution time of each job and the number of computers. We use the same values from the example mentioned."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# set a list of jobs\n",
    "inst_L = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n",
    "# set the number of Nodes\n",
    "inst_M = 3\n",
    "instance_data = {'L': inst_L, 'M': inst_M}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Undefined Multiplier\n",
    "This problem has one constraint, and we need to set the weight of that constraint.\n",
    "We will set it to match the name we gave in the `Constraint` part earlier using a dictionary type."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# set multipliers\n",
    "lam1 = 1.0\n",
    "multipliers = {'onehot': lam1}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Conversion to PyQUBO by JijModeling Transpiler\n",
    "\n",
    "JijModeling has executed all the implementations so far.\n",
    "By converting this to [PyQUBO](https://pyqubo.readthedocs.io/en/latest/), it is possible to perform combinatorial optimization calculations using OpenJij and other solvers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from jijmodeling.transpiler.pyqubo import to_pyqubo\n",
    "\n",
    "# convert to pyqubo\n",
    "pyq_model, pyq_chache = to_pyqubo(problem, instance_data, {})\n",
    "qubo, bias = pyq_model.compile().to_qubo(feed_dict=multipliers)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The PyQUBO model is created by `to_pyqubo` with the `problem` created by JijModeling and the `instance_data` we set to a value as arguments.\n",
    "Next, we compile it into a QUBO model that can be computed by OpenJij or other solver.\n",
    "\n",
    "### Optimization by OpenJij\n",
    "\n",
    "This time, we will use OpenJij's simulated annealing to solve the optimization problem.\n",
    "We set the `SASampler` and input the QUBO into that sampler to get the result of the calculation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openjij as oj\n",
    "\n",
    "# set sampler\n",
    "sampler = oj.SASampler(num_reads=100)\n",
    "# solve problem\n",
    "response = sampler.sample_qubo(qubo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Decoding and Displaying the Solution\n",
    "\n",
    "Decode the returned results to facilitate analysis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "# decode solution\n",
    "result = pyq_chache.decode(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "From the results thus obtained, we can see how the task execution is distributed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGwCAYAAACHJU4LAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA4hUlEQVR4nO3dd3RUdeL//9ckpBBSJpQ0CCTSSwhFQFBEyo8irri6ggUBF8u6IJaPq/JbEXFVLLs2FtGPCrj2Xj6osICEbiEJoYiUiBRJgUASkpA69/uHS9YxCWTCTO7NzPNxzpzD3Llz7+vmZjIv7n3PHZthGIYAAAAsyM/sAAAAAHWhqAAAAMuiqAAAAMuiqAAAAMuiqAAAAMuiqAAAAMuiqAAAAMtqZnaAc+FwOHTkyBGFhYXJZrOZHQcAANSDYRg6efKk4uLi5Od35mMmTbqoHDlyRPHx8WbHAAAADXDo0CG1a9fujPM06aISFhYm6ZcNDQ8PNzkNAACoj8LCQsXHx1e/j59Jky4qp0/3hIeHU1QAAGhi6jNsg8G0AADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAspr0tyeftmfPHoWGhnps+eXl5QoMDPTY8r0JPyvfwv4GzGO32xUTE2N2DI/ziqIy/bpp8vf398iyKyordPholqKiozy2Dm9RWVmpvLwjio1txc/KB1RUVOpgbrFaxsSxvwET2ENC9OXHH3l9WfGKonL70KnqFtvRI8v+el+aHv3yn7rkkksUFxfnkXV4i71792rFig80ZWorJSTYzY4DD0tPz9MzL59S+LQ/KSLhPLPjAD7l1M+HdWLxQuXn51NUmoL4lrHqHJ3okWUfOHZYktSqVSuv/2U4V0ePHpUkxcaGKCEhzOQ08LSffy6WJAXHtlULigrQ6E6YHaCRMJgWAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYFkUFAABYVjOzA3hacVmJFq5+Tek/7VD6wZ3KLynUs9c/pGsGXW52tHo7cuSIli1bpoMHD6qgoECBgYGKjY3V6NGj1bt3b7Pj+ZzU1GP6822ban3slVcvUlJSy0ZOBNRP4a4d2vfCs8rfni4ZhiKS+qjL7X9RWNfuZkcD6uT1RSWvKF9PL39ZbSNj1COuizbt22J2JJfl5eWptLRUF1xwgSIiIlReXq709HS98MILuv766zV06FCzI/qkiZMS1aOH3WlafHwLc8IAZ1H4w059d/P1Co6O0Xk3zZAMQ4c+eEtbbr1BA5e+pxYJ55kdEaiVqUVl/vz5+uijj/TDDz+oefPmGjJkiJ544gl17drVbeuIDm+tbY/8W1HhrbX14Pca+/fJblt2Y0lKSlJSUpLTtOHDh+uxxx7TqlWrKCom6dOnlUaOjDM7BlAvmS8+L7+gYA149R0F2iMlSbHjfqeNV43VvheeUfKTC0xOCNTO1DEqa9eu1YwZM/T1119r5cqVqqio0OjRo1VcXOy2dQQFBCoqvLXblmcVfn5+ioyM1KlTp8yO4tOKiytVWekwOwZwVie2blGrgYOrS4okBbWOUmS/ATq6IUWVJe77uwu4k6lHVJYvX+50f+nSpYqKilJqaqouvvhik1JZV1lZmSoqKnTq1CllZGRo586d6t+/v9mxfNYjf0tXSUmV/P1tSu7TUrNu76nuvzkVBFiFo7xcfkFBNab7BzeXUVGhosy9sif1afxgwFlYaoxKQUGBJKlly9oHI5aVlamsrKz6fmFhYaPksooPPvhA69evlyTZbDb17dtX11xzjcmpfE9AgJ+Gj4jVkCHRstsDtX//Sb35RqZuvXWDXn5lqLp2jTA7IlBDiw6JKtieIaOqSjZ/f0mSo6JcBTu2SZLKjuaYGQ+ok2WKisPh0J133qkLL7xQvXr1qnWe+fPna968eY2czDpGjhypfv36qaCgQKmpqXI4HKqqqjI7ls/p3bulevf+b5m++OIYjRgRq+uvW6sXFn6v554fbGI6oHbt/nCdfnj8Ie3821+VMOUmyeHQj4tfVNmxo5IkR2nZWZYAmMMy11GZMWOGduzYoXfeeafOeWbPnq2CgoLq26FDhxoxofliYmLUvXt3XXDBBZoxY4bKysq0cOFCGYZhdjSfFx8fqouHxSg1NU9VVewPWE/8Vdco8cZblb3ic22edJk2X3u5Th0+qIQp0yVJ/iEhJicEameJIyozZ87UsmXLtG7dOrVr167O+YKCghRUyzlWX9WvXz+9+eabysnJUUxMjNlxfF50VLAqKhw6dapSoaEBZscBauj057vUYfIfVfTjPjULDVVYp67au/BpSVJI+wRzwwF1MLWoGIah22+/XR9//LFSUlKUmJhoZpwmp7y8XJL45I9F/HykREFBfgoJsUT/B2oVEB6hyD7/HYR//NvNCoqK4ToqsCxTT/3MmDFDb7zxht566y2FhYUpOztb2dnZvPH+Rm2DhquqqvTNN98oICBAsbGxJqTyXSdO1DyXv2dPgdavy9bAQVHy87OZkApwXfa/v1Dh99vV/topsvlZZiQA4MTU//otWrRIknTJJZc4TV+yZImmTZvmtvW8uu4dFZ4qUnbBL4PGVu5Yp6z8XEnS9IsnKbx5mNvW5QlvvvmmSktL1blzZ9ntdhUWFurbb79Vdna2/vCHPyg4ONjsiD7lr/9/qoKC/dQ7qaUiWwZp/48n9cknBxQc7K8ZM7gUOazpRNp3+vGVF9TqggsVEGFXwfYMHVn2kVoNHqr210wxOx5QJ9NP/TSGRV+9rsPHs6rvf57xlT7P+EqSdNX5l1q+qJx//vnauHGj1q1bp6KiIgUHB6t9+/b6/e9/r+TkZLPj+Zxhl8RoxfLDeuutTBUXVyoyMlCXDI/VTTd1UXx8qNnxgFoFRUXL5u+nn15/VVUlxWoe104d/3SHOlw/TX7NOF0J6/KJ384tD31udoRzMmDAAA0YMMDsGPiPSZPO06RJnM9H0xLSrr36LXjV7BiAyzgpCQAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALIuiAgAALKuZ2QHc4dDxLLUICvHIsrPycyVJeXl5CgoK8sg6vEV+fr4kKSurREFBgeaGgcfl5pZKkkqzflYxrw2gUZ36+bDZERqNVxSVBetfk7+/v0eWXVFZIdlsSklJ8dg6vEVlZaUcjmb612t58vfPNzsOPKyiolLNDIcKl76oYl4bQKOzh4TIbrebHcPjvKKovPrWUoWGhnps+eXl5QoM5AhBffCz8i3sb8A8drtdMTExZsfwOK8oKl26dFF4eLjZMQAAgJsxmBYAAFgWRQUAAFgWRQUAAFiWW4rK6Y+lAgAAuJPLReWJJ57Qu+++W31/4sSJatWqldq2bauMjAy3hgMAAL7N5aLy4osvKj4+XpK0cuVKrVy5Ul9++aXGjRunv/zlL24PCAAAfJfLH0/Ozs6uLirLli3TxIkTNXr0aCUkJGjQoEFuDwgAAHyXy0dUIiMjdejQIUnS8uXLNWrUKEmSYRiqqqpybzoAAODTXD6icuWVV+q6665T586dlZeXp3HjxkmS0tPT1alTJ7cHBAAAvsvlovLMM88oMTFRBw8e1JNPPll96fqsrCz9+c9/dntAAADgu1wqKhUVFbr11ls1Z84cJSYmOj121113uTUYAACAS2NUAgIC9OGHH3oqCwAAgBOXB9NeccUV+uSTTzwQBQAAwJnLY1Q6d+6shx9+WBs3blT//v3VokULp8dnzZrltnAAAMC32QzDMFx5wm/HpjgtzGbTjz/+eM6h6quwsFAREREqKChQeHh4o60XAAA0nCvv3y4fUdm/f3+DgwEAALiiwV9KWF5ert27d6uystKdeQAAAKq5XFRKSko0ffp0hYSEqGfPnjp48KAk6fbbb9fjjz/u9oAAAMB3uVxUZs+erYyMDKWkpCg4OLh6+qhRo5y+VRkAAOBcuTxG5ZNPPtG7776rCy64QDabrXp6z549lZmZ6dZwAADAt7lcVI4ePaqoqKga04uLi52KS2Pas2dP9aX8m6ry8nIFBgaaHQNwCb+3gHez2+2KiYkxNYPLReX888/X559/rttvv12SqsvJK6+8osGDB7s3XT398eqr5e/X4HHBpquorNSREycVFR0nf39/s+MA9VJZWaHc3MOKjmojfz9+bwFvFBoerk+XLTO1rLhcVB577DGNGzdO33//vSorK/Xcc8/p+++/16ZNm7R27VpPZDyrP0VFq0tE072OyrdHj+nvx4s1fsBUtYtOMDsOUC+79qfr/VXPa0yvrmob1drsOADc7Gh+ob7Y+r3y8/ObVlG56KKLtHXrVj3++ONKSkrSv//9b/Xr10+bN29WUlKSJzKeVdsWIeoY1nSLysHiYklSG3us2rZJMDcMUE85x3+WJLWOCFNcq5YmpwHgrVwuKpLUsWNHvfzyy+7OAgAA4KRBRaWqqkoff/yxdu3aJUnq0aOHJkyYoGbNGrQ4AACAWrncLHbu3KnLL79c2dnZ6tq1qyTpiSeeUJs2bfR///d/6tWrl9tDAgAA3+TyR2Vuuukm9ezZU4cPH1ZaWprS0tJ06NAh9e7dW7fccosnMgIAAB/l8hGVrVu3asuWLYqMjKyeFhkZqUcffVQDBgxwazgAAODbXD6i0qVLF+Xk5NSYnpubq06dOrklFAAAgFTPolJYWFh9mz9/vmbNmqUPPvhAhw8f1uHDh/XBBx/ozjvv1BNPPOHpvAAAwIfU69SP3W53ujy+YRiaOHFi9TTDMCRJv/vd71RVVeWBmAAAwBfVq6isWbPG0zkAAABqqFdRGTZsmKdzAAAA1NCgK7SVlpZq27Ztys3NlcPhcHrs8ssvd0swAAAAl4vK8uXLNWXKFB07dqzGYzabjTEqAADAbVz+ePLtt9+uq6++WllZWXI4HE43SgoAAHAnl4tKTk6O7r77bkVHR3siDwAAQDWXi8of/vAHpaSkeCAKAACAM5fHqPzzn//U1VdfrfXr1yspKUkBAQFOj8+aNctt4QAAgG9zuai8/fbb+ve//63g4GClpKQ4XQjOZrNRVAAAgNu4XFT++te/at68ebr//vvl5+fymSMAAIB6c7lplJeXa9KkSZQUAADgcS63jalTp+rdd9/1RBYAAAAnLp/6qaqq0pNPPqkVK1aod+/eNQbTPv30024LBwAAfJvLRWX79u3q27evJGnHjh1Oj/16YC0AAMC5crmo8E3KAACgsTAiFgAAWJbLR1SGDx9+xlM8X3311TkFAgAAOM3lotKnTx+n+xUVFdq6dat27NihqVOnuisXAACA60XlmWeeqXX6Qw89pKKionMOBAAAcJrbxqhMnjxZixcvdtfiAAAA3FdUNm/erODgYHctDgAAwPVTP1deeaXTfcMwlJWVpS1btmjOnDluCwYAAOByUYmIiHC67+fnp65du+rhhx/W6NGj3RYMAADA5aKyZMkST+QAAACoweWiclp5eblyc3PlcDicprdv3/6cQwEAAEgNKCp79uzR9OnTtWnTJqfphmHIZrOpqqrKbeEAAIBvc7mo3HjjjWrWrJmWLVum2NhYvogQAAB4jMtFZevWrUpNTVW3bt08kQcAAKCay9dR6dGjh44dO+aJLAAAAE5cLipPPPGE7r33XqWkpCgvL0+FhYVONwAAAHdx+dTPqFGjJEkjR450ms5gWgAA4G4uF5U1a9Z4IodlZBw/ro8OHtDXR4/qcEmJ7IGB6tuype7u0VPnhYWZHQ9n8cIH87Q2/fM6H1907zK1jIhqxERA01VaXq7Pv0lV5pEc/ZiVreLSMt186f+ni3v3qDHvz8eO683V67Tn8BE18/dTn46Jum7kUIWHhJiQHN7E5aIybNgwT+SwjJf27FFqXp4ubddW3cIjdLSsVP/KzNTlX63Wh5cMV9ffXJkX1jJq4O+V1Gmg0zTDMPTKp4+rTWQsJQVwwcmSUn2y8Vu1Cg9T+6g22nXwcK3zHS88qUff/EDNgwJ19bAhKisv1xffpunQ0WOaN/UaNfP3b+Tk8CYNvuCbO6xbt05PPfWUUlNTlZWVpY8//lhXXHGFmZE0vXNnPTtwoAL9/jt8Z3y7dhq3apVe3LNbzwwYeIZnw2xd2vdWl/a9nab98NNWlVWU6qLksSalApome2iIFsy8SfbQFvoxK0dzX3un1vk+2/ydyioq9PC0a9Q6IlySdF5cjJ5452Ot2/69RvRJaszY8DJu+/bkhiguLlZycrIWLlxoZgwn/Vu1ciopkpQYGqYu4eHKPHnSpFQ4FxsyVshms+nC5DFmRwGalIBmzWQPbXHW+b7bvU99OiVWlxRJ6pXQXjEt7fp2115PRoQPMPWIyrhx4zRu3DgzI9SLYRg6VlqmzuGMUWlqKqsq9fWOVerSvreiIuPMjgN4neMni1RYckqJMTVPq3aMjVFG5k+NHwpexaUjKoZh6ODBgyotLfVUnjMqKysz5ePQnx46pOzSUxrfLr5R1gf3ydi7WSdLCnQRR1MAj8gvKpakWo+8RIS2UFFpqSoqKxs7FryIy0WlU6dOOnTokKfynNH8+fMVERFRfYuP93xxyDxZqAe3pqtfy5a6qkMHj68P7rUxY4X8/ZtpcNIos6MAXul0CQmoZcBs4H+mlVdy2Qo0nEtFxc/PT507d1ZeXp6n8pzR7NmzVVBQUH3zdGE6Wlqq6Zs2KSwgQAsHXSB/vteoSSktK9GWXeuU3OkChYXYzY4DeKWAZr+MIKio5Rpa5f+ZFtiMT/2g4VweTPv444/rL3/5i3bs2OGJPGcUFBSk8PBwp5unFFZU6MaNG1RYUaGlF16k6ObNPbYueMZ3u9b+8mmfPnzaB/CU06d8Tp8C+rWComKFBgdXlxmgIVz+7ZkyZYpKSkqUnJyswMBANf/NG/jx48fdFs4sZVVVunnTJu0vKtLrFw1VZw8WInjOhq3LFRwYovO7XWx2FMBrtQwLVVhIc+3Pzq3xWGZWttpHtzYhFbyJy0Xl2WefddvKi4qKtG/fvur7+/fv19atW9WyZUu1b9/ebetxRZVh6PZvv1H68Ty9NHiw+rVqZUoOnJvC4hPanvmthvQeraDAYLPjAF5tQNdO2rB9l/IKT6rVfz4dufOng8o+nq+xA/qanA5NnctFZerUqW5b+ZYtWzR8+PDq+3fffXf1OpYuXeq29bji0W3btCorSyNjYlVQXqFPDh50evwKkwoUXLNp20pVOao47QOco5WpGSouLVN+UZEkKX3fjzp+8pd/j+6frJDgIF0+eIC+/WGvHnvrQ405v49KKyr0xTepim/TShcn1bzcPuCKBp04zMzM1JIlS5SZmannnntOUVFR+vLLL9W+fXv17Nmz3su55JJLZBhGQyJ4zK6CfEnS6uwsrc7OqvE4RaVp2JCxXBEtWqp3R64kDJyLL75J1bHC/17scsueTG3ZkylJurBnN4UEB6lVeJj+et0f9NZX6/Tu2o1q5uevPp0SdN2IoYxPwTlz+Tdo7dq1GjdunC688EKtW7dOjz76qKKiopSRkaFXX31VH3zwgSdyNpq3L/bu7zLyFY/8abHZEQCv8Myf/1iv+dq1aaV7J/3ew2ngi1z+1M/999+vRx55RCtXrlRgYGD19BEjRujrr792azgAAODbXC4q27dv1+9/X7M1R0VF6dixY24JBQAAIDWgqNjtdmVl1Ry7kZ6errZt27olFAAAgNSAonLNNdfovvvuU3Z2tmw2mxwOhzZu3Kh77rlHU6ZM8URGAADgo1wuKo899pi6deum+Ph4FRUVqUePHrr44os1ZMgQPfDAA57ICAAAfJTLn/oJDAzUyy+/rAcffFDbt29XUVGR+vbtq86dO3siHwAA8GEuH1F5+OGHVVJSovj4eF166aWaOHGiOnfurFOnTunhhx/2REYAAOCjXC4q8+bNU9F/rlD4ayUlJZo3b55bQgEAAEgNKCqGYchms9WYnpGRoZYtW7olFAAAgOTCGJXIyEjZbDbZbDZ16dLFqaxUVVWpqKhIf/rTnzwSEgAA+KZ6F5Vnn31WhmHoj3/8o+bNm6eIiIjqxwIDA5WQkKDBgwd7JCQAAPBN9S4qp781OTExUUOGDFFAQIDHQgEAAEgN+HhyYmJirVemPa093y4MAADcxOWikpCQUOtg2tOqqqrOKRAAAMBpLheV9PR0p/sVFRVKT0/X008/rUcffdRtwQAAAFwuKsnJyTWmnX/++YqLi9NTTz2lK6+80i3BAAAAXL6OSl26du2q7777zl2LAwAAcP2ISmFhodN9wzCUlZWlhx56iO/7AQAAbuVyUbHb7TUG0xqGofj4eL3zzjtuCwYAAOByUVmzZo3TfT8/P7Vp00adOnVSs2YuLw4AAKBOLjeLYcOGeSIHAABADQ06BLJ7924tWLBAu3btkiR1795dM2fOVLdu3dwaDgAA+DaXP/Xz4YcfqlevXkpNTVVycrKSk5OVlpampKQkffjhh57ICAAAfJTLR1TuvfdezZ49Ww8//LDT9Llz5+ree+/VVVdd5bZwAADAt7l8RCUrK0tTpkypMX3y5Mln/A4gAAAAV7lcVC655BKtX7++xvQNGzZo6NChbgkFAAAgNeDUz+WXX6777rtPqampuuCCCyRJX3/9td5//33NmzdPn332mdO8AAAADeVyUfnzn/8sSXrhhRf0wgsv1PqYJNlsNr5JGQAAnBOXi4rD4fBEDgAAgBrc9qWEAAAA7tagC7599913WrNmjXJzc2scYXn66afdEgwAAMDlovLYY4/pgQceUNeuXRUdHe30BYW//bJCAACAc+FyUXnuuee0ePFiTZs2zQNxAAAA/svlMSp+fn668MILPZEFAADAictF5a677tLChQs9kQUAAMCJy6d+7rnnHo0fP14dO3ZUjx49FBAQ4PT4Rx995LZwAADAt7lcVGbNmqU1a9Zo+PDhatWqFQNoAQCAx7hcVF577TV9+OGHGj9+vCfyAAAAVHN5jErLli3VsWNHT2QBAABw4vIRlYceekhz587VkiVLFBIS4olMLvu5uEQtmjXo2nWWkF1ySpJ0ND9LQUFBJqcB6ud4Qa4k6VjBSX5vAS90NL/Q7AiSGlBUnn/+eWVmZio6OloJCQk1BtOmpaW5LVx9vZibI/9jRxt9ve5SUVkp2Rz6/LvX5O/vb3YcoF4qKyskPz+t2LFb/n77zI4DwANCw8Nlt9tNzeByUbniiis8EOPcLH7/fYWGhpod45yUl5crMDDQ7BiAS/i9Bbyb3W5XTEyMqRlshmEYpiY4B4WFhYqIiFBBQYHCw8PNjgMAAOrBlffvBg/sSE1N1a5duyRJPXv2VN++fRu6KAAAgFq5XFRyc3N1zTXXKCUlpfq8VX5+voYPH6533nlHbdq0cXdGAADgo1z+ePLtt9+ukydPaufOnTp+/LiOHz+uHTt2qLCwULNmzfJERgAA4KNcHqMSERGhVatWacCAAU7Tv/32W40ePVr5+fnuzHdGjFEBAKDpceX92+UjKg6Ho8ZHkiUpICBADofD1cUBAADUyeWiMmLECN1xxx06cuRI9bSff/5Zd911l0aOHOnWcAAAwLe5XFT++c9/qrCwUAkJCerYsaM6duyoxMREFRYWasGCBZ7ICAAAfJTLn/qJj49XWlqaVq1apR9++EGS1L17d40aNcrt4QAAgG/jgm8AAKBReWQw7VdffaUePXqosLDmlxQVFBSoZ8+eWr9+vetpAQAA6lDvovLss8/q5ptvrrX5RERE6NZbb9XTTz/t1nAAAMC31buoZGRkaOzYsXU+Pnr0aKWmprolFAAAgORCUcnJyan1+imnNWvWTEePHnVLKAAAAMmFotK2bVvt2LGjzse3bdum2NhYt4QCAACQXCgql156qebMmaPS0tIaj506dUpz587VZZdd5tZwAADAt9X748k5OTnq16+f/P39NXPmTHXt2lWS9MMPP2jhwoWqqqpSWlqaoqOjPRr41/h4MgAATY8r79/1vuBbdHS0Nm3apNtuu02zZ8/W6X5js9k0ZswYLVy4sFFLCgAA8H4uXZm2Q4cO+uKLL3TixAnt27dPhmGoc+fOioyM9FQ+AADgw1y+hL4kRUZGasCAAe7OAgAA4MTlLyUEAABoLA06omI1e/bsUWhoqNkxIKm8vFyBgYFmx4AX4XcKqJ3dbldMTIzZMTzOK4rKH66bKj9/f7Nj+LzKygqVHj2ottGt5O/PwTqcu4rKKu0/dlwto9vIn9c44CQiNEJffPKF15cVrygqrYdeq7DYRLNj+Ly8fRk6+uVzmntJc3WL4+PiOHfr9hbozi+qFHNljCLbMWgfOK04p1g/f/Cz8vPzKSpNQfOW0QqN7mB2DJ9XfOyIJCmhVZC6xYSYnAbe4MejpyRJIVEhCo+n/AK+iOPzAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAsigqAADAspqZHQDS3hX/0u7PX1ZYbKKGzf6X2XHgIWkHT+qhZQe0YV+hSiscOq9NsG65KFazRrQ1OxpQq5NHTmrHGzt07PtjKj9ZrpA2IWo/rL26/r6rmgXz9oHGwW+ayU6dyNW+la/LP7C52VHgQf/+/rh+98JO9Y0P1ZxL2ys0yF+Zx0p1+ESZ2dGAWpUcLdGqu1cpoEWAOo3vpMCwQOX9kKedb+3UicwTuuiBi8yOCB9hiaKycOFCPfXUU8rOzlZycrIWLFiggQMHmh2rUez6dKHsCT1kOByqKC4wOw48oPBUpaYs3a3xvVrqg1t6yM/PZnYk4Kx+WvOTKoorNOKJEYroECFJ6ji2owzD0IGvDqi8qFyBoYEmp4QvMH2Myrvvvqu7775bc+fOVVpampKTkzVmzBjl5uaaHc3j8vZtVdbWtep55Syzo8CD3vouVzmFFXp0QqL8/GwqLquSw2GYHQs4o8qSSklSsD3YaXrzyOay+dnk18z0tw/4CNN/055++mndfPPNuvHGG9WjRw+9+OKLCgkJ0eLFi82O5lGGo0o7PnhW8YMvU3hcR7PjwINW7cpXeLC/fs4vU9e53yn0jo0Kv2ujbntrr0orHGbHA2rVJqmNJOm7Bd/pxI8nVHK0RAfXH1Tml5nqdFknxqig0Zj6m1ZeXq7U1FTNnj27epqfn59GjRqlzZs315i/rKxMZWX/PadfWFjYKDk94cCGT3XqRI4u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0tjOtr+nTZtWYxvGjh171uU25f0tqdbXus1m01NPPVXnMpvC/vYkSxeVd999V3fffbfmzp2rtLQ0JScna8yYMcrNza11/k2bNunaa6/V9OnTlZ6eriuuuEJXXHGFduzY0cjJG27t2rWaMWOGvv76a61cuVIVFRUaPXq0iouLz/i88PBwZWVlVd8OHDjQSIndq2fPnk7bsWHDhjrn9Yb9LUnfffed0zavXLlSknT11VfX+ZymuL+Li4uVnJyshQsX1vr4k08+qeeff14vvviivvnmG7Vo0UJjxoxRaWlpnct09W+EGc603SUlJUpLS9OcOXOUlpamjz76SLt379bll19+1uW68loxw9n2tySNHTvWaRvefvvtMy6zqe9vSU7bm5WVpcWLF8tms+mqq64643Ktvr89yrCwgQMHGjNmzKi+X1VVZcTFxRnz58+vdf6JEyca48ePd5o2aNAg49Zbb/VoTk/Kzc01JBlr166tc54lS5YYERERjRfKQ+bOnWskJyfXe35v3N+GYRh33HGH0bFjR8PhcNT6uDfsb0nGxx9/XH3f4XAYMTExxlNPPVU9LT8/3wgKCjLefvvtOpfj6t8Is/12u2vz7bffGpKMAwcO1DmPq68Vs9W23VOnTjUmTJjg0nK8cX9PmDDBGDFixBnnaWr7290se0SlvLxcqampGjVqVPU0Pz8/jRo1Sps3b671OZs3b3aaX5LGjBlT5/xNQUFBgaT/fgFjXYqKitShQwfFx8drwoQJ2rlzZ2PEc7u9e/cqLi5O5513nq6//nodPHiwznm9cX+Xl5frjTfe0B//+EfZbLY65/OW/X3a/v37lZ2d7bQ/IyIiNGjQoDr3Z0P+RjQFBQUFstlsstvtZ5zPldeKVaWkpCgqKkpdu3bVbbfdpry8vDrn9cb9nZOTo88//1zTp08/67zesL8byrJF5dixY6qqqlJ0dLTT9OjoaGVnZ9f6nOzsbJfmtzqHw6E777xTF154oXr16lXnfF27dtXixYv16aef6o033pDD4dCQIUN0+PDhRkx77gYNGqSlS5dq+fLlWrRokfbv36+hQ4fq5MmTtc7vbftbkj755BPl5+dr2rRpdc7jLfv7107vM1f2Z0P+RlhdaWmp7rvvPl177bVn/HI6V18rVjR27Fj961//0urVq/XEE09o7dq1GjdunKqqqmqd3xv392uv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\n",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "# extract feasible solution\n",
    "feasibles = result.feasible()\n",
    "# get the index of the lowest objective function\n",
    "objectives = np.array(feasibles.evaluation.objective)\n",
    "lowest_index = np.argmin(objectives)\n",
    "# get indices of x = 1\n",
    "indices, _, _ = feasibles.record.solution['x'][lowest_index]\n",
    "# get task number and execution node\n",
    "tasks, nodes = indices\n",
    "# get instance information\n",
    "L = instance_data['L']\n",
    "M = instance_data['M']\n",
    "# initialize execution time\n",
    "exec_time = np.zeros(M, dtype=np.int64)\n",
    "# compute summation of execution time each nodes\n",
    "for i, j in zip(tasks, nodes):\n",
    "    plt.barh(j, L[i], left=exec_time[j],ec=\"k\", linewidth=1,alpha=0.8)\n",
    "    plt.text(exec_time[j] + L[i] / 2.0 - 0.25 ,j-0.05, str(i+1),fontsize=12)\n",
    "    exec_time[j] += L[i]\n",
    "plt.yticks(range(M))\n",
    "plt.ylabel('Computer numbers')\n",
    "plt.xlabel('Execution time')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With the above visualization, we obtain a graph where the execution times of three computers are approximately equal.\n",
    "The maximum value is 19, as explained at the beginning."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.15"
  },
  "vscode": {
   "interpreter": {
    "hash": "2e8d7574d7ec71e14cb1575cf43673432d6fae464c836a7b3733d4f6c20243fb"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}