Skip to content

Commit 6f96cb7

Browse files
committed
implement departure time choice DUE
1 parent 62cea8a commit 6f96cb7

File tree

1 file changed

+358
-0
lines changed

1 file changed

+358
-0
lines changed

uxsim/DTAsolvers/DTAsolvers.py

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,364 @@ def plot_link_stats(s):
888888

889889
plt.show()
890890

891+
def plot_vehicle_stats(s, orig=None, dest=None):
892+
ave_TT = []
893+
std_TT = []
894+
depature_time = []
895+
for vehid in s.route_log[0].keys():
896+
if (s.W_sol.VEHICLES[vehid].orig.name == orig or orig == None) and (s.W_sol.VEHICLES[vehid].dest.name == dest or dest == None):
897+
length = len(s.route_log)
898+
ts = [s.cost_log[day][vehid] for day in range(int(length/2), length)]
899+
ave_TT.append(np.average(ts))
900+
std_TT.append(np.std(ts))
901+
depature_time.append(s.W_sol.VEHICLES[vehid].departure_time_in_second)
902+
903+
plt.figure()
904+
orig_ = orig
905+
if orig == None:
906+
orig_ = "any"
907+
dest_ = dest
908+
if dest == None:
909+
dest_ = "any"
910+
plt.title(f"orig: {orig_}, dest: {dest_}")
911+
plt.errorbar(x=depature_time, y=ave_TT, yerr=std_TT,
912+
fmt='bx', ecolor="#aaaaff", capsize=0, label="travel time (mean $\pm$ std)")
913+
plt.xlabel("departure time of vehicle")
914+
plt.ylabel("travel time")
915+
plt.legend()
916+
plt.show()
917+
918+
919+
class SolverDUE_departure_time_choice:
920+
def __init__(s, func_World, desired_arrival_time, time_windows, alpha=1, beta=0.3, gamma=0.9, insensitive_ratio=0.333):
921+
"""
922+
Solve quasi Dynamic User Equilibrium (DUE) problem with departure time choice using day-to-day dynamics. WIP
923+
924+
Parameters
925+
----------
926+
func_World : function
927+
function that returns a World object with nodes, links, and demand specifications
928+
desired_arrival_time : float
929+
desired arrival time to the destination in second
930+
time_windows : list of float
931+
Time windows for departure time. List of the start time of each time window in second. The final element denotes the end of the windows.
932+
For example, `time_windows=[0,600,1200,1800]' means there are 3 windows, namely, 0-600 s, 600-1200 s, and 1200-1800 s.
933+
alpha : float
934+
travel time penalty coefficient per second
935+
beta : float
936+
early arrival penalty coefficient per second
937+
gamma : float
938+
late arrival penalty coefficient per second
939+
insensitive_ratio : float
940+
travelers become insensitive to the cost difference smaller than this ratio (i.e, bounded rationality, epsilon-Nash equilibirum)
941+
942+
Notes
943+
-----
944+
This function computes a near dynamic user equilibirum state as a steady state of day-to-day dynamical routing game.
945+
946+
This considers departure time choice as well as route choice.
947+
948+
Specifically, on day `i`, vehicles choose their route based on actual travel time on day `i-1` with the same departure time.
949+
If there are shorter travel time route, they will change with probability `swap_prob`.
950+
This process is repeated until `max_iter` day.
951+
It is expected that this process eventually reach a steady state.
952+
Due to the problem complexity, it does not necessarily reach or converge to Nash equilibrium or any other stationary points.
953+
However, in the literature, it is argued that the steady state can be considered as a reasonable proxy for Nash equilibrium or dynamic equilibrium state.
954+
There are some theoretical background for it; but intuitively speaking, the steady state can be considered as a realistic state that people's rational behavior will reach.
955+
956+
This method is based on the following literature:
957+
Route choice equilibrium:
958+
Ishihara, M., & Iryo, T. (2015). Dynamic Traffic Assignment by Markov Chain. Journal of Japan Society of Civil Engineers, Ser. D3 (Infrastructure Planning and Management), 71(5), I_503-I_509. (in Japanese). https://doi.org/10.2208/jscejipm.71.I_503
959+
Iryo, T., Urata, J., & Kawase, R. (2024). Traffic Flow Simulator and Travel Demand Simulators for Assessing Congestion on Roads After a Major Earthquake. In APPLICATION OF HIGH-PERFORMANCE COMPUTING TO EARTHQUAKE-RELATED PROBLEMS (pp. 413-447). https://doi.org/10.1142/9781800614635_0007
960+
Iryo, T., Watling, D., & Hazelton, M. (2024). Estimating Markov Chain Mixing Times: Convergence Rate Towards Equilibrium of a Stochastic Process Traffic Assignment Model. Transportation Science. https://doi.org/10.1287/trsc.2024.0523
961+
962+
Departure time choice equilibrium:
963+
Satsukawa, K., Wada, K., & Iryo, T. (2024). Stability analysis of a departure time choice problem with atomic vehicle models. Transportation Research Part B: Methodological, 189, 103039. https://doi.org/10.1016/j.trb.2024.103039
964+
965+
"""
966+
s.func_World = func_World
967+
968+
s.desired_arrival_time = desired_arrival_time
969+
s.time_windows = time_windows
970+
s.alpha = alpha
971+
s.beta = beta
972+
s.gamma = gamma
973+
s.insensitive_ratio = insensitive_ratio
974+
975+
s.W_sol = None #final solution
976+
s.W_intermid_solution = None #latest solution in the iterative process. Can be used when an user terminate the solution algorithm
977+
s.dfs_link = []
978+
979+
warnings.warn("DTA solver is experimental and may not work as expected. It is functional but unstable.")
980+
981+
def solve(s, max_iter, n_routes_per_od=10, swap_prob=0.05, print_progress=True, callback=None):
982+
"""
983+
Solve quasi Dynamic User Equilibrium (DUE) problem using day-to-day dynamics. WIP
984+
985+
Parameters
986+
----------
987+
max_iter : int
988+
maximum number of iterations
989+
n_routes_per_od : int
990+
number of routes to enumerate for each OD pair
991+
swap_prob : float
992+
probability of route swap
993+
print_progress : bool
994+
whether to print the information
995+
996+
Returns
997+
-------
998+
W : World
999+
World object with quasi DUE solution (if properly converged)
1000+
1001+
Notes
1002+
-----
1003+
`self.W_sol` is the final solution.
1004+
`self.W_intermid_solution` is a latest solution in the iterative process. Can be used when an user terminate the solution algorithm.
1005+
"""
1006+
s.start_time = time.time()
1007+
1008+
W_orig = s.func_World()
1009+
if print_progress:
1010+
W_orig.print_scenario_stats()
1011+
1012+
# enumerate routes for each OD pair
1013+
n_routes_per_od = n_routes_per_od
1014+
1015+
dict_od_to_vehid = defaultdict(lambda: [])
1016+
for key, veh in W_orig.VEHICLES.items():
1017+
o = veh.orig.name
1018+
d = veh.dest.name
1019+
dict_od_to_vehid[o,d].append(key)
1020+
1021+
# dict_od_to_routes = {}
1022+
# for o,d in dict_od_to_vehid.keys():
1023+
# routes = enumerate_k_shortest_routes(W_orig, o, d, k=n_routes_per_od)
1024+
# dict_od_to_routes[o,d] = routes
1025+
1026+
if W_orig.finalized == False:
1027+
W_orig.finalize_scenario()
1028+
dict_od_to_routes = enumerate_k_random_routes(W_orig, k=n_routes_per_od)
1029+
1030+
if print_progress:
1031+
print(f"number of OD pairs: {len(dict_od_to_routes.keys())}, number of routes: {sum([len(val) for val in dict_od_to_routes.values()])}, number of departure time windows: {len(s.time_windows)}")
1032+
1033+
# day-to-day dynamics
1034+
s.ttts = []
1035+
s.n_swaps = []
1036+
s.potential_swaps = []
1037+
s.cost_gaps = []
1038+
s.route_log = []
1039+
s.dep_t_log = []
1040+
s.cost_log = []
1041+
swap_prob = swap_prob
1042+
max_iter = max_iter
1043+
1044+
print("solving DUE...")
1045+
for i in range(max_iter):
1046+
W = s.func_World()
1047+
# if i != max_iter-1:
1048+
# W.vehicle_logging_timestep_interval = -1
1049+
1050+
if i != 0:
1051+
for key in W.VEHICLES:
1052+
if key in routes_specified:
1053+
W.VEHICLES[key].enforce_route(routes_specified[key])
1054+
if key in dep_time_specified:
1055+
W.VEHICLES[key].departure_time = int(dep_time_specified[key]/W.DELTAT)
1056+
W.VEHICLES[key].departure_time_in_second = dep_time_specified[key]
1057+
W.VEHICLES[key].log_t_link[0][0] = int(dep_time_specified[key]/W.DELTAT)
1058+
1059+
route_set = defaultdict(lambda: []) #routes[o,d] = [Route, Route, ...]
1060+
for o,d in dict_od_to_vehid.keys():
1061+
for r in dict_od_to_routes[o,d]:
1062+
route_set[o,d].append(W.defRoute(r))
1063+
1064+
# simulation
1065+
W.exec_simulation()
1066+
1067+
# results
1068+
W.analyzer.print_simple_stats()
1069+
#W.analyzer.network_average()
1070+
1071+
# attach route choice set to W object for later re-use at different solvers like DSO-GA
1072+
W.dict_od_to_routes = dict_od_to_routes
1073+
1074+
s.W_intermid_solution = W
1075+
1076+
s.dfs_link.append(W.analyzer.link_to_pandas())
1077+
1078+
# route swap
1079+
routes_specified = {}
1080+
dep_time_specified = {}
1081+
route_actual = {}
1082+
dep_time_actual = {}
1083+
cost_actual = {}
1084+
n_swap = 0
1085+
total_cost_gap = 0
1086+
potential_n_swap = 0
1087+
for key,veh in W.VEHICLES.items():
1088+
o = veh.orig.name
1089+
d = veh.dest.name
1090+
r, ts = veh.traveled_route()
1091+
dep_time = veh.departure_time*W.DELTAT
1092+
travel_time = ts[-1]-ts[0]
1093+
1094+
def schedule_cost(arr_t):
1095+
if arr_t > 0: #negative is null (not arrived)
1096+
if arr_t < s.desired_arrival_time:
1097+
arr_time_penalty = s.beta * (s.desired_arrival_time-arr_t)
1098+
else:
1099+
arr_time_penalty = s.gamma * (arr_t-s.desired_arrival_time)
1100+
else:
1101+
arr_time_penalty = (s.beta+s.gamma)*W.TMAX + W.TMAX*s.alpha
1102+
return arr_time_penalty
1103+
1104+
1105+
route_actual[key] = [rr.name for rr in r]
1106+
dep_time_actual[key] = veh.departure_time*W.DELTAT
1107+
cost_actual[key] = s.alpha*travel_time + schedule_cost(veh.arrival_time*W.DELTAT)
1108+
1109+
veh.departure_time_choice_cost = cost_actual[key]
1110+
veh.departure_time_choice_cost_tt = s.alpha*travel_time
1111+
veh.departure_time_choice_cost_sc = schedule_cost(veh.arrival_time*W.DELTAT)
1112+
1113+
if veh.state != "end":
1114+
continue
1115+
1116+
flag_changed = False
1117+
flag_potential_change = False
1118+
route_changed = None
1119+
dep_t_changed = None
1120+
1121+
#stabilization technique: portion change
1122+
change_possibility = random.random() < swap_prob
1123+
#stabilization technique: travelers with earlier departure time is unlikely to change
1124+
# dep_t_ratio = (dep_time_actual[key]-s.time_windows[0])/(s.time_windows[-1]-s.time_windows[0])
1125+
# if random.random() > dep_t_ratio+0.1:
1126+
# change_possibility = False
1127+
1128+
actual_cost = s.alpha*r.actual_travel_time(ts[0]) + schedule_cost(dep_time_actual[key]+r.actual_travel_time(ts[0]))
1129+
#print("time:", r.actual_travel_time(ts[0]), "+", dep_time_actual[key], "=", dep_time_actual[key]+r.actual_travel_time(ts[0]), "; cost:", actual_cost)
1130+
chosen_cost = actual_cost
1131+
best_cost = actual_cost #現在の選択と最適選択のコスト差cost_gap算出用.均衡に近いとコスト差が小さい
1132+
1133+
for j,t1 in enumerate(s.time_windows[:-1]):
1134+
t2 = s.time_windows[j+1] #time window [t1, t2]
1135+
alt_t_dep = W.rng.random()*(t2-t1) + t1
1136+
1137+
#stabilization technique: departure time change is relatively rare. choose only 1 time slot on average
1138+
change_possibility_time = False
1139+
if random.random() < 1/len(s.time_windows[:-1]):
1140+
change_possibility_time = True
1141+
1142+
for alt_route in route_set[o,d]:
1143+
alt_cost = s.alpha*alt_route.actual_travel_time(alt_t_dep) + schedule_cost(alt_t_dep+alt_route.actual_travel_time(alt_t_dep))
1144+
1145+
#print("time:", s.alpha*alt_route.actual_travel_time(ts[0]), "+", alt_t_dep, "=", alt_t_dep+alt_route.actual_travel_time(ts[0]), "; cost:", alt_cost)
1146+
1147+
if alt_cost < chosen_cost:
1148+
if alt_cost < best_cost:
1149+
best_cost = alt_cost
1150+
1151+
if actual_cost - alt_cost > actual_cost*s.insensitive_ratio:
1152+
#stabilization technique: bounded rational behavior. insensitve to difference smaller than x%
1153+
flag_potential_change = 1
1154+
1155+
if change_possibility and change_possibility_time:
1156+
flag_changed = True
1157+
route_changed = alt_route
1158+
dep_t_changed = alt_t_dep
1159+
chosen_cost = alt_cost
1160+
1161+
total_cost_gap += actual_cost-best_cost
1162+
routes_specified[key] = r
1163+
dep_time_specified[key] = dep_time
1164+
if flag_potential_change:
1165+
potential_n_swap += W.DELTAN
1166+
if flag_changed:
1167+
n_swap += W.DELTAN
1168+
routes_specified[key] = route_changed
1169+
dep_time_specified[key] = dep_t_changed
1170+
1171+
veh.departure_time_choice_changed = dep_time_specified[key]
1172+
1173+
cost_gap_per_vehicle = total_cost_gap/len(W.VEHICLES)
1174+
if print_progress:
1175+
print(f' iter {i}: cost gap: {cost_gap_per_vehicle:.1f}, potential change: {potential_n_swap}, change: {n_swap}, total travel time: {W.analyzer.total_travel_time: .1f}, delay ratio: {W.analyzer.average_delay/W.analyzer.average_travel_time: .3f}')
1176+
1177+
if callback:
1178+
callback(i, W)
1179+
1180+
s.route_log.append(route_actual)
1181+
s.dep_t_log.append(dep_time_actual)
1182+
s.cost_log.append(cost_actual)
1183+
1184+
s.ttts.append(int(W.analyzer.total_travel_time))
1185+
s.n_swaps.append(n_swap)
1186+
s.potential_swaps.append(potential_n_swap)
1187+
s.cost_gaps.append(cost_gap_per_vehicle)
1188+
1189+
1190+
s.end_time = time.time()
1191+
1192+
print("DUE summary:")
1193+
last_iters = int(max_iter/4)
1194+
print(f" total travel time: initial {s.ttts[0]:.1f} -> average of last {last_iters} iters {np.average(s.ttts[-last_iters:]):.1f}")
1195+
print(f" number of potential changes: initial {s.potential_swaps[0]:.1f} -> average of last {last_iters} iters {np.average(s.potential_swaps[-last_iters:]):.1f}")
1196+
print(f" cost gap: initial {s.cost_gaps[0]:.1f} -> average of last {last_iters} iters {np.average(s.cost_gaps[-last_iters:]):.1f}")
1197+
print(f" computation time: {s.end_time - s.start_time:.1f} seconds")
1198+
1199+
s.W_sol = W
1200+
return s.W_sol
1201+
1202+
def plot_convergence(s):
1203+
# iteration plot
1204+
plt.figure(figsize=(6,2))
1205+
plt.title("total travel time")
1206+
plt.plot(s.ttts)
1207+
plt.xlabel("iter")
1208+
1209+
plt.figure(figsize=(6,2))
1210+
plt.title("number of route change")
1211+
plt.plot(s.n_swaps)
1212+
plt.ylim(0,None)
1213+
plt.xlabel("iter")
1214+
1215+
plt.figure(figsize=(6,2))
1216+
plt.title("travel time difference between choosen route and minimum cost route")
1217+
plt.plot(s.t_gaps)
1218+
plt.ylim(0,None)
1219+
plt.xlabel("iter")
1220+
1221+
plt.show()
1222+
1223+
# plt.figure()
1224+
# plot_multiple_y(ys=[s.ttts, s.n_swaps, s.potential_swaps, s.total_t_gaps, np.array(s.total_t_gaps)/np.array(s.potential_swaps)], labels=["total travel time", "number of route change", "number of potential route change", "time gap for potential route change", "time gap per potential route change"])
1225+
# plt.xlabel("iter")
1226+
# plt.show()
1227+
1228+
def plot_link_stats(s):
1229+
plt.figure()
1230+
plt.title("traffic volume")
1231+
for i in range(len(s.dfs_link[0])):
1232+
vols = [df["traffic_volume"][i] for df in s.dfs_link]
1233+
plt.plot(vols, label=s.dfs_link[0]["link"][i])
1234+
plt.xlabel("iteration")
1235+
plt.ylabel("volume (veh)")
1236+
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
1237+
1238+
plt.figure()
1239+
plt.title("average travel time")
1240+
for i in range(len(s.dfs_link[0])):
1241+
vols = [df["average_travel_time"][i] for df in s.dfs_link]
1242+
plt.plot(vols, label=s.dfs_link[0]["link"][i])
1243+
plt.xlabel("iteration")
1244+
plt.ylabel("time (s)")
1245+
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
1246+
1247+
plt.show()
1248+
8911249
def plot_vehicle_stats(s, orig=None, dest=None):
8921250
ave_TT = []
8931251
std_TT = []

0 commit comments

Comments
 (0)