Download

Source code for eqc_models.assignment.resource

  • from typing import (Dict, List)
  • import numpy as np
  • from eqc_models.base.quadratic import ConstrainedQuadraticModel
  • from eqc_models.base.constraints import InequalitiesMixin
  • [docs]
  • class ResourceAssignmentModel(InequalitiesMixin, ConstrainedQuadraticModel):
  • """
  • Resource assignment model
  • Parameters
  • ------------
  • resources : List
  • tasks : List
  • >>> # name is not a required attribute of the resources or tasks
  • >>> crews = [{"name": "Maintenance Crew 1", "skills": ["A", "F"], "capacity": 5, "cost": 4},
  • ... {"name": "Baggage Crew 1", "skills": ["B"], "capacity": 4, "cost": 1},
  • ... {"name": "Maintenance Crew 2", "skills": ["A", "F"], "capacity": 5, "cost": 2}]
  • >>> tasks = [{"name": "Refuel", "skill_need": "F", "load": 3},
  • ... {"name": "Baggage", "skill_need": "B", "load": 1}]
  • >>> model = ResourceAssignmentModel(crews, tasks)
  • >>> assignments = model.createAssignmentVars()
  • >>> assignments
  • [{'resource': 0, 'task': 0}, {'resource': 1, 'task': 1}, {'resource': 2, 'task': 0}]
  • >>> A, b, senses = model.constrainAssignments(assignments)
  • >>> A
  • array([[3., 0., 0.],
  • [0., 1., 0.],
  • [0., 0., 3.],
  • [3., 0., 3.],
  • [0., 3., 0.]], dtype=float32)
  • >>> b
  • array([5., 4., 5., 3., 3.], dtype=float32)
  • >>> senses
  • ['LE', 'LE', 'LE', 'EQ', 'EQ']
  • >>> A, b = model.constraints
  • >>> A
  • array([[3., 0., 0., 1., 0., 0.],
  • [0., 1., 0., 0., 1., 0.],
  • [0., 0., 3., 0., 0., 1.],
  • [3., 0., 3., 0., 0., 0.],
  • [0., 3., 0., 0., 0., 0.]])
  • """
  • def __init__(self, resources, tasks):
  • self.resources = resources
  • self.checkTasks(tasks)
  • self.tasks = tasks
  • self.assignments = assignments = self.createAssignmentVars()
  • n = len(assignments) + len(resources)
  • self.variables = [f"a{i}" for i in range(len(assignments))]
  • self.upper_bound = np.ones((n,))
  • self.upper_bound[-len(resources):] = [resource["capacity"] for resource in resources]
  • A, b, senses = self.constrainAssignments(assignments)
  • J = np.zeros((n, n))
  • C = np.zeros((n,), dtype=np.float32)
  • # objective is to minimize cost of assignments
  • for j, assignment in enumerate(assignments):
  • C[j] = resources[assignment["resource"]]["cost"] * tasks[assignment["task"]]["load"]
  • super(ResourceAssignmentModel, self).__init__(C, J, A, b)
  • self.senses = senses
  • # always use a machine slack
  • self.machine_slacks = 1
  • [docs]
  • @classmethod
  • def checkTasks(cls, tasks):
  • for task in tasks:
  • if "skill_need" not in task:
  • raise ValueError("All tasks must have the skill_need attribute")
  • if "load" not in task:
  • raise ValueError("All tasks must have the load attribute")
  • [docs]
  • def createAssignmentVars(self):
  • """ Examine all combinatins of possible crew-task assignments """
  • assign_vars = []
  • resources = self.resources
  • tasks = self.tasks
  • for i, resource in enumerate(resources):
  • skills = resource["skills"]
  • for j, task in enumerate(tasks):
  • if task["skill_need"] in skills:
  • assign_vars.append({"resource": i, "task": j})
  • return assign_vars
  • [docs]
  • def constrainAssignments(self, assignments : List) -> List:
  • """
  • Examine the assignments to determine the necessary constraints to
  • ensure feasibility of solution.
  • """
  • # A is sized using the number of crews and the number of assignment variables plus slacks
  • m1 = len(self.resources)
  • m2 = len(self.tasks)
  • n1 = len(assignments)
  • m = m1 + m2
  • n = n1
  • A = np.zeros((m, n), dtype=np.float32)
  • b = np.zeros((m,), dtype=np.float32)
  • for i, resource in enumerate(self.resources):
  • b[i] = resource["capacity"]
  • for k, assignment in enumerate(assignments):
  • if assignment["resource"] == i:
  • A[i, k] = self.tasks[assignment["task"]]["load"]
  • assignment_coeff = np.max(A)
  • for i, task in enumerate(self.tasks):
  • b[m1+i] = assignment_coeff
  • for k, assignment in enumerate(assignments):
  • if assignment["task"] == i:
  • A[m1+i, k] = assignment_coeff
  • senses = ["LE" for resource in self.resources] + ["EQ" for task in self.tasks]
  • return A, b, senses
  • @property
  • def sum_constraint(self) -> int:
  • """ This value is a suggestion which should be used with a machine slack """
  • sc = 0
  • sc += sum([resource["capacity"] for resource in self.resources])
  • sc += len(self.tasks)
  • return sc
  • [docs]
  • def decode(self, solution : np.array) -> List[Dict]:
  • """
  • Convert the binary solution into a list of tasks
  • """
  • # ensure solution is array
  • solution = np.array(solution)
  • resource_assignments = [[] for resource in self.resources]
  • vals = [val for val in set(solution) if val <= 1.0]
  • # check if there are fractional values less than 1
  • if solution[~np.logical_or(solution==0, solution>=1)].size>0:
  • # iterate over the values and assign tasks by largest value for tasks
  • # not assigned already
  • remaining_tasks = list(range(len(self.tasks)))
  • fltr = self.upper_bound==1
  • while len(remaining_tasks) > 0 and solution[fltr].shape[0]>0:
  • largest = np.max(solution[fltr])
  • indices, = np.where(np.logical_and(fltr, solution == largest))
  • for idx in indices:
  • assignment = self.assignments[idx]
  • if assignment["task"] in remaining_tasks:
  • task = self.tasks[assignment["task"]]
  • resource_assignments[assignment["resource"]].append(task)
  • del remaining_tasks[remaining_tasks.index(assignment["task"])]
  • break
  • fltr = np.logical_and(fltr, solution < largest)
  • else:
  • # Use the restriction that a task cannot be assigned more than once
  • for j, task in enumerate(self.tasks):
  • highest = 0
  • best_resource = None
  • for a, assignment in zip(solution, self.assignments):
  • if assignment["task"] == j:
  • if a > highest:
  • highest = a
  • best_resource = assignment["resource"]
  • assert best_resource is not None, f"solution had no positive assignment values for {task}"
  • resource_assignments[best_resource].append(task)
  • return resource_assignments