Source code for quantum_launcher.problems.problem_formulations.jssp.pyqubo_scheduler

  1from __future__ import print_function
  2
  3from bisect import bisect_right
  4
  5# from pyqubo import Binary
  6from .Binary import Binary
  7
  8from .scheduler import JobShopScheduler, KeyList, get_label
  9
 10
[docs] 11def get_jss_bqm(job_dict, max_time, disable_till=None, disable_since=None, disabled_variables=None, lagrange_one_hot=3, 12 lagrange_precedence=1, lagrange_share=2): 13 if disable_till is None: 14 disable_till = {} 15 if disable_since is None: 16 disable_since = {} 17 if disabled_variables is None: 18 disabled_variables = [] 19 20 scheduler = DWaveScheduler(job_dict, max_time) 21 return scheduler.get_bqm(disable_till, disable_since, disabled_variables, 22 lagrange_one_hot, 23 lagrange_precedence, 24 lagrange_share)
25 26
[docs] 27class DWaveScheduler(JobShopScheduler): 28 29 def __init__(self, job_dict, max_time=None): 30 super().__init__(job_dict, max_time) 31 32 def _add_one_start_constraint(self, lagrange_one_hot=1): 33 """self.csp gets the constraint: A task can start once and only once 34 """ 35 for task in self.tasks: 36 task_times = {get_label(task, t) for t in range(self.max_time)} 37 H_term = 0 38 for label in task_times: 39 if label in self.absurd_times: 40 continue 41 if label not in self.H_vars: 42 var = Binary(label) 43 self.H_vars[label] = var 44 else: 45 var = self.H_vars[label] 46 H_term += var 47 self.H += lagrange_one_hot * ((1 - H_term) ** 2) 48 49 def _add_precedence_constraint(self, lagrange_precedence=1): 50 """self.csp gets the constraint: Task must follow a particular order. 51 Note: assumes self.tasks are sorted by jobs and then by position 52 """ 53 for current_task, next_task in zip(self.tasks, self.tasks[1:]): 54 if current_task.job != next_task.job: 55 continue 56 57 # Forming constraints with the relevant times of the next task 58 for t in range(self.max_time): 59 current_label = get_label(current_task, t) 60 if current_label in self.absurd_times: 61 continue 62 63 if current_label not in self.H_vars: 64 var1 = Binary(current_label) 65 self.H_vars[current_label] = var1 66 else: 67 var1 = self.H_vars[current_label] 68 69 for tt in range(min(t + current_task.duration, self.max_time)): 70 71 next_label = get_label(next_task, tt) 72 if next_label in self.absurd_times: 73 continue 74 if next_label not in self.H_vars: 75 var2 = Binary(next_label) 76 self.H_vars[next_label] = var2 77 else: 78 var2 = self.H_vars[next_label] 79 80 self.H += lagrange_precedence * var1 * var2 81 82 def _add_share_machine_constraint(self, lagrange_share=1): 83 """self.csp gets the constraint: At most one task per machine per time unit 84 """ 85 sorted_tasks = sorted(self.tasks, key=lambda x: x.machine) 86 # Key wrapper for bisect function 87 wrapped_tasks = KeyList(sorted_tasks, lambda x: x.machine) 88 89 head = 0 90 while head < len(sorted_tasks): 91 92 # Find tasks that share a machine 93 tail = bisect_right(wrapped_tasks, sorted_tasks[head].machine) 94 same_machine_tasks = sorted_tasks[head:tail] 95 96 # Update 97 head = tail 98 99 # No need to build coupling for a single task 100 if len(same_machine_tasks) < 2: 101 continue 102 103 # Apply constraint between all tasks for each unit of time 104 for task in same_machine_tasks: 105 for other_task in same_machine_tasks: 106 if task.job == other_task.job and task.position == other_task.position: 107 continue 108 109 for t in range(self.max_time): 110 current_label = get_label(task, t) 111 if current_label in self.absurd_times: 112 continue 113 114 if current_label not in self.H_vars: 115 var1 = Binary(current_label) 116 self.H_vars[current_label] = var1 117 else: 118 var1 = self.H_vars[current_label] 119 120 for tt in range(t, min(t + task.duration, self.max_time)): 121 this_label = get_label(other_task, tt) 122 if this_label in self.absurd_times: 123 continue 124 if this_label not in self.H_vars: 125 var2 = Binary(this_label) 126 self.H_vars[this_label] = var2 127 else: 128 var2 = self.H_vars[this_label] 129 130 self.H += lagrange_share * var1 * var2 131
[docs] 132 def get_bqm(self, disable_till, disable_since, disabled_variables, 133 lagrange_one_hot, lagrange_precedence, lagrange_share): 134 """Returns a BQM to the Job Shop Scheduling problem. """ 135 136 # Apply constraints to self.csp 137 self._remove_absurd_times( 138 disable_till, disable_since, disabled_variables) 139 self._add_one_start_constraint(lagrange_one_hot) 140 self._add_precedence_constraint(lagrange_precedence) 141 self._add_share_machine_constraint(lagrange_share) 142 # Get BQM 143 # bqm = dwaveBinarycsp.stitch(self.csp, **stitch_kwargs) 144 145 # Edit BQM to encourage the shortest schedule 146 # Overview of this added penalty: 147 # - Want any-optimal-schedule-penalty < any-non-optimal-schedule-penalty 148 # - Suppose there are N tasks that need to be scheduled and N > 0 149 # - Suppose the the optimal end time for this schedule is t_N 150 # - Then the worst optimal schedule would be if ALL the tasks ended at time t_N. (Since 151 # the optimal schedule is only dependent on when the LAST task is run, it is irrelevant 152 # when the first N-1 tasks end.) Note that by "worst" optimal schedule, I am merely 153 # referring to the most heavily penalized optimal schedule. 154 # 155 # Show math satisfies any-optimal-schedule-penalty < any-non-optimal-schedule-penalty: 156 # - Penalty scheme. Each task is given the penalty: base^(task-end-time). The penalty 157 # of the entire schedule is the sum of penalties of these chosen tasks. 158 # - Chose the base of my geometric series to be N+1. This simplifies the math and it will 159 # become apparent why it's handy later on. 160 # 161 # - Comparing the SUM of penalties between any optimal schedule (on left) with that of the 162 # WORST optimal schedule (on right). As shown below, in this penalty scheme, any optimal 163 # schedule penalty <= the worst optimal schedule. 164 # sum_i (N+1)^t_i <= N * (N+1)^t_N, where t_i the time when the task i ends [eq 1] 165 # 166 # - Now let's show that all optimal schedule penalties < any non-optimal schedule penalty. 167 # We can prove this by applying eq 1 and simply proving that the worst optimal schedule 168 # penalty (below, on left) is always less than any non-optimal schedule penalty. 169 # N * (N+1)^t_N < (N+1)^(t_N + 1) 170 # Note: t_N + 1 is the smallest end time for a non-optimal 171 # schedule. Hence, if t_N' is the end time of the last 172 # task of a non-optimal schedule, t_N + 1 <= t_N' 173 # <= (N+1)^t_N' 174 # < sum^(N-1) (N+1)^t_i' + (N+1)^t_N' 175 # = sum^N (N+1)^t_i' 176 # Note: sum^N (N+1)^t' is the sum of penalties for a 177 # non-optimal schedule 178 # 179 # - Therefore, with this penalty scheme, all optimal solution penalties < any non-optimal 180 # solution penalties 181 base = len(self.last_task_indices) + 1 # Base for exponent 182 # Get our pruned (remove_absurd_times) variable list so we don't undo pruning 183 # pruned_variables = list(bqm.variables) 184 for i in self.last_task_indices: 185 task = self.tasks[i] 186 187 for t in range(self.max_time): 188 end_time = t + task.duration 189 190 # Check task's end time; do not add in absurd times 191 if end_time > self.max_time: 192 continue 193 194 # Add bias to variable 195 bias = 2 * base ** (end_time - self.max_time) 196 label = get_label(task, t) 197 if label in self.absurd_times: 198 continue 199 if label not in self.H_vars: 200 var = Binary(label) 201 self.H_vars[label] = var 202 else: 203 var = self.H_vars[label] 204 self.H += var * bias 205 206 # Get BQM 207 self.model = self.H.compile() 208 bqm = self.model.to_bqm() 209 return bqm