def _build_first_label(self) -> core.NodeLabel: """Constructs the initial label to kick off the labeling algorithm.""" supp_pts = [[0], [self.init_soc]] energy_to_end = self.min_energy_consumed_after_node[self.node_local_id_dep] if self.init_soc >= energy_to_end: key_time = self.min_travel_time_after_node[self.node_local_id_dep] else: key_time = self.min_travel_charge_time_after_node[self.node_local_id_dep] return core.NodeLabel(self.node_local_id_dep, key_time, 0, None, self.init_soc, 0, supp_pts, None, 0, 0, None)
def _build_label_list(self, curr_label: core.NodeLabel) -> List[core.NodeLabel]: """Builds list of labels to extend from the curr_label. Specifically, creates one new label for each supporting point of the current SOC function in order to explore the possibility of switching over to the new CS at that point. """ label_list = [] curr_local_id = curr_label.node_id_for_label curr_node = self.nodes_gpr[curr_local_id] # we only split into new labels at charging stations if curr_node.type == core.NodeType.CHARGING_STATION: # charging function details cs_supp_pts = self.instance.get_cf_breakpoints( curr_node) # CS's charging func breakpoints # slopes of the CS's charging func segments cs_slope = self.instance.get_slope(curr_node) # num breakpoints (n bpts -> (n-1) segments) cs_n_pts = len(cs_supp_pts[0]) # current SOC function lbl_supp_pts = curr_label.supporting_pts lbl_n_pts = curr_label.get_num_supp_pts() # iterate over points in the label's SOC function for k in range(lbl_n_pts): trip_time = lbl_supp_pts[0][k] soc_at_cs = lbl_supp_pts[1][k] # don't switch if it leads to a time-infeasible path if trip_time > self.latest_departure_time[curr_local_id]: continue # don't switch if energy is sufficient to finish the route if soc_at_cs > self.max_energy_at_departure[curr_local_id]: continue # switch only if the new slope is better than the current if k < lbl_n_pts - 1: curr_slope = curr_label.slope[k] new_slope = self.instance.get_slope( node=curr_node, soc=max(0, soc_at_cs)) if curr_slope >= new_slope: continue # don't switch if soc when departing from last CS was sufficient to finish route if (curr_label.last_visited_cs is not None and soc_at_cs + curr_label.energy_consumed_since_last_cs > self.max_energy_at_departure[curr_label.last_visited_cs]): continue # passed all checks. Will switch to new CS # Compute first break pt of charging function above the SOC with which we arrived first_k = 0 if soc_at_cs < 0: raise ValueError("Supporting point has negative energy") while first_k < cs_n_pts and cs_supp_pts[1][first_k] <= soc_at_cs: first_k += 1 e_to_end = self.min_energy_consumed_after_node[curr_local_id] # the ones from first_k to the end, plus the arrival point n_pts_new = cs_n_pts - first_k + 1 supp_pts_new = [ [None for k in range(n_pts_new)] for _ in range(2)] # if there is more than one supp pt, compute slopes of segments, otherwise, it's None slope_new = [cs_slope[l-1] for l in range(first_k, cs_n_pts)] if n_pts_new > 1 else None supp_pts_new[0][0] = trip_time supp_pts_new[1][0] = soc_at_cs # compute time to charge shift_time = self.instance.get_time_to_charge_from_zero( curr_node, soc_at_cs) for supp_pt_idx in range(first_k, cs_n_pts): supp_pts_new[0][supp_pt_idx-first_k+1] = trip_time + \ cs_supp_pts[0][supp_pt_idx]-shift_time supp_pts_new[1][supp_pt_idx-first_k + 1] = cs_supp_pts[1][supp_pt_idx] # compute key if soc_at_cs > e_to_end: key_time = trip_time + \ self.min_travel_time_after_node[curr_local_id] else: key_time = trip_time + \ self.min_travel_charge_time_after_node[curr_local_id] - \ soc_at_cs/self.max_slope # make new label label_list.append(core.NodeLabel(curr_local_id, key_time, trip_time, curr_local_id, curr_label.soc_arr_to_last_cs, curr_label.energy_consumed_since_last_cs, supp_pts_new, slope_new, 0, 0, curr_label.parent) ) # not a CS. just return the current label else: label_list.append(curr_label) return label_list
def _compute_supporting_points(self, label: core.NodeLabel) -> core.NodeLabel: """Provides a new label that is identical to the argument, but with the supporting points, slopes, and y-intercepts updated. """ if label.time_last_arc == 0 and label.energy_last_arc == 0: return label local_id = label.node_id_for_label supp_pts = label.supporting_pts n_pts = label.get_num_supp_pts() new_supp_pts_temp = [[None for _ in range(n_pts)] for _ in range(2)] # last supporting point index to consider last_k = -1 # need to compute first supporting point compute_first_point = False for k in range(n_pts): new_supp_pts_temp[0][k] = supp_pts[0][k] + label.time_last_arc new_supp_pts_temp[1][k] = supp_pts[1][k] - label.energy_last_arc # stop at first supp pt with: # more SOC than needed or # the one that leads to violation of route duration limit if (last_k == -1 and (new_supp_pts_temp[1][k] > self.max_energy_at_departure[local_id] or new_supp_pts_temp[0][k] > self.latest_departure_time[local_id])): last_k = k break # if we don't need to restrict final supp pt index if last_k == -1: last_k = n_pts # determine first supp pt to consider first_k = 0 while (first_k < last_k and new_supp_pts_temp[1][first_k] < self.min_energy_at_departure[local_id] ): first_k += 1 # if pts are on a single segment, just consider one valid point if first_k == last_k: if first_k in (0, n_pts): # no feasible supporting pts, which shouldn't happen raise ValueError( f'No feasible supporting points for label\n{label}') # first and last supp pt may be on single segment between two pts compute_first_point = True # need to compute first point if first_k != 0 if first_k != 0: compute_first_point = True first_k -= 1 # increment one since pts will be considered from first_k to last_k-1 if last_k != n_pts: last_k += 1 # instantiate things we'll set for the next label slope_now = None y_int_now = None new_supp_pts = None # make new supporting points if not compute_first_point: # all supp pts (up to those with index >= last_k) are feasible new_supp_pts = [[new_supp_pts_temp[i][k] for k in range(last_k)] for i in range(2)] # if label not reduced to singleton if label.slope is not None: n_pts_rmd = n_pts - last_k # number of removed points from the soc function # number of segments previously n_seg_initial = len(label.slope) n_seg_now = n_seg_initial - n_pts_rmd # number of segments now # if number of segments is >0, compute slope and y-intercept if n_seg_now > 0: slope_now = [label.slope[k] for k in range(n_seg_now)] y_int_now = [(label.y_intercept[k]-label.energy_last_arc - label.time_last_arc*label.slope[k]) for k in range(n_seg_now)] # need to compute first supp pt on seg [first_k, first_k+1] else: n_pts_now = last_k - first_k new_supp_pts = [[None for k in range(n_pts_now)] for _ in range(2)] # compute slope and y-intercept (which must be shifted) of current segment slope = label.slope[first_k] y_int = label.y_intercept[first_k] - label.energy_last_arc - \ label.time_last_arc * label.slope[first_k] trip_time = (self.min_energy_at_departure[local_id] - y_int)/slope new_supp_pts[0][0] = trip_time new_supp_pts[1][0] = self.min_energy_at_departure[local_id] for k in range(first_k+1, last_k): new_supp_pts[0][k-first_k] = new_supp_pts_temp[0][k] new_supp_pts[1][k-first_k] = new_supp_pts_temp[1][k] # if num segments in label is > 0, compute slope and y-intercept if n_pts_now > 0: slope_now = [label.slope[k] for k in range(first_k, last_k-1)] y_int_now = [(label.y_intercept[k] - label.energy_last_arc - label.time_last_arc*label.slope[k]) for k in range(first_k, last_k-1)] # construction complete. return new label return core.NodeLabel(local_id, label.key_time, label.trip_time, label.last_visited_cs, label.soc_arr_to_last_cs, label.energy_consumed_since_last_cs, new_supp_pts, slope_now, 0, 0, label.parent, y_int_now)
def _relax_arc(self, curr_label: core.NodeLabel, next_node_local_id: int, energy_arc: float, time_arc: float ) -> core.NodeLabel: """Returns the label built by the extension of curr_label to the node given by next_node_local_id.""" max_q = self.instance.max_q # going to modify the following new_e_consumed_since_last_cs = None new_soc_at_last_cs = None new_last_visited = None # modifications if curr_label is a charging station if self.nodes_gpr[curr_label.node_id_for_label].type == core.NodeType.CHARGING_STATION: new_e_consumed_since_last_cs = energy_arc new_last_visited = curr_label.node_id_for_label new_soc_at_last_cs = curr_label.get_first_supp_pt_soc() # modifications if cust or depot else: new_e_consumed_since_last_cs = curr_label.energy_consumed_since_last_cs + energy_arc new_last_visited = curr_label.last_visited_cs new_soc_at_last_cs = curr_label.soc_arr_to_last_cs # compute max soc reachable at next node max_soc_at_next = curr_label.get_last_supp_pt_soc() - energy_arc # if it's not enough, then we don't extend if max_soc_at_next < self.min_energy_at_departure[next_node_local_id]: return None # compute min soc reachable or needed at next node min_soc_at_next = max(self.min_energy_at_departure[next_node_local_id], new_soc_at_last_cs - new_e_consumed_since_last_cs) # determine if we need to charge at the last visited CS to reach # next node with sufficient SOC e_to_charge_at_last_cs = max(0, self.min_energy_at_departure[next_node_local_id] + new_e_consumed_since_last_cs - new_soc_at_last_cs) trip_time = curr_label.trip_time + time_arc min_time = trip_time # if we need to charge some energy at last visited CS if e_to_charge_at_last_cs > 0: # if there is no prev CS, we can't charge, so it's impossible to extend if new_last_visited is None: return None # if it requires more charge than can be acquired, it's impossible to extend if new_soc_at_last_cs + e_to_charge_at_last_cs > max_q: return None # compute time needed to retroactively charge cs_node = self.nodes_gpr[new_last_visited] charging_time = self.instance.get_charging_time(cs_node, new_soc_at_last_cs, e_to_charge_at_last_cs) min_time += charging_time # if min time of departure is larger than allowed, it's impossible to extend if min_time > self.latest_departure_time[next_node_local_id]: return None # compute key time key_time = None # if we don't need to charge after the next node if min_soc_at_next > self.min_energy_consumed_after_node[next_node_local_id]: key_time = min_time + \ self.min_travel_time_after_node[next_node_local_id] # if we will need to charge else: key_time = (min_time + self.min_travel_charge_time_after_node[next_node_local_id] - min_soc_at_next/self.max_slope) # return a new label for the relaxation to the new node return core.NodeLabel(next_node_local_id, key_time, trip_time, new_last_visited, new_soc_at_last_cs, new_e_consumed_since_last_cs, curr_label.supporting_pts, curr_label.slope, time_arc, energy_arc, curr_label, y_intercept=curr_label.y_intercept)