コード例 #1
0
class SimulatorClass(object):
    def __init__(self,
                 nr_cars: int,
                 data_reader: DataReader,
                 savecars: str = None,
                 loadcars: str = None,
                 **kwargs):
        """ Class for dynamic simulation

        :param nr_cars:         Number of cars to simulate
        :param data_reader:     Instance of DataReader class
        :param savecars:        Path for saving the generated cars
        :param loadcars:        Path for loading the cars
        """

        # Attributes for some stats and real time plots
        self.veh_total_distance = 0
        self.veh_service_distance = 0
        self.veh_total_empty_distance = 0
        self.simulation_delay = {0: 0.0}
        self.PickupDelay = []
        self.MeanDelay = [0.0]
        self.settings: Settings = data_reader.settings

        # Setup the data reader and get the cars
        self.data_reader: DataReader = data_reader
        self.router: AbstractRouter = self.data_reader.router
        self.reqs_gen = None
        self.scale_factor_generator: tp.Optional[tp.Generator] = None

        # Cars
        self.cars_list: tp.List[
            isc.MovingObject] = self.data_reader.generate_cars(
                nr_cars, savecars, loadcars)
        self.nr_cars = len(self.cars_list)
        self.carbyid: tp.Dict[str, isc.Car] = {
            car.ID: car
            for car in self.cars_list
        }

        # A car can belong to a single group at a time
        self.cars_at_visitables_dict: tp.Dict[str,
                                              NamedSet[isc.MovingObject]] = {}
        self.cars_repositioning: NamedSet[isc.MovingObject] = NamedSet(
            name="Repositioning Vehicles")
        self.cars_serving_customers: NamedSet[isc.MovingObject] = NamedSet(
            name="Vehicles Serving Customers")
        self.cars_idle_scheduled: NamedSet[isc.MovingObject] = NamedSet(
            self.cars_list, name="Idle Vehicles with Schedule")
        self.cars_idle: NamedSet[isc.MovingObject] = NamedSet(
            self.cars_list, name="Idle Vehicles")
        self.cars_idle.count = len(self.cars_list)

        # Dictionary for label of the set to which car belings to; initially all are Idle
        self.car_label_dict: tp.Dict[isc.MovingObject, NamedSet[isc.MovingObject]] = \
            {car: self.cars_idle for car in self.cars_list}
        # Record the travelled distance of each vehicle as dict
        car_key_dict: tp.Dict[str,
                              float] = {car.ID: 0
                                        for car in self.cars_list}
        self.car_miles = {
            "pickup_distance": car_key_dict.copy(),
            "in_car_distance": car_key_dict.copy(),
            "service_distance": car_key_dict.copy()
        }

        # Attributes for tracking visitable objects, these objects are "time-invariant" meaning they don't get expired
        # over time
        self.visitables_by_id: tp.Dict[str, isc.Visitable] = {}
        self.visitables_list: tp.List[isc.Visitable] = []

        # Attributes for keeping track of servable with time window objects
        self.stw_list: tp.List[isc.ServableTW] = [
        ]  # List to maintain order of generation
        self.stw_label_dict: tp.Dict[isc.ServableTW,
                                     NamedSet[isc.ServableTW]] = {}
        self.unscheduled_stw: NamedSet[isc.ServableTW] = NamedSet(
            name="unscheduled_stw")
        self.scheduled_stw: NamedSet[isc.ServableTW] = NamedSet(
            name="scheduled_stw")
        self.serving_stw: NamedSet[isc.ServableTW] = NamedSet(
            name="serving_stw")
        self.expired_stw: NamedSet[isc.ServableTW] = NamedSet(
            name="expired_stw")
        self.fulfilled_stw: NamedSet[isc.ServableTW] = NamedSet(
            name="fulfilled_stw")
        self.recent_expired_stw: tp.List[isc.ServableTW] = []
        # visitable object ID to car dictionary for which car fulfilled which request
        self.fulfilled_by: tp.Dict[str, isc.MovingObject] = {}
        # Number of times a ServableWithTimeWindows has been considered for optimization
        self.nrReqsOptims: tp.Dict[str:int] = {}
        self.nr_all_reqs = 0

        # A dictionary of all stationary objects by their ids
        self.stationary_by_ids: tp.Dict[str, isc.StationaryObject] = {}

        if self.settings.DEBUG is True:
            # list of tuples data_readers.reqnode object, the time of arrival and passenger count
            self.carhistroy: tp.Dict[isc.MovingObject, tp.List[tp.Tuple[isc.PathNode, float, int]]] = \
                {car: [] for car in self.cars_list}
            # cars original positions
            self.car_initial_positions: tp.Dict[isc.MovingObject,
                                                isc.Point] = {
                                                    car: car.orig
                                                    for car in self.cars_list
                                                }

        self.process_pool = None

        self.passed_timedelta: tp.Optional[timedelta] = None
        self.vehicle_emulator: tp.Optional[VehicleEmulator] = None

        self.logger = None
        self.results_folder = None
        self.history = None
        self._graph_process: tp.Optional[LivePlot] = None
        self._log_thread = None
        self._log_queue = None
        self._manager = None
        self.enddt_backup_path: tp.Optional[Path] = None

    def __getstate__(self):
        attrbs = vars(self).copy()
        generators = [
            x for x, value in attrbs.items()
            if isinstance(value, types.GeneratorType)
        ]
        [
            attrbs.pop(x) for x in [
                "_manager", "_log_queue", "_log_thread", "_graph_process",
                "history", "logger", "process_pool", "settings"
            ] + generators
        ]
        return attrbs

    def __setstate__(self, state):
        for name, value in state.items():
            setattr(self, name, value)
        [
            setattr(self, x, None) for x in [
                "_manager", "_log_queue", "_log_thread", "_graph_process",
                "logger", "process_pool"
            ]
        ]
        self.settings = self.data_reader.settings

    @classmethod
    def from_pickled(cls, filepath: tp.Union[str, Path]):
        """ Creates the SimulatorClass from pickled file

        :param filepath:    File path of the pickled file
        """

        with open(Path(filepath), 'rb') as f:
            simulator: SimulatorClass = load(f)
        return simulator

    def _create_backup(self):
        """ Creates backup of the current state in the results folder
        :return: filename of the backup file
        """

        folder = Path(self.results_folder, "backups")
        if not folder.exists():
            folder.mkdir()
        filename = "simulator_backup_{}.pickle".format(
            self.data_reader.actual_time.strftime("%Y-%m-%d %H-%M-%S"))
        with open(Path(folder, filename), "wb") as f:
            dump(self, f)
        return Path(folder, filename)

    def run(self,
            results_folder=None,
            live_plot=True,
            live_plot_class=None,
            save_live_plot=False,
            save_frames=False,
            log_output=False,
            save_results=True,
            progress_bar=True,
            tqdm_position=0,
            pre_bar_text="",
            resume_state=False,
            simulate_cooldown=True):
        """ Basic method for starting the simulation

        :param results_folder:  full path where simulation results are to be written
        :param live_plot:       whether to plot realtime plot or not. A separate process is used for realtime plots.
                                Default is True
        :param save_live_plot:  save the live plot as movie instead of showing plots in realtime. The live_plot
                                parameter must be True for it to work
        :param save_frames:     save the frames of each time step and make movie from it at the end. It does not need
                                separate process, and thus, can be used in jupyter notebook as well
        :param live_plot_class: A customized subclass of LivePlot used for plotting data
        :param log_output:      If True, a log file in generated in the results folder. Default is False
        :param save_results:    Save results to files. Default is True.
        :param progress_bar:    print the tqdm based progress bar. Default True.
        :param resume_state:    Resume from last state
        :param simulate_cooldown:    Finish already assigned trips at the end when all requests have been generated.
                                This runs the simulation for more time than enddt to finish the lastly assigned trips.
        :return:                History class object for the simulation
        """

        try:
            if save_results is True:
                self.results_folder = self.__get_results_folder(results_folder)
            self.initialize_run(live_plot, save_live_plot, log_output,
                                live_plot_class, save_frames, resume_state)
            self.__main_simulation_loop(progress_bar, save_frames,
                                        tqdm_position, pre_bar_text,
                                        resume_state, simulate_cooldown)
            self._end_simulation(live_plot, save_frames)
            if save_results is True:
                self.history.write_to_file(self.results_folder)
            return self.history
        except Exception as e:
            if logging.getLogger().hasHandlers() is True:
                logging.getLogger().exception(
                    "An error occurred while running the simulator")
            self._end_simulation(live_plot, save_frames)
            raise e

    def initialize_generator_functions(self, resume_state):
        self.reqs_gen = self.data_reader.dynamic_requests(skip_initial=True)
        if self.settings.window_time_matrix_scale != 0:
            self.scale_factor_generator = self.data_reader.calculate_time_factor(
                self.settings.window_time_matrix_scale)

    def initialize_run(self, live_plot, save_live_plot, log_output,
                       live_plot_class, save_frames, resume_state):
        """ Initializes some of the settings dependent class attributes and required processes and threads """

        if self.results_folder is not None:
            self.settings.update({
                "startdt": str(self.data_reader.startdt),
                "enddt": str(self.data_reader.enddt),
                "data_file": self.data_reader.data_file_path,
                "nr_cars": self.nr_cars
            })
            self.settings.export_to_file(
                os.path.join(self.results_folder, "simulator_settings.yaml"))

        # Thread based logger
        if log_output is True:
            from pymodsim.simulator.logger_setup import listener_process, worker_configurer
            from threading import Thread
            from queue import Queue

            self._log_queue = Queue()
            self._log_thread = Thread(target=listener_process,
                                      args=(
                                          self._log_queue,
                                          self.results_folder,
                                      ))
            self._log_thread.start()
            worker_configurer(self._log_queue)
        self.logger = logging.getLogger()

        self.history = History(self.settings.to_dict())
        plot_class = LivePlot if live_plot_class is None else live_plot_class
        if live_plot is True:
            SyncManager.register("History", History)
            self._manager = SyncManager()
            self._manager.start()
            self.history = self._manager.History(self.settings.as_dict())
            self._graph_process = plot_class(self.history,
                                             self.settings.as_dict(),
                                             self.results_folder,
                                             save_live_plot)
            self._graph_process.start()
        elif save_frames is True:
            self._graph_process = plot_class(self.history,
                                             self.settings.as_dict(),
                                             self.results_folder,
                                             save_live_plot)

        self.initialize_generator_functions(resume_state)

        # Read the fixed visitable objects that don't change during simulation
        if self.settings.stations_file_path and resume_state is False:
            self.visitables_list = self.data_reader.generate_visitable_objects(
                self.settings.stations_file_path)
            self.visitables_by_id: tp.Dict[str, isc.ServiceStation] = \
                {visitable.ID: visitable for visitable in self.visitables_list}
            visitable_dict = defaultdict(list)
            for v in self.visitables_list:
                visitable_dict[type(v).__name__].append(v)
            self.cars_at_visitables_dict = {
                s: NamedSet(name="Vehicles at {}".format(s))
                for s in visitable_dict.keys()
            }
            self.stationary_by_ids.update(self.visitables_by_id.copy())
            self.history.set_attrb_by_name("fixed_visitables",
                                           dict(visitable_dict))

    def _end_simulation(self, live_plot, save_frames):
        logger = logging.getLogger()
        logger.info("Ending Simulation")
        self.history.set_attrb_by_name("stop_animation", True)
        self.history.set_attrb_by_name("summary", self.get_summary_dict())
        if self._log_thread is not None:
            logger.handlers = []
            # Send signal to logger for ending the while loop
            self._log_queue.put_nowait(None)
            self._log_thread.join()
        if live_plot is True:
            self._graph_process.join()
            self.history = self.history._getvalue()
            self._manager.shutdown()
        if save_frames is True:
            self._graph_process.make_movie_from_images(2)

    def get_summary_dict(self):
        return OrderedDict({
            "Total Vehicles": len(self.cars_list),
            "Total Reqs": self.nr_all_reqs,
            "Total Expired": self.expired_stw.count,
            "Total Served": self.fulfilled_stw.count,
            "Total Service Distance": self.veh_service_distance,
            "Total Empty Distance": self.veh_total_empty_distance,
            "Total Distance": self.veh_total_distance
        })

    def __main_simulation_loop(self, progress_bar, save_frames, tqdm_position,
                               pre_bar_text, resume_state, simulate_cooldown):
        def single_iteration():
            self.passed_timedelta += timedelta(
                seconds=self.settings.synchronous_batching_period)
            self.vehicle_emulator.update(self.passed_timedelta)
            if self.settings.window_time_matrix_scale != 0:
                self.refactor_time_matrix()
            end_simulation = self.update_odm_controller()
            # assign stationary vehicles to next locations
            if len(self.vehicle_emulator.stationary_cars) > 0:
                self.send_vehicles(self.vehicle_emulator.stationary_cars)
            self.update_real_time_data()
            return end_simulation

        if resume_state is False:
            self.logger.info(
                "Starting new simulation with startdt: {}, enddt: {}".format(
                    self.data_reader.startdt, self.data_reader.enddt))
            self.passed_timedelta = timedelta()
            self.vehicle_emulator = VehicleEmulator(self.cars_list,
                                                    self.passed_timedelta,
                                                    self.router)
            total_iterations = int((self.data_reader.enddt -
                                    self.data_reader.startdt).total_seconds() /
                                   self.settings.synchronous_batching_period)
            # initial scaling of the travel time factor
            if self.settings.window_time_matrix_scale != 0:
                self.router.reset_time_factor()
                self.refactor_time_matrix()
        else:
            self.logger.info(
                "Resuming simulation from time: {} to enddt: {}".format(
                    self.data_reader.actual_time, self.data_reader.enddt))
            total_iterations = int(
                (self.data_reader.enddt -
                 self.data_reader.actual_time).total_seconds() /
                self.settings.synchronous_batching_period)

        if progress_bar is True:
            with tqdm(total=total_iterations, position=tqdm_position) as pbar:
                while True:
                    end_simulation = single_iteration()
                    if save_frames is True:
                        self._graph_process.save_single_plot(
                            self.data_reader.actual_time)
                    pbar.update(1)
                    pbar.set_description(
                        str(pre_bar_text) + "_" +
                        str(self.data_reader.actual_time))
                    post_dict = OrderedDict({
                        "all_reqs":
                        self.nr_all_reqs,
                        "expired":
                        self.expired_stw.count,
                        "fulfilled":
                        self.fulfilled_stw.count,
                        "scheduled":
                        len(self.scheduled_stw),
                        "serving":
                        len(self.serving_stw),
                        "idle_cars":
                        len(self.cars_idle)
                    })
                    if self.settings.DEBUG is True:
                        post_dict.update({
                            "sim_delay":
                            np.mean(
                                np.array(list(self.simulation_delay.values())))
                        })
                    pbar.set_postfix(post_dict)
                    if self.enddt_backup_path is None and self.data_reader.actual_time >= self.data_reader.enddt:
                        self.enddt_backup_path = self._create_backup()
                        if simulate_cooldown is False:
                            break
                    if end_simulation is True:
                        break
        else:
            while True:
                if single_iteration() is True:
                    break

    def refactor_time_matrix(self):
        current_time = self.passed_timedelta.total_seconds()
        if current_time % self.settings.window_time_matrix_scale == 0:
            try:
                self.router.time_factor = next(self.scale_factor_generator)
                self.vehicle_emulator.update_time_factor(
                    self.router.time_factor)
                # Update the schedules of currently assigned moving objects paths
                for car in self.cars_serving_customers.union(
                        self.cars_repositioning):
                    reach_time = self.vehicle_emulator.car_routes[
                        car].reach_time
                    car.serving_node.reach_time = reach_time
                    car.recalculate_path_reach_times(current_time,
                                                     self.settings,
                                                     self.router)
                self.history.add_info(
                    "time_factor", {
                        "ActualTime": str(self.data_reader.actual_time),
                        "Router Time Factor": self.router.time_factor
                    })

            except StopIteration:
                return

    def __get_results_folder(self, results_folder):
        if results_folder is None:
            results_folder = Path("Results", "result_" + str(self.nr_cars))
        # if summary file exist in folder, then use another folder
        i = 0
        while Path(results_folder, "summary.csv").exists():
            i += 1
            results_folder = results_folder.joinpath("_{}".format(i))
        results_folder = Path(results_folder)
        if not results_folder.exists():
            results_folder.mkdir(parents=True)
        return results_folder

    def __mark_servable_wtw(self, servables: tp.Set[isc.ServableTW],
                            new_set: NamedSet[isc.ServableTW]):
        """ function for marking the ServableWithTimeWindows to a specific new_set set. The new servables are by
        defaults are placed in self.unscheduled_stw
        """
        if len(servables) > 0:
            for servable in servables:
                old_set = self.stw_label_dict[servable]
                if old_set.name == self.serving_stw.name:
                    assert new_set.name not in {
                        "scheduled_stw", "unscheduled_stw", "expired_stw"
                    }
                old_set.remove(servable)
                old_set.count -= 1
                self.stw_label_dict[servable] = new_set
            if new_set.name == "expired_stw":
                self.recent_expired_stw.extend(
                    sorted(servables, key=lambda x: x.orig_window[0]))
            new_set.count += len(servables)
            # If the debug mode is off, then don't keep references to expired and fulfilled requests
            if self.settings.DEBUG is False and new_set.name in {
                    "expired_stw", "fulfilled_stw"
            }:
                # also remove reference to the request to free memory
                self.stw_list = [
                    x for x in self.stw_list if x not in servables
                ]
                for servable in servables:
                    del self.stationary_by_ids[servable.ID]
                    del self.stw_label_dict[servable]
            else:
                new_set.update(servables)

    def __mark_car(self, car: isc.MovingObject,
                   new_set: NamedSet[isc.MovingObject]):
        """ function for marking list of cars to a new sets"""
        old_set = self.car_label_dict[car]
        old_set.count -= 1
        old_set.remove(car)
        self.car_label_dict[car] = new_set
        new_set.add(car)
        new_set.count += 1

    def __assign_moving_objects_to_set(self, cars: tp.Set[isc.MovingObject]):
        for car in cars:
            if car.serving_node is None:
                if len(car.path) > 0:
                    self.__mark_car(car, self.cars_idle_scheduled)
                else:
                    self.__mark_car(car, self.cars_idle)
            elif isinstance(car.serving_node.stationary_object, isc.Visitable):
                st_object = car.serving_node.stationary_object
                self.__mark_car(
                    car,
                    self.cars_at_visitables_dict[type(st_object).__name__])
            elif isinstance(car.serving_node.stationary_object,
                            isc.CustomerRequest):
                self.__mark_car(car, self.cars_serving_customers)
            elif isinstance(car.serving_node.stationary_object,
                            isc.RepositioningRequest):
                self.__mark_car(car, self.cars_repositioning)
            else:
                raise ValueError("Unknown stationary object type {} for "
                                 "marking the cars".format(
                                     type(car.serving_node.stationary_object)))

    def __test_customer_sets(self):
        " Assertion tests for the consistency of all the customer request sets"
        assert len(self.scheduled_stw) + len(self.unscheduled_stw) + len(self.serving_stw) + \
               self.fulfilled_stw.count + self.expired_stw.count == self.nr_all_reqs, \
            " ".join(["Count Failed: "] + [str(len(x)) for x in [self.scheduled_stw,
                                                                 self.unscheduled_stw,
                                                                 self.serving_stw,
                                                                 self.fulfilled_stw,
                                                                 self.expired_stw]] + str(self.nr_all_reqs))
        assert len(self.serving_stw.intersection(self.scheduled_stw)) == 0

        # test if the requests in car paths and scheduled set are same, and that some request is not scheduled for
        #   more than one car
        scheduled_stw = set()
        for car in self.cars_list:
            for node in car.path:
                node_object = node.stationary_object
                if isinstance(node_object, isc.ServableTW):
                    if node.geographical_point == node_object.locations[0]:
                        assert node_object not in scheduled_stw, "same request is scheduled for multiple cars. " \
                                                            "{}: {},  node={}".format(type(car), car, node)
                        scheduled_stw.add(node_object)
        assert len(scheduled_stw.difference(self.scheduled_stw)) == 0, "Some requests in vehicle path " \
                                                                  "not found in scheduled requests set"

        reqs = self.unscheduled_stw.union(self.scheduled_stw)
        for car in self.cars_list:
            if car.serving_node is not None:
                assert car.serving_node.stationary_object not in reqs, " Serving Request found in other sets"

    def merge_batch_solution(self, paths_dict: tp.Dict[isc.MovingObject,
                                                       tp.List[isc.Point]],
                             matched_servables: tp.Set[isc.ServableTW],
                             unmatched_servables: tp.Set[isc.ServableTW],
                             next_loop_servables: tp.Set[isc.ServableTW]):
        """ Merges the batch optimization solution into the simulation

        :param paths_dict:              The batch solution dictionary with the duplicated cars as keys and list of
                                        points or path as values
        :param matched_servables:       The servables that have been assigned to a vehicle
        :param unmatched_servables:     The servables that could not be assigned to any vehicle
        :param next_loop_servables:     The servables whose decisions is postponed for now. They will not be marked as
                                        expired. Thus included in the next call to optimization
        """
        def unequal_intersection(set1: tp.Set[T],
                                 set2: tp.Set[T]) -> tp.Set[T]:
            """ Intersection with preference given to first set elements"""
            return {element for element in set1 if element in set2}

        current_time = self.passed_timedelta.total_seconds()
        original_moveables = {self.carbyid[car.ID] for car in paths_dict}

        # remove rescheduled servables from older cars
        rescheduled = unequal_intersection(matched_servables,
                                           self.scheduled_stw)
        # Also remove servables that were previously scheduled but now not assigned to anyone
        total_remove = unequal_intersection(set(unmatched_servables),
                                            self.scheduled_stw)
        rescheduled = total_remove.union(rescheduled)

        removal_dict: tp.Dict[isc.MovingObject,
                              tp.Set[isc.ServableTW]] = defaultdict(set)
        for servable_copy in rescheduled:
            servable_original = self.stationary_by_ids[servable_copy.ID]
            assert isinstance(
                servable_original,
                (isc.Servable,
                 isc.ServableTW)), "Non-Servable for rescheduling"
            if servable_copy.serving_moving_object != servable_original.serving_moving_object:
                original_car = self.carbyid[
                    servable_original.serving_moving_object.ID]
                servable_original.serving_moving_object = None
                self.__mark_servable_wtw({servable_original},
                                         self.unscheduled_stw)
                removal_dict[original_car].add(servable_original)

        # Only remove servables and recalculate for moving objects whose schedule will not be recalculated later
        for car in set(removal_dict.keys()).difference(original_moveables):
            car.remove_object_from_path(current_time, removal_dict[car],
                                        self.settings, self.router)

        # Recalculate the schedule
        servables_for_mark = set()
        fulfilled_serving = self.serving_stw.union(self.fulfilled_stw)

        for car, path_points in paths_dict.items():
            original_car = self.carbyid[car.ID]
            if original_car.serving_node is not None:
                serving_object = {original_car.serving_node.stationary_object}
            else:
                serving_object = set()
            # remove the points that have been visited already, except the already serving one for the
            #   current moving object

            new_path = [
                point for point in path_points
                if self.stationary_by_ids[point.associated_object_id] not in
                fulfilled_serving.difference(serving_object)
            ]
            # change the node's stationary objects with the original objects
            for i, point in enumerate(new_path):
                stationary_object = self.stationary_by_ids[
                    point.associated_object_id]
                if isinstance(stationary_object,
                              (isc.ServableTW, isc.Servable)):
                    stationary_object.serving_moving_object = original_car
                    servables_for_mark.add(stationary_object)
                original_car.add_to_path(stationary_object, 0, point)
            servables_for_mark.difference_update(serving_object)
            assert len(original_car.path) == len(set(original_car.path)), "duplicate nodes found in new " \
                                                                          "path = {}".format(new_path)
            original_car.recalculate_path_reach_times(current_time,
                                                      self.settings,
                                                      self.router)

        self.__assign_moving_objects_to_set(
            original_moveables.union(removal_dict.keys()))
        self.__mark_servable_wtw(servables_for_mark, self.scheduled_stw)
        unmatched_expired = set()
        # Mark requests that have been considered maximum allowed number of times as expired
        if self.settings.max_times_unmat_opt is not None:
            unmatched_expired = {
                self.stationary_by_ids[r.ID]
                for r in unmatched_servables
                if self.nrReqsOptims[r.ID] >= self.settings.max_times_unmat_opt
            }
        unmatched_expired = {
            r
            for r in unmatched_expired if r not in next_loop_servables
        }
        self.__mark_servable_wtw(unmatched_expired, self.expired_stw)
        self.__test_customer_sets()

    def send_vehicles(self, moving_objects: tp.Set[isc.MovingObject]):
        current_time = self.passed_timedelta.total_seconds()
        cars_send_dict = {}
        for car in moving_objects:
            lastnode = car.serving_node
            node_reach_time = self.vehicle_emulator.point_reached_time[car]
            covered_distance = 0

            # First store the necessary updates from last node served
            if lastnode is not None and str(
                    lastnode.geographical_point) not in self.simulation_delay:
                self.simulation_delay.update({
                    str(lastnode.geographical_point):
                    node_reach_time - lastnode.reach_time
                })
                if self.settings.DEBUG:
                    self.carhistroy[car].append(
                        (lastnode, node_reach_time, car.nr_current_onboard))
                if abs(node_reach_time - lastnode.reach_time) > 1:
                    self.logger.error("-ve sim delay, timedelta: " +
                                      str(self.passed_timedelta) + " car: " +
                                      str(car) + " point: " + str(lastnode) +
                                      " simulation_delay: " +
                                      str(node_reach_time -
                                          lastnode.reach_time))
                if isinstance(lastnode.stationary_object, isc.CustomerRequest):
                    if lastnode.geographical_point == lastnode.stationary_object.orig:
                        self.PickupDelay.append(
                            max(
                                0, node_reach_time -
                                lastnode.stationary_object.orig_window[0]))
                    else:
                        self.__mark_servable_wtw({lastnode.stationary_object},
                                                 self.fulfilled_stw)

                _, covered_distance = self.vehicle_emulator.get_time_distance_route(
                    car)

                # Record history
                self.history.add_info(
                    "trip_info", {
                        "CurrentTime":
                        current_time,
                        "ActualTime":
                        str(self.data_reader.actual_time),
                        "Arrival Time":
                        node_reach_time,
                        "Previous Estimated Arrival Time":
                        lastnode.reach_time,
                        "moving object":
                        car.ID,
                        "moving object type":
                        type(car).__name__,
                        "stationary object":
                        str(lastnode.stationary_object),
                        "stationary object type":
                        type(lastnode.stationary_object).__name__,
                        "nr of times optimized":
                        self.nrReqsOptims.get(lastnode.stationary_object.ID,
                                              None)
                    })
            next_point, move_time = car.reached_serving_node(
                current_time, node_reach_time, covered_distance, self.settings)
            if next_point is not None:
                self.logger.info(
                    "timedelta: {} sending {} from {} to {}".format(
                        self.passed_timedelta, car, car.orig,
                        car.serving_node))
                cars_send_dict.update({car: (next_point, move_time)})
                if isinstance(car.serving_node.stationary_object,
                              isc.ServableTW):
                    self.__mark_servable_wtw(
                        {car.serving_node.stationary_object}, self.serving_stw)
        self.__test_customer_sets()
        self.__assign_moving_objects_to_set(moving_objects)

        # Send vehicles
        if len(cars_send_dict) > 0:
            sendcars, destination_and_movetime = list(
                zip(*cars_send_dict.items()))
            destinations, move_times = zip(*destination_and_movetime)
            self.vehicle_emulator.send_cars_to_points(sendcars, destinations,
                                                      move_times)

    def update_real_time_data(self):
        # Update data for the plots after every self.settings.plotrate seconds
        timedelta_sec = self.passed_timedelta.total_seconds()

        # Update the total vehicle miles
        self.veh_total_distance = sum(car.total_distance_covered
                                      for car in self.carbyid.values())
        self.veh_service_distance = sum(car.service_distance_covered
                                        for car in self.carbyid.values())
        self.veh_total_empty_distance = sum(car.empty_distance_covered
                                            for car in self.carbyid.values())

        # Record the data for later plots
        real_time_info = {
            "TimeDelta":
            str(self.passed_timedelta),
            "ActualTime":
            str(self.data_reader.actual_time),
            "ScheduledReqs":
            len(self.scheduled_stw),
            "ServingReqs":
            len(self.serving_stw),
            "ExpiredReqs":
            self.expired_stw.count,
            "UnscheduledReqs":
            len(self.unscheduled_stw),
            "TotalReqs":
            self.nr_all_reqs,
            "ServiceVehicleDistance":
            self.veh_service_distance,
            "TotalEmptyDistance":
            self.veh_total_empty_distance,
            "TotalVehicleDistance":
            self.veh_total_distance,
            "PickUpDelay":
            np.mean(np.array(self.PickupDelay)) /
            60 if self.PickupDelay else 0.0
        }
        real_time_info.update({
            "{}Vehicles".format(key): len(named_set)
            for key, named_set in self.cars_at_visitables_dict.items()
        })
        self.history.add_info("realtime_info", real_time_info)

        # Reset the accumulated delays
        self.PickupDelay = []

        # Record vehicle miles
        self.history.set_attrb_by_name("vehicle_miles", self.car_miles)

        # Update vehicle locations
        all_vehicle_sets = [self.cars_idle, self.cars_serving_customers, self.cars_repositioning] + \
                           list(self.cars_at_visitables_dict.values())
        vehicle_positions = {car_set.name: [] for car_set in all_vehicle_sets}

        [
            vehicle_positions[car_set.name].append(
                self.vehicle_emulator.car_locations[car].latlon)
            for car, car_set in self.car_label_dict.items()
        ]

        self.history.set_attrb_by_name("vehicle_positions", vehicle_positions)
        # recently expired requests in last 5 minutes
        self.recent_expired_stw = [
            req for req in self.recent_expired_stw
            if req.orig_window[0] > timedelta_sec - 5 * 60
        ]
        expired_pos = [req.orig.latlon for req in self.recent_expired_stw]
        self.history.set_attrb_by_name("recent_expired_requests", expired_pos)

    def __create_optimization_problem(
        self
    ) -> (tp.List[isc.MovingObject], tp.List[isc.ServableTW], tp.Dict[
            str, tp.List[isc.PathNode]], dict):
        current_time = self.passed_timedelta.total_seconds()
        reqs = self.unscheduled_stw.union(self.scheduled_stw)
        submitted_reqs = reqs.copy()

        removed_nodes_dict = defaultdict(tp.List[isc.PathNode])
        submitted_cars = []
        for car in self.cars_list:
            car_copy, removed_nodes = car.get_optimization_copy(
                current_time, self.nrReqsOptims, self.settings)
            if car_copy is not None:
                if len(removed_nodes) > 0:
                    removed_nodes_dict[car.ID] = removed_nodes
                submitted_cars.append(car_copy)
        removed_objects = set().union(*removed_nodes_dict.values())
        removed_objects = {r.stationary_object for r in removed_objects}
        submitted_reqs.difference_update(removed_objects)
        for req in submitted_reqs:
            self.nrReqsOptims[req.ID] += 1
        # Copy the requests and keep the original order of generation
        submitted_reqs = [
            copy(r) for r in self.stw_list if r in submitted_reqs
        ]

        matching_stats = OrderedDict({
            "ActualTime":
            str(self.data_reader.actual_time),
            "NrCars":
            len(submitted_cars),
            "NrReqs":
            len(reqs),
            "NrOriginalFollowups":
            len(self.scheduled_stw),
            "Removed Scheduled Reqs":
            len(removed_objects),
            "Total Considered Reqs":
            len(submitted_reqs)
        })

        return submitted_cars, submitted_reqs, removed_nodes_dict, matching_stats

    def __call_optimization(self):

        if len(self.unscheduled_stw) > 0:
            submitted_cars, submitted_reqs, removed_nodes_dict, matching_stats = self.__create_optimization_problem(
            )

            if len(submitted_reqs) > 0:
                t1 = default_timer()
                visitables = self.visitables_list.copy()
                time_dict, distance_dict = self.router.calculate_dict(
                    submitted_cars,
                    submitted_reqs + visitables,
                    factored_time=True)
                time_matrix_calc_time = default_timer() - t1
                paths_dict, skipped, info_dict = self.solve_assignment_problem(
                    submitted_cars, submitted_reqs, visitables, time_dict,
                    distance_dict)
                matched = set()
                submitted_by_id = {r.ID: r for r in submitted_reqs}
                for car, point_list in paths_dict.items():
                    for pt in point_list:
                        if isinstance(
                                self.stationary_by_ids[
                                    pt.associated_object_id],
                            (isc.Servable, isc.ServableTW)):
                            submitted_servable = submitted_by_id[
                                pt.associated_object_id]
                            submitted_servable.serving_moving_object = car
                            matched.add(submitted_servable)

                matching_stats.update({
                    "NrMatched": len(matched),
                    "TMMatrix": time_matrix_calc_time,
                    "TotalTime": default_timer() - t1
                })
                matching_stats.update(info_dict)
                self.logger.info("Assignment Stats \t " + str(matching_stats))
                self.merge_batch_solution(
                    paths_dict, matched,
                    set(submitted_reqs).difference(matched), skipped)

            self.history.add_info("matching_stats",
                                  matching_stats,
                                  append_to_not_present=True)

    def solve_assignment_problem(
        self, cars: tp.List[isc.MovingObject],
        requests: tp.List[isc.ServableTW], visitables: tp.List[isc.Visitable],
        time_dict: dict, distance_dict: dict
    ) -> (tp.Dict[isc.MovingObject, tp.List[isc.Point]], tp.Set[
            isc.ServableTW], dict):
        """ Method for solving the assignment problem

        Any subclass that wants to implement its own strategy for the assignment problem should override this method.
        By default it uses the simplest nearest neighbor policy

        :param cars: list of moving objects
        :param requests: list of time bounded servable objects
        :param visitables: list of visitable stationary objects
        :param time_dict: travel time dictionary in seconds with point.key as keys
        :param distance_dict: travel distances in meters with point.key as keys
        :return:
                - paths_dict -              Dictionary of lists of Points to visit with MovingObjects as key
                - unmatched_requests -      Set of unmatched servable objects
                - skip_requests -           Set of unmatched servable objects whose decisions is skipped for now.
                                            These servables will be included in the next call
                - info_dict -               Dictionary of any statistical information that should be stored for analysis
        """

        paths_dict = nearest_neighbour(cars, requests, time_dict,
                                       self.settings)
        return paths_dict, set(), {}

    def update_odm_controller(self):

        current_time = self.passed_timedelta.total_seconds()
        self.data_reader.update_time(self.passed_timedelta)

        new_reqs = []
        try:
            new_reqs = next(self.reqs_gen)
        except (StopIteration, RuntimeError):
            if len(self.scheduled_stw) == 0 and len(self.serving_stw) == 0:
                # Stop the simulation
                return True

        if len(new_reqs) > 0:
            self.stw_list.extend(new_reqs)
            self.stationary_by_ids.update({r.ID: r for r in new_reqs})
            self.nr_all_reqs += len(new_reqs)
            self.nrReqsOptims.update({r.ID: 0 for r in new_reqs})
            self.unscheduled_stw.update(new_reqs)
            self.unscheduled_stw.count += len(new_reqs)
            self.stw_label_dict.update(
                {r: self.unscheduled_stw
                 for r in new_reqs})
            for r in new_reqs:
                self.logger.info("Request generated: ID: {}, orig_window: {}, "
                                 "dest_windows: {}".format(
                                     r, r.orig_window, r.dest_window))

        # Mark the requests which are not scheduled and have expired
        expired_stws = {
            r
            for r in self.unscheduled_stw
            if current_time > r.time_windows[0][1]
        }
        self.__mark_servable_wtw(expired_stws, self.expired_stw)

        # call optimization
        self.__call_optimization()

        return False