Example #1
0
    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)
Example #2
0
    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
Example #3
0
    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)
Example #4
0
    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)