def __is_time_to_load_truck(self, truck_id: int) -> bool: time_threshold = copy.copy(self.current_time).add_hours(1).add_minutes(15) truck_capacity = self.trucks[truck_id].package_capacity def __package_needs_delivery(p: Package) -> bool: return p.status is PackageStatus.PENDING or p.status is PackageStatus.READY_FOR_PICKUP packages_to_deliver = list(filter(__package_needs_delivery, self.packages.values())) min_delivery_deadline = time_threshold min_arrival_time = time_threshold assigned_truck_ids = [] for package in packages_to_deliver: # Are the delivery deadlines close enough to the current time that a truck should be loaded if package.delivery_deadline != Time(0) and package.delivery_deadline < min_delivery_deadline: min_delivery_deadline = package.delivery_deadline # Are the arrival times of any packages close enough to wait to load the truck if package.arrival_time != Time(0) and package.arrival_time < min_arrival_time: if package.arrival_time > self.current_time: min_arrival_time = package.arrival_time # What truck ids are packages assigned to if package.truck_id is not None: assigned_truck_ids.append(package.truck_id) # Determine if we can fit all packages on a truck and one or more have a truck already assigned remaining_packages_have_truck = len(packages_to_deliver) <= truck_capacity and len(assigned_truck_ids) > 0 if min_delivery_deadline < time_threshold: return True elif min_arrival_time < time_threshold: return False elif remaining_packages_have_truck and truck_id not in assigned_truck_ids: return False else: return True
def update_state(self, current_time: Time) -> None: # If at Hub wait if self.status is TruckStatus.AT_HUB: return # If out for deliveries if self.status is TruckStatus.OUT_FOR_DELIVERIES and self.next_destination is not None: # If next destination set and the arrival time hasn't occurred, wait if self.arrival_time > current_time: return # Else we have arrived else: # update package status, log package change, and pop id off package_ids debug( f"Delivering Package {self.package_ids[0]} at {current_time}" ) package_id_to_deliver = self.package_ids.pop(0) package_to_deliver = self.hub.packages[package_id_to_deliver] delivery_deadline = package_to_deliver.delivery_deadline # Determine if the delivery is on time or late if delivery_deadline != Time( 0) and delivery_deadline < current_time: package_to_deliver.status = PackageStatus.DELIVERED_LATE else: package_to_deliver.status = PackageStatus.DELIVERED self.hub.log_status(package_to_deliver.id, package_to_deliver.status) # If packages set the next destination and arrival time if len(self.package_ids) > 0: self.__set_next_delivery_destination(current_time) # If out of packages set next destination as Hub else: self.__return_to_hub(current_time)
def __init__(self, status: TruckStatus, miles_traveled: int, route_traveled: List[Location], next_destination: Location, arrival_time: Time, start_time: Time): self.status = status self.miles_traveled = miles_traveled self.route_traveled = route_traveled self.next_destination = next_destination self.arrival_time = arrival_time self.start_time = start_time self.end_time = Time()
def print_stats(self, time: Time = Time(0)): time = self.current_time if time == Time(0) else time time_operated = time - self.start_time package_logs = [(p_id, self.__get_log_by_id(p_id, time, self.packages_log)) for p_id in self.packages.keys()] pending = [p_id for p_id, log in package_logs if log.status is PackageStatus.PENDING] out_for_delivery = [p_id for p_id, log in package_logs if log.status is PackageStatus.OUT_FOR_DELIVERY] delivered = [p_id for p_id, log in package_logs if log.status is PackageStatus.DELIVERED] delivered_late = [p_id for p_id, log in package_logs if log.status is PackageStatus.DELIVERED_LATE] truck_logs = [(t_id, self.__get_log_by_id(t_id, time, self.trucks_log)) for t_id in self.trucks.keys()] total_miles_traveled = reduce(lambda stored, k_v: stored + k_v[1].miles_traveled, truck_logs, 0.0) trucks_used = [t_id for t_id, log in truck_logs if log.miles_traveled > 0.0] # Build package status details to print for each truck used trucks_str = '' def is_on_truck(package_id: int): return self.packages[package_id].truck_id == truck_id for truck_id in trucks_used: trucks_str += f"Truck {truck_id}:\n" trucks_str += ''.join([ f"\tPackages out for delivery: {[p_id for p_id in out_for_delivery if is_on_truck(p_id)]}\n" f"\tPackages delivered: {[p_id for p_id in delivered if is_on_truck(p_id)]}\n", f"\tPackages delivered late: {[p_id for p_id in delivered_late if is_on_truck(p_id)]}\n", ]) hub_stats = ''.join([ f"{'-' * 32} Today the WGUPS Hub {'-' * 32}\n", f"Hours of operation: {self.start_time} to {time}", f"\t({time_operated[0]} Hrs and {time_operated[1]} Min)\n", f"Total number of packages handled: {len(self.packages)}\n", f"Number of trucks used: {len(trucks_used)}\n", f"Packages pending: {pending}\n", trucks_str, f"Total miles traveled for deliveries: {total_miles_traveled:.2f} Miles\n", '-' * 85 ]) print(hub_stats)
def __init_package_data(self) -> None: # Load package data into the Hub with open("../resources/wgups_package_data.csv", newline='') as package_data_file: reader = csv.reader(package_data_file, delimiter=',') # Skip header row next(reader, None) # For each package for row in reader: # Map csv row to array of proper types for easy Package initialization mapped_row: List[Any] = row[:] mapped_row[0] = int(mapped_row[0]) mapped_row[4] = int(mapped_row[4]) mapped_row[5] = Time(*Time.str_to_time_tuple(mapped_row[5])) mapped_row[6] = Time(*Time.str_to_time_tuple(mapped_row[6])) mapped_row[7] = float(mapped_row[7]) mapped_row[8] = str_to_int_or_none(mapped_row[8]) mapped_row[9] = str_to_int_or_none(mapped_row[9]) # Create a new package object with the data new_package = Package(*mapped_row) # Store the package in the Hub for retrieval later self.packages.insert(new_package.id, new_package)
def __calculate_delivery_order(self, truck_id: int, current_location: Location) -> None: truck = self.trucks[truck_id] package_ids = truck.package_ids debug("Starting delivery order: " + str(package_ids)) debug("Total miles: " + str(self.__calculate_total_route_distance(package_ids))) for i in range(len(truck.package_ids)): min_dist_index = i # Everything before 'i' is sorted. Each time we start looking for the closest location to our # current location, we set the first index of out unsorted portion as the potential closest location. for j in range(i, len(truck.package_ids)): delivery_location = self.packages[package_ids[j]].location delivery_distance = self.locations[current_location][delivery_location] delivery_deadline = self.packages[package_ids[j]].delivery_deadline min_dist_location = self.packages[package_ids[min_dist_index]].location current_min_distance = self.locations[current_location][min_dist_location] current_deadline = self.packages[package_ids[min_dist_index]].delivery_deadline # Maintains deadline priorities no_deadlines = delivery_deadline == Time(0) and current_deadline == Time(0) keep_deadline = delivery_deadline <= current_deadline and delivery_deadline != Time(0) # If we find a location closer that does not have a later deadline if delivery_distance < current_min_distance and (no_deadlines or keep_deadline): min_dist_index = j # Set the closest location found as the new end of our sorted list portion temp_id = package_ids[i] package_ids[i] = package_ids[min_dist_index] package_ids[min_dist_index] = temp_id # Set out new current location as the closest location selected # and set a new initial min distance index current_location = self.packages[package_ids[i]].location debug("Ending delivery order: " + str(package_ids)) debug("Total miles: " + str(self.__calculate_total_route_distance(package_ids)))
from wgups.time import Time from wgups.hub import Hub from wgups.models import Location from wgups.view import View from wgups.utilities import parse_user_choice # Ryan Dorman # ID: 001002824 if __name__ == "__main__": # Initialize WGUPS HUB and start the simulation wgups_hub = Hub("WGUPS", Location("4001 South 700 East", "Salt Lake City", "UT", 84107), Time(8)) # Run the simulation wgups_hub.run() # Create View to allow easy access to pre-defined ui components view = View() # Controller logic to allow the user to use the view to query data stored in models # associated with the Hub, Truck, and Packages while True: view.print_logo() wgups_hub.print_stats() user_choice = parse_user_choice(input(view.main_menu)) if user_choice == 0: # Exit break elif user_choice == 1: # View Hub info view.print_hr() time_filter = input(view.time_filter_prompt)
def __init__(self, status: PackageStatus, start_time: Time): self.status = status self.start_time = start_time self.end_time = Time()
def __load_truck_with_packages(self, truck_id: int) -> None: # Ensure truck is at the HUB, has package space remaining, and its an optimal time to load truck = self.trucks[truck_id] package_capacity_left = truck.package_capacity - len(truck.package_ids) no_capacity = package_capacity_left == 0 truck_out = truck.status is not TruckStatus.AT_HUB if truck_out or no_capacity or not self.__is_time_to_load_truck(truck_id): return # Get ids for packages ready to go out for delivery, keep track of any packages # not going on this truck that belong to a package group. The remaining group # members will need to be removed too. groups_to_remove = [] def __can_be_loaded(package_id: int) -> bool: potential_package = self.packages[package_id] ready = potential_package.status is PackageStatus.READY_FOR_PICKUP ok_for_truck_id = potential_package.truck_id is None or potential_package.truck_id == truck_id package_group_id = potential_package.group_id if (not ready or not ok_for_truck_id) and package_group_id is not None: groups_to_remove.append(package_group_id) return ready and ok_for_truck_id # Narrow down packages currently ready to go out for delivery. Space complexity # of O(1) and time complexity of O(n). package_ids_to_deliver = list(filter(__can_be_loaded, self.packages.keys())) # Filter out and group members of packages we can't deliver currently. Space # complexity of O(1) and time complexity of O(n). package_ids_to_deliver = list(filter(lambda package_id: self.packages[package_id].group_id not in groups_to_remove, package_ids_to_deliver)) # If no packages meet our criteria we can stop here, otherwise we start prioritizing packages # to determine what gets loaded. if len(package_ids_to_deliver) == 0: return # We want to ensure any packages that are required to be on a certain truck get loaded when that # truck is docked. def truck_id_cmp(package_id: int) -> Optional[int]: truck_id_to_sort = self.packages[package_id].truck_id return truck_id_to_sort is None, truck_id_to_sort # First, we sort by delivery location so when possible we can load packages being delivered to the # same location on one truck. We then prioritize any packages that must be delivered on the truck being loaded. # Space complexity of O(1) and time complexity O(nlogn). package_ids_to_deliver.sort(key=lambda package_id: (self.packages[package_id].location.key(), truck_id_cmp(package_id))) # Identify all packages to be delivered that have deadlines and those that have no deadlines. # Space complexity of O(1) and time complexity of O(n). package_ids_w_deadline = list(filter(lambda package_id: self.packages[package_id].delivery_deadline != Time(0), package_ids_to_deliver)) package_ids_no_deadline = list(filter(lambda package_id: self.packages[package_id].delivery_deadline == Time(0), package_ids_to_deliver)) # Last, sort the list of # package ids based on delivery deadlines so we end up with a list # of package ids sorted by deadline, same location, required truck id. # Space complexity # of O(1) and time complexity O(nlogn). package_ids_w_deadline.sort(key=lambda package_id: self.packages[package_id].delivery_deadline) # Combine package ids with deadlines and those without into one list with the highest priority package id # as the first element. package_ids_to_deliver_by_deadline = [*package_ids_w_deadline, *package_ids_no_deadline] # While the truck has space and there are packages to go out we load the truck. while package_capacity_left > 0 and len(package_ids_to_deliver) > 0: # Get the package id with the next highest priority to potentially load potential_package_id = package_ids_to_deliver_by_deadline[0] # If this package belongs to a group and the truck can fit # all those in the group, load them. Space complexity of O(1) and # time complexity of O(n). group_id = self.packages[potential_package_id].group_id packages_loaded = [] if group_id is not None: packages_in_group = list( filter(lambda package_id: self.packages[package_id].group_id == group_id, package_ids_to_deliver)) if len(packages_in_group) <= package_capacity_left: for p_id in packages_in_group: truck.package_ids.append(p_id) packages_loaded.append(p_id) # Otherwise add this package with no group to the truck alone else: truck.package_ids.append(potential_package_id) packages_loaded.append(potential_package_id) # Remove all packages loaded from the lists to load and update Package statuses and log the changes. # Space complexity of O(1) and time complexity of O(n). for loaded_id in packages_loaded: package = self.packages[loaded_id] package.status = PackageStatus.OUT_FOR_DELIVERY package.truck_id = truck_id self.log_status(loaded_id, package.status) package_ids_to_deliver.remove(loaded_id) package_ids_to_deliver_by_deadline.remove(loaded_id) package_capacity_left -= len(packages_loaded) # If any packages were loaded onto the truck calculate the delivery order and send the truck out to deliver if len(truck.package_ids) > 0: self.__calculate_delivery_order(truck_id, self.location) self.trucks[truck_id].depart(self.current_time)
def __get_log_by_id(self, item_id: int, time: Time, logs: ChainingHashTable) -> StatusLog: items_log = logs[item_id] for log in items_log: before_log_end = log.end_time == Time(0) or time < log.end_time if log.start_time <= time and before_log_end: return log