Source code for eqc_models.allocation.allocation
- from typing import List, Tuple
- from enum import Enum
- import numpy as np
- from eqc_models.base.constraints import InequalitiesMixin
- from eqc_models.base.quadratic import ConstrainedQuadraticModel
- [docs]
- class AllocationModel(ConstrainedQuadraticModel):
-     """
-     Parameters
-     ----------
-     resources: List 
-         names of available resources.
-     tasks: List
-         names of tasks.
-     resource_usage: List of Lists or 2D np.ndarray 
-         rows represent tasks and columns represent resources,
-         specifying the amount of each resource required per task.
-     resource_limits: 1D array or List
-         specifying the limit on each resource.
-     cost_per_task: 1D array List
-         specifying the cost per task (or benefit with negative of value).
-     Attributes
-     ----------
-     penalty_multiplier: float
-         value for weighting the penalties formed from the equality constraints
-     qubo: eqc_models.base.operators.QUBO
-         QUBO operator representation
-     polynomial: eqc_models.base.operators.Polynomial
-         Polynomial operator representation
-     This class represents a resource allocation model for maximizing total benefit. In other words,
-     Given a list of resources and a list of tasks, allocate the resources among the tasks so as to 
-     maximize the economic benefit.
-     Here's an example. Five tasks must share 4 resources. Each task can use a different amount of
-     each resource. 
-     +--------------------+------+------+---------+---------+---------+
-     |                    | Spam | Eggs | Coconut | Sparrow | Benefit |
-     +--------------------+------+------+---------+---------+---------+
-     | Breakfast          | 1    | 2    | 0       | 0       | 3       | 
-     +--------------------+------+------+---------+---------+---------+
-     | Countryside Stroll | 0    | 0    | 1       | 0       | 1       |
-     +--------------------+------+------+---------+---------+---------+
-     | Storm Castle       | 0    | 12   | 1       | 1       | 10      | 
-     +--------------------+------+------+---------+---------+---------+
-     | Availability       | 1    | 12   | 2       | 1       |         |
-     +--------------------+------+------+---------+---------+---------+
-     >>> resources = ["Spam", "Eggs", "Coconut", "Sparrow"]
-     >>> tasks = ["Breakfast", "Countryside Stroll", "Storm Castle"]
-     >>> resource_usage = [[1, 2, 0, 0], [0, 0, 1, 0], [0, 12, 1, 1]]
-     >>> resource_limits = [1, 12, 2, 1]
-     >>> cost_per_task = [-3, -1, -10.] 
-     >>> allocation_model = AllocationModel(resources, tasks, resource_usage, resource_limits, cost_per_task)
-     >>> allocation_model.penalty_multiplier = 1
-     >>> C, J = allocation_model.H 
-     >>> C # -3 -2 * (12 * 2 + 1 * 1), -1 -2 * 2*1, -10 -2 * (12 * 12 + 1 * 2 + 1 * 1) 
-     ...   # doctest: +NORMALIZE_WHITESPACE
-     array([ -53., -5.,     -304.])
-     >>> J # doctest: +NORMALIZE_WHITESPACE
-     array([[ 5., 0., 24.],
-            [ 0., 1.,  1.],
-            [ 24., 1., 146.]])
-     """
-     def __init__(self, resources: List, tasks: List, resource_usage: List, resource_limits: List,
-                  cost_per_task: List):
-         if not isinstance(resources, list):
-             raise TypeError("Argument 'resources' must be a list")
-         if not isinstance(tasks, list):
-             raise TypeError("Argument 'tasks' must be a list")
-         if not isinstance(resource_usage, list):
-             raise TypeError("Argument 'resource_usage' must be a list")
-         if not isinstance(resource_limits, list):
-             raise TypeError("Argument 'resource_limits' must be a list")
-         if not isinstance(cost_per_task, list):
-             raise TypeError("Argument 'cost_per_task' must be a list")
-         
-         self.resources = resources
-         self.tasks = tasks
-         self.resource_usage = np.array(resource_usage)
-         self.resource_limits = np.array(resource_limits)
-         self.cost_per_task = np.array(cost_per_task)
-         super(AllocationModel, self).__init__(self.cost_per_task, np.zeros((len(self.tasks), len(self.tasks))), self.resource_usage.T, self.resource_limits)
-         
-         
-         
-         
-         self._validate_dimensions()
-         
-     @property
-     def upper_bound(self) -> np.array:
-         return np.array([np.floor(max(self.resource_limits[self.resource_limits != 0]) /
-                                   min(self.resource_usage[self.resource_usage != 0]))] * len(self.tasks), dtype=int)
-     @upper_bound.setter
-     def upper_bound(self, value: List):
-         self._upper_bound = value
-     @property
-     def variables(self):
-         return [(i, j) for i in self.tasks for j in self.resources]
-     def _validate_dimensions(self):
-         """Raises ValueErrors for inconsistent dimensions."""
-         
-         num_tasks = len(self.tasks)
-         num_resources = len(self.resources)
-         
-         
-         
-         if self.resource_usage.shape != (num_tasks, num_resources):
-             raise ValueError("resource_usage matrix dimensions don't match number of tasks and resources")
-         
-         if self.resource_limits.shape[0] != num_resources:
-             raise ValueError("resource_limits length doesn't match number of resources")
-         
-         if self.cost_per_task.shape[0] != num_tasks:
-             raise ValueError("cost_per_task length doesn't match number of tasks")
- [docs]
-     def add_task(self, task: str, task_resource_usage: List, task_cost: float):
-         """ 
-         task: str
-             Name of task to add
-         task_resource_usage: List
-             Quantity of resource used for task for all tasks
-         task_cost: float
-             Quantity indicating the cost or benefit (negative) of the task
-         
-         Add a task to the problem, modifying the resource usage and task cost arrays.
-         """
-         self.tasks += [task]
-         self.resource_usage = np.vstack([self.resource_usage, task_resource_usage])
-         self.cost_per_task = np.append(self.cost_per_task, task_cost)
-     @property
-     def H(self) -> Tuple[np.ndarray,np.ndarray]:
-         """ Return linear, quadratic portions of the (quadratic) Hamiltonian """
-         Pl, Pq = self.penalties
-         alpha = self.penalty_multiplier
-         obj_linear, obj_quad = self.linear_objective, self.quad_objective 
-         self._C = obj_linear + alpha * Pl
-         self._J = obj_quad + alpha * Pq
-         return obj_linear + alpha * Pl, obj_quad + alpha * Pq
- [docs]
- class ResourceRuleEnum(Enum):
-     """ 
-     Enumeration of the allowed resource rules, mapping to the mathematical expression:
-     MAXIMUM -> LE (less than or equal to)
-     MINIMUM -> GE (greater than or equal to)
-     EXACT -> EQ (equal to)
-     """
-     MAXIMUM = "LE"
-     MINIMUM = "GE"
-     EXACT = "EQ"
- [docs]
- class AllocationModelX(InequalitiesMixin, AllocationModel):
-     """ 
-     Parameters
-     ----------
-     resources: List 
-         names of available resources.
-     tasks: List
-         names of tasks.
-     resource_usage: List of Lists or 2D np.ndarray 
-         rows represent tasks and columns represent resources,
-         specifying the amount of each resource required per task.
-     resource_limits: 1D array or List
-         specifying the limit on each resource.
-     resource_rule: List
-         ResourceRuleEnum values for each resource
-     cost_per_task: 1D array List
-         specifying the cost per task (or benefit with negative of value).
-     Attributes
-     ----------
-     penalty_multiplier: float
-         value for weighting the penalties formed from the equality constraints
-     qubo: eqc_models.base.operators.QUBO
-         QUBO oeprator representation
-     polynomial: eqc_models.base.operators.Polynomial
-         Polynomial operator representation
-     variables: List
-         names of variables formed from tasks and assignments
-     This class represents a resource allocation model for maximizing total benefit. In other words,
-     Given a list of resources and a list of tasks, allocate the resources among the tasks so as to 
-     maximize the economic benefit.
-     Adds resource_rule as an argument. This must be a list of strings specifying
-     constraints for each resource (LE, GE, or EQ).
-     Here's an example. Five tasks must share 4 resources. Each task can use a different amount of
-     each resource. 
-     +--------------------+------+------+---------+---------+---------+
-     |                    | Spam | Eggs | Coconut | Sparrow | Benefit |
-     +--------------------+------+------+---------+---------+---------+
-     | Breakfast          | 1    | 2    | 0       | 0       | 3       | 
-     +--------------------+------+------+---------+---------+---------+
-     | Countryside Stroll | 0    | 0    | 1       | 0       | 1       |
-     +--------------------+------+------+---------+---------+---------+
-     | Storm Castle       | 0    | 12   | 1       | 1       | 10      | 
-     +--------------------+------+------+---------+---------+---------+
-     | Availability       | 1    | 12   | 2       | 1       |         |
-     +--------------------+------+------+---------+---------+---------+
-     >>> resources = ["Spam", "Eggs", "Coconut", "Sparrow"]
-     >>> tasks = ["Breakfast", "Countryside Stroll", "Storm Castle"]
-     >>> resource_usage = [[1, 2, 0, 0], [0, 0, 1, 0], [0, 12, 1, 1]]
-     >>> resource_limits = [1, 12, 2, 1]
-     >>> cost_per_task = [-3, -1, -10.] 
-     >>> resource_rules = [ResourceRuleEnum.MAXIMUM for i in range(len(resources))]
-     >>> allocation_model = AllocationModelX(resources, tasks, resource_usage, resource_limits, resource_rules, cost_per_task)
-     >>> allocation_model.penalty_multiplier = 1
-     >>> C, J = allocation_model.H 
-     >>> C # -3 -2 * (12 * 2 + 1 * 1), -1 -2 * 2*1, -10 -2 * (12 * 12 + 1 * 2 + 1 * 1) 
-     ...   # doctest: +NORMALIZE_WHITESPACE
-     array([ -53., -5., -304., -2., -24., -4., -2.])
-     >>> J # doctest: +NORMALIZE_WHITESPACE
-     array([[  5.,  0.,  24., 1.,  2., 0., 0.],
-            [  0.,  1.,   1., 0.,  0., 1., 0.],
-            [ 24.,  1., 146., 0., 12., 1., 1.],
-            [  1.,  0.,   0., 1.,  0., 0., 0.], 
-            [  2.,  0.,  12., 0.,  1., 0., 0.], 
-            [  0.,  1.,   1., 0.,  0., 1., 0.],
-            [  0.,  0.,   1., 0.,  0., 0., 1.]])
-     """
-     def __init__(self, resources: List, tasks: List, resource_usage: List, resource_limits: List,
-                  resource_rule: List[ResourceRuleEnum], cost_per_task: List):
-         super().__init__(resources, tasks, resource_usage, resource_limits, cost_per_task)
-         if not isinstance(resource_rule, list):
-             raise TypeError("Argument 'resource_rule' must be a list")
-         elif len(resource_rule) != len(resources):
-             raise ValueError("Argument 'resource_rule' must be the same length as 'resources'")
-         try:
-             check_rule = set([rule.value for rule in resource_rule])
-             if not check_rule.issubset({"LE", "GE", "EQ"}):
-                 raise ValueError("Argument 'resource_rule' must contain only enums 'ResourceRuleEnum.MAXIMUM', "
-                                  "'ResourceRuleEnum.MINIMUM' or 'ResourceRuleEnum.EXACT'.")
-         except AttributeError as e:
-             
-             raise TypeError("Argument 'resource_rule' must contain only enums. Elements lack a 'value' "
-                             "attribute.") from e
-         self.senses = list([rule.value for rule in resource_rule])
-     @property
-     def upper_bound(self) -> np.array:
-         return np.array([np.floor(max(self.resource_limits[self.resource_limits != 0]) /
-                                   min(self.resource_usage[self.resource_usage != 0]))] * self.n, dtype=int)
-     @upper_bound.setter
-     def upper_bound(self, value: List):
-         self._upper_bound = value
-     @property
-     def linear_objective(self) -> np.ndarray:
-         """
-         Returns a 1D numpy array representing the linear part of the objective function (total profit).
-         """
-         return np.hstack([self.cost_per_task, np.zeros(self.num_slacks)])
-     @linear_objective.setter
-     def linear_objective(self, value : np.ndarray):
-         
-         assert (value[len(self.cost_per_task):]==0).all(), "additional values beyond cost length must be 0"
-         self.cost_per_task[:] = value[:len(self.cost_per_task)]
-     @property
-     def quad_objective(self) -> np.ndarray:
-         """
-         Returns a 2D numpy array representing the quadratic part of the objective function (always zero in this case).
-         """
-         
-         n = self.n
-         return np.zeros((n, n))
-     @quad_objective.setter
-     def quad_objective(self, value: np.ndarray):
-         """ Don't let anything be passed in except arrays of all 0 """
-         
-         
-         assert (value==0).all(), "quadratic terms in objective must be 0"
-         
-     @property
-     def H(self):
-         """
-         Overrides the parent build method to incorporate slack variables based on resource_rule.
-         """
-         
-         Pl, Pq = self.penalties
-         alpha = self.penalty_multiplier
-         obj_linear, obj_quad = self.linear_objective, self.quad_objective
-         self._C = obj_linear + alpha * Pl
-         self._J = obj_quad + alpha * Pq
-         return obj_linear + alpha * Pl, obj_quad + alpha * Pq
- [docs]
-     def checkResources(self, solution):
-         """
-         Parameters
-         ----------
-         solution: List or np.ndarray
-             solution vector to check for resource violations
-         Returns
-         -------
-         List of violations: (name, rule, violation quantity, message)
-         """
-         violations = []
-         solution = np.array(solution)
-         for name, usage, rule, limit in zip(self.resources, self.resource_usage.T,
-                                             self.senses, self.resource_limits):
-             decision_vars = self.n - self.num_slacks
-             value = np.squeeze(usage@solution[:decision_vars])
-             if rule == ResourceRuleEnum.MINIMUM:
-                 mult = -1
-             else:
-                 mult = 1
-             if rule == ResourceRuleEnum.EXACT:
-                 if value - limit != 0:
-                     msg = f"{value} of {name} violates {rule} {limit}"
-                     violations.append((name, rule, value - limit, msg))
-             else:
-                 if mult * (value - limit) > 0:
-                     msg = f"{value} of {name} violates {rule} {limit}"
-                     violations.append((name, rule, (value - limit), msg))
-         return violations
-     @property
-     def n(self):
-         return len(self.tasks) + self.num_slacks