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)
-         
-         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
-         
-         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.
-         """
-         
-         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 
-         """
-         
-         solution = np.array(solution)
-         resource_assignments = [[] for resource in self.resources]
-         vals = [val for val in set(solution) if val <= 1.0]
-         
-         if solution[~np.logical_or(solution==0, solution>=1)].size>0:
-             
-             
-             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:
-             
-             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