import warnings
import numpy as np
import pandas as pd
from copy import deepcopy
from activity import Activity, Schedule, ActivityFactory
from typing import List, Dict, Tuple, Optional
[docs]class Operator:
"""
This class creates an "operator" unit to be used in the estimation process. Operators can easily be created with the OperatorFactory class.
Attributes:
-------------------
- optype: label of the operator
- proba: probability associated with the operator
- list_operators: the list of currently available operators.
Methods:
------------------
- describe: prints information on the operator
- apply_change: applies a change to the given schedule.
- compute_change_proba: computes probability of change
"""
def __init__(self, optype:str, proba:float, **kwargs):
self._optype = optype
self._proba = proba
self._list_operators = ["Block", "Assign", "AddAnchor", "InflateDeflate", "Swap"]
[docs] def describe(self):
print(f"Type: {self._optype} \n Probability: {self._proba}")
@property
def proba(self):
return self._proba
@proba.setter
def proba(self, proba: float):
self._proba = proba
@property
def optype(self):
return self._optype
@optype.setter
def optype(self, optype:str):
self._optype = optype
@property
def list_operators(self):
return self._list_operators
@list_operators.setter
def list_operators(self, l_op):
self._list_operators = l_op
[docs] def apply_change(self, schedule: Schedule) -> Schedule:
"""
Applies change on given schedule.
"""
return schedule
[docs] def compute_change_proba(self):
"""Computes forward probability of change."""
pass
[docs]class Block(Operator):
"""
Block operator: changes the discretization of the schedule.
Parameters:
-------------
proba: probability of the operator
discret_list: list of possible discretizations in hours
"""
def __init__(self, proba: float, **kwargs)->None:
super().__init__("Block", proba)
self.discret_list = [
5/60,
15/60,
30/60,
1,
] # expressed in hours (5 min, 15 min, 30 min, 1h)
[docs] def apply_change(self, schedule: Schedule)->Schedule:
"""Changes discretization of given schedule"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
n = np.random.choice(self.discret_list)
schedule.discretization = n
#delta = schedule.discretization # check discretization from input schedule
#if n != delta: # Discretization changes
#act_in_sched = schedule.list_act
#for a in act_in_sched:
# new_time = a.start_time * delta / n
# a.start_time = new_time
#schedule.discretization = n
#schedule.list_act = act_in_sched
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, n_discret:int=4, **kwargs)->float:
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
-n_discret: number of possible discretizations
"""
if prev_sched.discretization== new_sched.discretization:
#probability that discretization hasn't changed
proba = 1 / n_discret
else:
#probability that discretization is different
proba = 1 - (1 / n_discret)
return proba
[docs]class AddAnchor(Operator):
"""
Anchor operator: changes the anchors of the schedule.
Parameters:
-------------
proba: probability of the operator
"""
def __init__(self, proba:float, **kwargs):
super().__init__("Anchor", proba)
[docs] def apply_change(self, schedule:Schedule)-> Schedule:
"""
Changes anchor nodes of given schedule.
"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
current_anchors = schedule.anchor_nodes
new_anchor = np.random.uniform(0, 24)
if new_anchor not in current_anchors:
current_anchors.append(new_anchor)
schedule.anchor_nodes = current_anchors
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, **kwargs) ->float:
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
"""
previous_anchors = prev_sched.anchor_nodes
new_anchors = new_sched.anchor_nodes
if previous_anchors == new_anchors:
#probability of drawing an anchor that is already present
proba = len(previous_anchors)/24
else:
#probability of drawing an anchor that is not in the list
proba = 1 - len(previous_anchors)/24
return proba
[docs]class Assign(Operator):
"""
Assign operator: adds an activity in the schedule
Parameters:
-------------
proba: probability of the operator
list_act: list of activities to choose from
p_act: choice probabilities for the activities
chosen_act_proba: probability of the chosen activity
"""
def __init__(self, proba, list_act:Optional[List] = None, p_act:Optional[List] = None, **kwargs):
super().__init__("Assign", proba)
if list_act:
self.list_act = list_act
else:
self.list_act = ["home","work","education","shopping","errands_services","business_trip","leisure","escort"]
if p_act:
self.p_act = p_act
else:
self.p_act = [1/len(self.list_act)]*len(self.list_act)
self.chosen_act_proba = 1/len(self.list_act)
[docs] def apply_change(self, schedule:Schedule)->Schedule:
"""
Assigns an activity to an existing block or anchor. The boundary conditions
(first and last block are at home) must be respected.
"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
act_in_sched = [x for x in schedule.list_act if x]
rnd_node = np.random.choice(schedule.anchor_nodes) #choose a random anchor
rnd_idx = np.random.choice(range(len(self.list_act)), p = self.p_act) #select random activity in list
rnd_act = self.list_act[rnd_idx]
default_loc = schedule.get_home_location()
new_act = Activity(rnd_act, rnd_node, duration = schedule.discretization, mode = 'driving', location = default_loc)
#find where the new activity fits in the schedule
all_starts = schedule.all_starts
if rnd_node in all_starts:
#If another activity starts at the same time, shift by a minute to maintain the order of activities -
# if the new activity is longer than the older one it will be deleted during the streamline operation
#otherwise, the timings will be adjusted accordingly
idx = all_starts.index(rnd_node)
up_act = act_in_sched[idx]
up_act.start_time = act_in_sched[idx].start_time + 1/60
act_in_sched[idx] = up_act
new_act.location = up_act.location
act_in_sched.append(new_act)
act_in_sched = [x for x in act_in_sched if x] #Remove NaNs
act_in_sched.sort(key=lambda x: x.start_time) #sort activities by start time
schedule.list_act = act_in_sched
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, **kwargs)-> float:
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
"""
#Probability of choosing an anchor, and that activity is different than the current one
proba = (1/len(prev_sched.anchor_nodes))*(1-self.chosen_act_proba)
return proba
[docs]class InflateDeflate(Operator):
"""
Inflate/deflate operator: increases or decreases duration of an activity
Parameters:
-------------
proba: probability of the operator
"""
def __init__(self, proba, **kwargs):
super().__init__("InflateDeflate", proba)
[docs] def apply_change(self, schedule:Schedule)->Schedule:
"""Randomly increases duration of one activity, and decrease duration of another by same amount."""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
act_in_sched = schedule.list_act
nodes = schedule.anchor_nodes
block_size = schedule.discretization
rnd_act_inf = schedule.which_activity(np.random.choice(nodes))
direction = np.random.randint(0, 2) # 0 right (or clockwise) 1 left (or counterclockwise)
#get position of activities in schedule
idx_inf = act_in_sched.index(rnd_act_inf.label)
if direction == 0:
rnd_act_inf.end_time = rnd_act_inf.end_time + block_size #increase by one unit of time (from current discretization)
act_in_sched[idx_inf] = rnd_act_inf
else:
rnd_act_inf.end_time = rnd_act_inf.end_time - block_size #decrease by one unit of time (from current discretization)
act_in_sched[idx_inf] = rnd_act_inf
#update list of activities in schedule
schedule.list_act = act_in_sched
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, **kwargs) -> float:
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
"""
proba = 0.5 * 1/len(prev_sched.anchor_nodes)
return proba
[docs]class Swap(Operator):
"""
Swap operator: swaps 2 adjacent blocks (start time and duration)
Parameters:
-------------
proba: probability of the operator
"""
def __init__(self, proba, **kwargs):
super().__init__("Swap", proba)
[docs] def apply_change(self, schedule:Schedule)->Schedule:
"""
Swaps 2 adjacent blocks (start time and duration)
"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
act_in_sched = schedule.list_act
if len(act_in_sched) < 3:
#nothing to swap
return schedule
rnd_act = schedule.which_activity(np.random.choice(schedule.anchor_nodes))
swap = np.random.randint(0, 2) # if swap = 0 swap with next act, else swap with previous one
idx = act_in_sched.index(rnd_act)
first_act = act_in_sched[idx]
second_act = act_in_sched[idx + (1 - 2 * swap)]
try:
first_act.label = second_act.label
second_act.label = rnd_act.label
act_in_sched[idx] = first_act
act_in_sched[idx + (1 - 2 * swap)] = second_act
#update list of activities in schedule
schedule.list_act = act_in_sched
except ValueError:
print("Error in swapping activities")
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, **kwargs):
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
"""
proba = 0.5 * 1/len(prev_sched.anchor_nodes)
return proba
[docs]class Mode(Operator):
"""
Mode operator: changes mode of transportation associated with activity
Parameters:
-------------
proba: probability of the operator
list_modes: list of possible modes to choose from
p_modes: associated probabilities
"""
def __init__(self, proba, list_modes = None, p_modes = None, **kwargs):
super().__init__("Mode", proba)
if list_modes:
self.list_modes = list_modes
else:
self.list_modes = ["driving","pt","cycling"]
if p_modes:
self.p_modes = p_modes
else:
self.p_modes= [1/len(self.list_modes)]*len(self.list_modes)
[docs] def apply_change(self, schedule:Schedule)->Schedule:
"""
This operator randomly changes the travel mode of the selected activty
"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
act_in_sched = [x for x in schedule.list_act if x]
#acts = schedule.get_list_act()
rnd_act = schedule.which_activity(np.random.choice(schedule.anchor_nodes))
rnd_mode = np.random.choice(self.list_modes, p = self.p_modes) # select random modes in list
try:
#modify mode of current activity
rnd_act.mode = rnd_mode
idx = act_in_sched.index(rnd_act)
act_in_sched[idx] = rnd_act
schedule.list_act = act_in_sched
except ValueError:
print("Error in changing the mode of activity")
print(schedule, rnd_act, rnd_mode)
return schedule
[docs] def compute_change_proba(self, prev_sched:Schedule, new_sched:Schedule, **kwargs):
"""
Computes change probability.
Parameters:
----------
-prev_sched, new_sched: Schedule objects
"""
prev_modes = [x.mode for x in prev_sched.list_act]
new_modes = [x.mode for x in new_sched.list_act]
if prev_modes == new_modes:
proba = (1/len(prev_sched.anchor_nodes))*(1/len(self.list_modes))
else:
proba = (1/len(prev_sched.anchor_nodes))*(1 - 1/len(self.list_modes))
return proba
[docs]class Location(Operator):
"""
Location operator: changes location associated with activity
Parameters:
-------------
proba: probability of the operator
list_loc: list of possible locations to choose from
p_loc: associated probabilities
"""
def __init__(self, proba, list_loc=None, p_loc=None, **kwargs):
super().__init__("Location", proba)
self.list_loc = list_loc
self.p_loc = p_loc
[docs] def apply_change(self, schedule: Schedule)->Schedule:
"""
This operator randomly changes the travel mode of the selected activty
"""
#if np.log(np.random.rand()) >= np.log(self.proba):
# return schedule
if self.list_loc is None:
self.list_loc = set(schedule.all_locations)
self.p_loc = [1/len(self.list_loc)]*len(self.list_loc)
elif self.p_loc is None:
self.p_loc = [1/len(self.list_loc)]*len(self.list_loc)
act_in_sched = [x for x in schedule.list_act if x]
rnd_act = schedule.which_activity(np.random.choice(schedule.anchor_nodes))
rnd_loc = np.random.choice(self.list_loc, p=self.p_loc) # select random modes in list
try:
#modify location of current activity
rnd_act.location = rnd_loc
idx = act_in_sched.index(rnd_act)
act_in_sched[idx] = rnd_act
schedule.list_act = act_in_sched
except ValueError:
print("Error in changing the location of activity")
print(schedule, rnd_act, rnd_loc)
return schedule
[docs] def compute_change_proba(self, prev_sched, new_sched, **kwargs):
prev_loc = [x.location for x in prev_sched.list_act]
new_loc = [x.location for x in new_sched.list_act]
if prev_loc == new_loc:
proba = (1/len(prev_sched.anchor_nodes))*(1/len(self.list_loc))
else:
proba = (1/len(prev_sched.anchor_nodes))*(1 - 1/len(self.list_loc))
return proba
[docs]class OperatorFactory:
"""
This class creates an object from the Operator class.
"""
def __init__(self):
pass
[docs] def create(self, optype:str, **kwargs) -> Operator:
"""Creates an object from the Operator class
Parameters
-----------
-optype: label of operator
-kwargs: other keyword arguments that will be passed to the constructor of the Operator class.
Return
----------
Operator
"""
object = globals()[optype](**kwargs)
return object
[docs] def draw(self, list_operators: List, p_operators: Optional[List] =None, **kwargs) -> Operator:
"""
Randomly creates an object from the Operator class, given a list of possible operators and probabilities.
Parameters
-----------
-list_operators: list of possible operators to choose from
-p_operators: list of operator probabilities (must be the same length as list_operators and sum up to 1)
-kwargs: other keyword arguments that will be passed to the constructor of the Operator class.
Return
----------
Operator
"""
if not p_operators:
p_operators = len(list_operators)*[1/len(list_operators)]
rnd_op = np.random.choice(range(len(list_operators)), p=p_operators)
n_op = None
if list_operators[rnd_op] == 'MetaOperator':
#Choose how many operators to combine
n_op = np.random.randint(2, len(list_operators))
operator = self.create(list_operators[rnd_op], proba = p_operators[rnd_op], n_op = n_op, proba_operators = p_operators, **kwargs)
return operator