Source code for activity

import numpy as np
import pandas as pd

from itertools import groupby

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.colors import ListedColormap, Normalize
import seaborn as sns
from typing import List, Tuple, Dict, Optional, Union
from settings import DESIRED_TIMES



#desired_times = desired_times_stud

# def load_desired_times(pickle_file):
#     '''Loads desired times from input pikle file'''
#     desired_times = pickle.load(open(pickle_file, 'wb'))
#     return desired_times


[docs]class Activity: """ This class creates an "activity" unit to be used in the estimation process. A list of activities constitutes a schedule. Activity objects can be easily created with the ActivityFactory class. Attributes: ------------------- - label: unique label of the activity - start_time: discrete start time (int) - duration: discrete duration (int) - end_time: discrete end time (int) - mode : mode of transportation of the associated travel - next_act: next activity in the schedule object - prev_act: previous activity in the schedule object - early: deviation from preferred start time (early) - late: deviation from preferred start time (late) - short: deviation from preferred duration (short) - long: deviation from preferred duration (long) - boundary_inf: lower bound for feasible start time - boundary_sup: upper bound for feasible end travel_time Methods: ------------------ - Getters and setters for protected attributes - compute_utility: computed utility function for activity """ def __init__(self,label: str,start_time: int, duration: int, end_time: Optional[int] =None, mode: Optional[str]= None, location: Union[Tuple, str, None] = None, prev_act: Optional[str]= None, next_act: Optional[str]= None, early: Optional[float] = None, late: Optional[float] = None, short: Optional[float] = None, long: Optional[float] = None): self._label = label self._start_time = start_time self._duration = duration self._end_time = end_time self._mode = mode self._location = location self._prev_act = prev_act self._next_act = next_act self.early = early self.late = late self.short = short self.long = long self._boundary_inf = (self.label == "home") and self.start_time == 0 self._boundary_sup = (self.label == "home") and self.start_time == (24 - self.duration) if self._boundary_inf : self._prev_act = None else: self._prev_act = prev_act if self._boundary_sup: self._next_act = None else: self._next_act = next_act if self._end_time is None: self._end_time = self._start_time + self._duration if self._label not in ['home', 'dawn', 'dusk']: st_diff = self._start_time - DESIRED_TIMES[self._label]['desired_start_time'] d_diff = self._duration - DESIRED_TIMES[self._label]['desired_duration'] else: st_diff = 0 d_diff = 0 if early is None: self.early = ((st_diff>=-12) & (st_diff<=0))*(-st_diff) + ((st_diff>=12) & (st_diff<=24))*(24-st_diff) if late is None: self.late = ((st_diff>=0)&(st_diff<12))*(st_diff) + ((st_diff>=-24) & (st_diff<-12))*(24+st_diff) if long is None: self.long = (d_diff>=0)*d_diff if short is None: self.short = (d_diff<=0)*(-d_diff) def __eq__(self, othr): return (isinstance(othr, type(self)) and (self.label, self.start_time, self.duration) == (othr.label, othr.start_time, othr.duration) ) def __hash__(self): return hash((self.label, self.start_time, self.duration)) def __str__(self): return f"{self.label}: start time {self.start_time}, duration {self.duration} h, location {self.location}" #Getters and setters for protected attributes @property def label(self): return self._label @label.setter def label(self, lab): self._label = lab @property def duration(self): return self._duration @duration.setter def duration(self, dur): self._duration = dur @property def start_time(self): return self._start_time @start_time.setter def start_time(self, st): self._start_time = st @property def end_time(self): return self._end_time @end_time.setter def end_time(self, et): self._end_time = et @property def mode(self): return self._mode @mode.setter def mode(self, m): self._mode = m @property def location(self): return self._location @location.setter def location(self,loc): self._location = loc @property def prev_act(self): return self._prev_act @prev_act.setter def prev_act(self, act): if self._boundary_inf: self._prev_act = None else: self._prev_act = act @property def next_act(self): return self._next_act @next_act.setter def next_act(self, act): if self._boundary_sup: self._next_act = None else: self._next_act = act
[docs] def compute_utility(self, params: Dict, reference_act: List = ['home', 'dawn', 'dusk']): """Computes the activity-specific utility function. Parameters ---------- params: Dictionary of parameters to be used in the utility function reference_act: List of label(s) of the reference activity (default is home) Return ---------- V: value of the utility function """ at = self.label flexibility_lookup = {'education': 'NF','work': 'NF','errands_services': 'F','escort': 'NF','leisure': 'F','shopping': 'F', 'home': 'F', 'business_trip': 'NF'} early = self.early late = self.late short = self.short long = self.long V = 0 if at not in reference_act: if at in ["business_trip", "escort", "errands_services"]: params[f"{at}:constant"] = 0 fd = flexibility_lookup[at] V += params[f"{at}:constant"] + params[f'{fd}:early']* early + params[f'{fd}:late']* late + params[f'{fd}:short']* short \ + params[f'{fd}:long'] * long return V
[docs]class Travel(Activity): """ This class defines the specific Travel activity. """ def __init__(self): super().__init__("Travel", location = None) def __str__(self): return f"Travel from {self.prev_act.label} (location: {self.prev_act.location}) to {self.next_act.label} (location: {self.next_act.location}), by {self.mode}.\n Travel time: {self.duration}"
[docs]class ActivityFactory: """ This class creates and Activity object. Methods: ------------------- - create: creates object from Activity class """ def __init__(self): pass
[docs] def create(self, label: Optional[str] = None, random: bool = False, list_act: List = ["home","work","education","shopping","errands_services", "leisure","escort", "business_trip"],**kwargs) -> Activity: """ Creates an object from the Activity class. Parameters: --------------- -label: label of the activity to create -random: if True, creates a random activity -list_act: list of possible activity labels -**kwargs: other keywords arguments that will be passed to the Activity constructor. """ if (label is None) and (random == False): raise ValueError("No activity label was passed.") if random: label = np.random.choice(list_act) new_act = Activity(label, **kwargs) return new_act
[docs]class Schedule: """ This class stores schedules of activity objects. Attributes: ----------------- -list_act: list of Activity objects representing the activities in the schedule -total_dur: total schedule duration, default is 24h -start: start time of the schedule (default: Oh - midnight) -end: end time of the schedule (default: 24h ) -discretization: schedule discretization in hours, default: 1/60 h -feasibility: boolean that indicates if the schedule is feasible -travel_time_mat: matrix of travel times -anchor_nodes: time points in the schedule where operators changes will be applied. Default: at every hour for empty schedules -all_starts: list of the start times of every activity in the schedule -all_locations: list of the locations of every activity in the schedule -all_labels: list of the labels of every activity in the schedule -list_modes: list of the possible modes of transportation Methods: ----------------- """ def __init__(self, list_act: Optional[List]=None, total_dur:int=24, start:int=0, end:int=24, discretization:float=1/60, travel_time_mat:Dict = None)-> None: self._list_act = list_act self._total_dur = total_dur self._start = start self._end = end self._discretization = discretization self._feasibility = False self._travel_time_mat = travel_time_mat if list_act and len(list_act)>2: #set the anchor nodes to the start times of the existing activities, excluding the first and last one (boundary at home which cannot be changed) self._anchor_nodes = [x.start_time for x in list_act[1:-1]] else: #if no activity has been passed the anchors are initialized at every hour self._anchor_nodes = np.arange(1, 24) self.all_starts = [act.start_time for act in self.list_act] self.all_ends = [act.end_time for act in self.list_act] self.all_locations = [act.location for act in self.list_act] self.all_labels = [act.label for act in self.list_act] self.list_modes = ["driving","pt","cycling"] def __eq__(self, othr): return (isinstance(othr, type(self)) and (self.list_act, self.all_starts) == (othr.list_act, othr.all_starts)) def __hash__(self): return hash((tuple(self.list_act), tuple(self.all_starts))) @property def list_act(self): return self._list_act @list_act.setter def list_act(self, l_a: List): self._list_act = l_a self.update() @property def total_dur(self): return self._total_dur @total_dur.setter def total_dur(self, dur: int): self._total_dur = dur @property def start(self): return self._start @start.setter def start(self,st:Union[float,int]): self._start = st @property def end(self): return self._end @end.setter def end(self,et:Union[float,int]): self._end = et @property def discretization(self): return self._discretization @discretization.setter def discretization(self, discret: float): self._discretization = discret @property def feasibility(self): return self._feasibility @feasibility.setter def feasibility(self, status: bool): self._feasibility = status @property def travel_time_mat(self): return self._travel_time_mat @travel_time_mat.setter def travel_time_mat(self, tt_mat: Dict): self._travel_time_mat = tt_mat @property def anchor_nodes(self): return self._anchor_nodes @anchor_nodes.setter def anchor_nodes(self,nodes: List): self._anchor_nodes = nodes
[docs] def get_travel_time(self, origin: Union[Tuple,str,int], destination: Union[Tuple,str,int], mode: str)->float: """ Extract OD travel time. Parameters --------------- origin: ID of the origin (must be a valid key in the travel time matrix) destination: ID of the destination (must be a valid key in the travel time matrix) mode: mode of transportation (must be a valid key in the travel time matrix) Returns -------------- tt: travel time in hours """ if (None in [origin, destination, mode]) or (self.travel_time_mat is None): #!-----change this if you want another behaviour for missing values (here it's setting the TT to 0)---! return 0 try: tt = self.travel_time_mat[mode][origin][destination] except KeyError: #!-----change this if you want another behaviour for missing values (here it's setting the TT to 0)---! print('Couldnt compute travel time. Setting to 0.') tt = 0 return tt
[docs] def get_home_location(self): """ Returns the location identified as home. """ return self.list_act[0].location
[docs] def update(self): """ Updates variables when the activities change. """ self.all_starts = [act.start_time for act in self.list_act] self.all_ends = [act.end_time for act in self.list_act] self.all_locations = [act.location for act in self.list_act] self.all_labels = [act.label for act in self.list_act]
[docs] def streamline(self) -> None: """This function checks that the schedule is valid in terms of continuity (e.g. no gaps in time or space)""" sched_to_update = [x for x in self.list_act if x] if not sched_to_update: #empty list - set all at home self.list_act = [Activity("home", 0, 24, location = self.get_home_location(), mode = 'driving')] return None #Sort by start time sched_to_update.sort(key=lambda x: x.start_time) #Check boundary conditions (start and end at home) if sched_to_update[0].label not in ['home', 'dawn']: current_start = sched_to_update[0].start_time #current start of the schedule sched_to_update.insert(0, Activity('home', 0, current_start, location = self.get_home_location(), mode = 'driving')) if sched_to_update[-1].label not in ['home', 'dusk']: current_end = sched_to_update[-1].end_time#current_end sched_to_update.append(Activity('home', current_end, 24-current_end, location = self.get_home_location(), mode = 'driving')) for i,a in enumerate(sched_to_update[1:], 1): #Check that the start time matches the end time of the previous activity + travel time prev_act = sched_to_update[i-1] try: tt = self.get_travel_time(prev_act.location, a.location, prev_act.mode) except KeyError: print(f"Couldn't find location. Travel Time matrix: {self.travel_time_mat[prev_act.mode]} \n Locations: {prev_act.location, a.location, prev_act.mode}") tt = 0 if a.start_time != (prev_act.end_time + tt): a.start_time = prev_act.end_time + tt #Put back updated activity in schedule sched_to_update[i] = a #Define boundary times sched_to_update[0].start_time = self.start sched_to_update[-1].end_time = self.end #Combine consecutive duplicates of activities grouped_labels = groupby(sched_to_update, lambda x: x.label) new_list = [] for _, group in grouped_labels: group = list(group) new_act = group[0] new_act.end_time = group[-1].end_time new_list.append(new_act) sched_to_update = new_list #Fix durations and drop invalid ones (no or negative duration) to_drop = [] for i,a in enumerate(sched_to_update): #check durations #prev_act = sched_to_update[i-1] duration = a.end_time - a.start_time if duration > 0: a.duration = duration else: to_drop.append(i) #Put back updated activity in schedule sched_to_update[i] = a #Drop invalid activities updated_sched = [x for i,x in enumerate(sched_to_update) if i not in to_drop] #Set updated schedule as attribute self.list_act = updated_sched #Delete variables from namespace del sched_to_update, updated_sched, new_list, to_drop
[docs] def compute_utility(self, params:Dict, rnd_term:Optional[float]= None): """Computes utility of full schedule, given utility parameters Parameters --------------- params: Dictionary of utility parameters rnd_term:random term to add to the utility function Returns --------------- utility: utility function of the schedule """ if not rnd_term: #Draw an EV distributed error term rnd_term = np.random.gumbel() list_act = self.list_act #Compute all travel times travel_times = 0 for i in range(len(list_act)-1): origin = list_act[i].location destination = list_act[i+1].location mode = list_act[i].mode travel_times += self.get_travel_time(origin, destination, mode) utility = sum([a.compute_utility(params) for a in list_act]) + params['travel_time']*travel_times + rnd_term return utility
[docs] def which_activity(self, time:float)->Activity: """ Returns activity that is happening at a given time """ start_idx = np.searchsorted(self.all_starts, time, side='right') activity = self.list_act[start_idx] return activity
[docs] def output(self, plot:bool = False, **kwargs) -> pd.DataFrame: """ This method creates a formatted pandas DataFrame of the schedule. Parameters: --------------- plot: if True, plots schedule **kwargs: keyword arguments to be passed to the plotting function Returns: --------------- df: DataFrame """ cols = ['activity', 'start', 'end', 'duration'] df = pd.DataFrame(columns = cols) df.activity = pd.Series([x.label for x in self.list_act]) df.start = pd.Series([x.start_time for x in self.list_act]) df.end = pd.Series([x.end_time for x in self.list_act]) df.duration = pd.Series([x.duration for x in self.list_act]) if plot: pl = self.plot(**kwargs) return df, pl return df
[docs] def plot(self, list_act: List =None, title: str=None, figure_size: List = [20,3],**kwargs): """ Plots given schedule. Parameters: --------------- -list_act: Default list of activities (for activity colors) - title: plot title - figure_size: size of the matplotlib figure - kwargs: other keyword arguments for matplotlib's functions """ colors = self.activity_colors(list_act) fig = plt.figure(figsize=figure_size) y1 = [0, 0] y2 = [1, 1] plt.fill_between([0, 24], y1, y2, color="silver") for item in self.list_act: x = [item.start_time, item.end_time] plt.fill_between(x, y1, y2, color=colors[item.label]) plt.xticks(np.arange(0, 25)) plt.yticks([]) plt.xlim([0, 24]) plt.ylim([-1, 2]) plt.xlabel("Time [h]") if title: plt.title(title, fontsize=12, fontweight="bold") leg_labels = [x.label if x not in ["dawn", "dusk"] else "home" for x in self.list_act] legend_handles = [mpatches.Patch(color=colors[act], label=act) for act in set(leg_labels)] plt.legend(handles=legend_handles) return fig
[docs] def activity_colors(self, list_act: List =None, palette: str ="colorblind")-> List: """Match each activity from list to a color from the input palette. Useful to keep consistent colors across visualizations Parameters: ---------------------- - list_act: list of activity all_labels - palette; name of matplotlib/searborn color palette. """ if list_act is None: list_act = [ "home", "work", "education", "shopping", "errands_services", "business_trip", "leisure", "escort", ] colors = { a: c for a, c in zip(list_act, sns.color_palette(palette, len(list_act)).as_hex()) } colors["home"] = "gainsboro" if "home" in colors.keys(): colors["dawn"] = colors["home"] colors["dusk"] = colors["home"] return colors