Example 03 - Introduction to SPaT Data#
This example will introduce:
Load the parsed Signal Phase and Timing data with
mtldp.utils.data_io
Basic classes for the SPaT data
How to get SPaT information for a specific time period over a range of days at the corridor, intersection, and movement level
After the SPaT data is parsed from original CSV files, you can load the them using the data platform (mtldp
) package. The following flow chart illustrates how the data platform loads and manages the SPaT data.
[3]:
from mtldp.utils.common import show_attributes_and_methods
Load SPaT data#
Load region SPaT history from pickle file#
[4]:
from mtldp.utils.data_io import load_region_spat_from_pickle
region_spat = load_region_spat_from_pickle('data/spat.pickle')
[5]:
show_attributes_and_methods(region_spat)
attributes: {'version_history_df', 'ring_structure_df', 'region_id', 'timezone', 'tod_event_df', 'network_mapping_df', 'node_id_ls', 'node_controller_dict', 'controller_spat_history_dict', 'dial_split_df', 'program_week_df', 'program_day_df'}
methods: {'get_corridor_spat_period', 'get_controller_spat_period', 'get_controller_spat_history', 'get_tod_event_time_splits'}
Get SPaT history of one controller from region#
[6]:
node_id = region_spat.node_id_ls[0]
controller_spat = region_spat.get_controller_spat_history(node_id=node_id)
[7]:
show_attributes_and_methods(controller_spat)
attributes: {'version_history', 'version_history_df', 'ring_structure_df', 'phase_mappings', 'timezone', 'movement_mappings', 'tod_event_df', 'dial_split_versions', 'node_id_ls', 'tod_event_versions', 'dial_split_df', 'program_week_df', 'program_day_df', 'controller_id'}
methods: {'get_controller_spat_period', 'add_node', 'get_tod_event_time_splits', 'get_tod_event_starts'}
Get corridor SPaT information for a time period over a range of days#
[8]:
# specify date range
date_range = ["2022-03-07", "2022-03-08", "2022-03-09", "2022-03-10", "2022-03-11"]
# corridor name
name = 'adams_rd'
# specify nodes in the corridor
node_ls = ["62183487", "62300620", "61950324", "62344947", "62344935", "62355467", "62036287"]
# get TOD splits based on all the TOD boundaries from the nodes in this node list
tod_splits = region_spat.get_tod_event_time_splits(date_range, node_ls)
print(tod_splits)
[[0.0, 6.0], [6.0, 7.0], [7.0, 10.0], [10.0, 15.0], [15.0, 15.25], [15.25, 15.667], [15.667, 19.0], [19.0, 22.0], [22.0, 24]]
[9]:
# examine the first tod split
tod_split = tod_splits[0]
# get the corridor spat
corridor_period_spat = region_spat.get_corridor_spat_period(name, node_ls, date_range, tod_split)
WARNING: nodes have different cycle lengths for date range ['2022-03-07', '2022-03-08', '2022-03-09', '2022-03-10', '2022-03-11'] over time interval [0.0, 6.0]
{
"90": [
"62183487",
"62344947",
"62344935",
"62355467"
],
"0": [
"62300620",
"61950324",
"62036287"
]
}
A warning is raised because not all of the nodes have the same cycle length. Some of the nodes in this example have a flashing operation during the night hours, so the cycle is 0. This is also stored in the object.
[10]:
print(corridor_period_spat.same_cycle_lengths)
False
Now let’s get a corridor period when all the nodes have the same cycle length:
[11]:
# examine the third tod split
tod_split = tod_splits[2]
# get the corridor spat
corridor_period_spat = region_spat.get_corridor_spat_period(name, node_ls, date_range, tod_split)
# check that cycle length is the same
print(corridor_period_spat.same_cycle_lengths)
True
[12]:
show_attributes_and_methods(corridor_period_spat)
attributes: {'controller_spat_period_dict', 'timezone', 'date_ls', 'controller_cycle_lengths', 'corridor_id', 'node_id_ls', 'cycle_length', 'node_controller_dict', 'node_offsets', 'time_range', 'same_cycle_lengths'}
methods: {'get_movement_spat_period', 'get_controller_spat_period'}
[13]:
corridor_period_spat.node_offsets
[13]:
{'62183487': 37,
'62300620': 40,
'61950324': 89,
'62344947': 35,
'62344935': 35,
'62355467': 78,
'62036287': 69}
Get the period SPaT information for a node in the corridor#
[14]:
intersection_id = corridor_period_spat.node_id_ls[0]
controller_period_spat = corridor_period_spat.get_controller_spat_period(intersection_id)
show_attributes_and_methods(controller_period_spat)
attributes: {'next_phases', 'clearance_intervals', 'yellow_intervals', 'shift', 'controller_id', 'version_history', 'phase_mappings', 'ring_structure_df', 'movement_spat_period_dict', 'fixed_ends', 'date_ls', 'tod_events', 'phase_modes', 'node_id_ls', 'program_weeks', 'ring_phases', 'ped_channels', 'dial_splits', 'cycle', 'timezone', 'movement_mappings', 'fixed_starts', 'concurrent_phases', 'phase_starts', 'offset', 'program_days', 'channels', 'phase_splits', 'min_splits', 'time_range'}
methods: {'get_movement_spat_period'}
Get the period SPaT information for a movement in the corridor#
[16]:
# examine the SB movement
movement_index = 8
You can either retrieve the movement information from either the corridor or controller level
[17]:
movement_period_spat_from_corridor = corridor_period_spat.get_movement_spat_period(node_id, movement_index)
movement_period_spat_from_controller = controller_period_spat.get_movement_spat_period(node_id, movement_index)
movement_period_spat_from_corridor == movement_period_spat_from_controller
WARNING: node id 61950324 not in controller 279
[17]:
False
[19]:
movement_period_spat = movement_period_spat_from_corridor
movement_period_spat.__dict__
[19]:
{'movement_id': None,
'movement_index': 8,
'phase_ls': [2],
'fx_end': True,
'fx_start': True,
'green_start': 89,
'green_duration': 60,
'yellow': 3.5,
'clearance': 2.5}
The movement id needs to be matched from the map data
Get intersection SPaT information for a time period over a range of days#
You can also get an intersection’s SPaT information for a time period directly from the controller history
[20]:
tod_splits = controller_spat.get_tod_event_time_splits(date_range)
print(tod_splits)
[[0.0, 7.0], [7.0, 10.0], [10.0, 15.0], [15.0, 15.25], [15.25, 15.667], [15.667, 19.0], [19.0, 22.0], [22.0, 24]]
It is recommended that you call the controller SPaT period within these tod splits. Otherwise an error might be raised because of incompatible parameters.
[21]:
# try a different tod split
tod_split = [6, 8]
controller_period_spat = controller_spat.get_controller_spat_period(date_range, tod_split)
WARNING: time interval includes more than one programmed SPaT TOD on 2022-03-07 for controller 348 (nodes: ['61950324'])
[
[
0.0,
7.0
],
[
7.0,
10.0
]
]
WARNING: time interval includes more than one programmed SPaT TOD on 2022-03-08 for controller 348 (nodes: ['61950324'])
[
[
0.0,
7.0
],
[
7.0,
10.0
]
]
WARNING: time interval includes more than one programmed SPaT TOD on 2022-03-09 for controller 348 (nodes: ['61950324'])
[
[
0.0,
7.0
],
[
7.0,
10.0
]
]
WARNING: time interval includes more than one programmed SPaT TOD on 2022-03-10 for controller 348 (nodes: ['61950324'])
[
[
0.0,
7.0
],
[
7.0,
10.0
]
]
WARNING: time interval includes more than one programmed SPaT TOD on 2022-03-11 for controller 348 (nodes: ['61950324'])
[
[
0.0,
7.0
],
[
7.0,
10.0
]
]
WARNING: date range includesF more than one dial splits for controller 348 (nodes: ['61950324'])
{
"5:5": [
"2022-03-07_0.0-7.0",
"2022-03-08_0.0-7.0",
"2022-03-09_0.0-7.0",
"2022-03-10_0.0-7.0",
"2022-03-11_0.0-7.0"
],
"2:1": [
"2022-03-07_7.0-10.0",
"2022-03-08_7.0-10.0",
"2022-03-09_7.0-10.0",
"2022-03-10_7.0-10.0",
"2022-03-11_7.0-10.0"
]
}
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In[21], line 4
1 # try a different tod split
2 tod_split = [6, 8]
----> 4 controller_period_spat = controller_spat.get_controller_spat_period(date_range, tod_split)
File d:\mtldp_project\mtldp-meta-utils\mtldp\meta\SPaT\HistoryControllerSPaT.py:125, in HistoryControllerSPaT.get_controller_spat_period(self, date_ls, time_range)
122 program_days = self._get_program_days(program_weeks)
123 dial_splits, tod_events = self._get_dial_splits_and_tod_events(program_days, time_range,
124 version_history['tod_events'])
--> 125 parameter_set = self._get_timing_parameters(dial_splits, version_history['dial_splits'], time_range)
126 shift = self._get_shift(time_range, parameter_set)
127 return PeriodControllerSPaT(self.controller_id, self.node_id_ls, time_range, ring_structure_df,
128 self.phase_mappings, self.movement_mappings, date_ls, version_history,
129 program_weeks, program_days, dial_splits, tod_events, parameter_set, shift)
File d:\mtldp_project\mtldp-meta-utils\mtldp\meta\SPaT\HistoryControllerSPaT.py:317, in HistoryControllerSPaT._get_timing_parameters(self, dial_splits, dial_split_versions, time_range)
313 parameter_set[dial_split] = {"phase_splits": dial_split_info.phase_splits,
314 "cycle": dial_split_info.cycle, "offset": dial_split_info.offset,
315 "phase_modes": dial_split_info.phase_modes}
316 final_parameter_set = parameter_set[dial_split]
--> 317 self._uniform_info(parameter_set,
318 note=f'timing parameter set (phase splits, cycle, offset, phase modes) for time interval '
319 f'{time_range}', fatal=True)
320 return final_parameter_set
File d:\mtldp_project\mtldp-meta-utils\mtldp\meta\SPaT\HistoryControllerSPaT.py:342, in HistoryControllerSPaT._uniform_info(self, a_dict, note, fatal)
340 if len(group_dict) > 1:
341 if fatal:
--> 342 raise Exception(f"FATAL: date range includes more than one {note} for controller {self.controller_id} "
343 f"(nodes: {self.node_id_ls}) \n"
344 f"{json.dumps(group_dict, indent=1)}")
345 else:
346 print(f"WARNING: date range includes more than one {note} for controller {self.controller_id} "
347 f"(nodes: {self.node_id_ls}) \n"
348 f"{json.dumps(group_dict, indent=1)}")
Exception: FATAL: date range includes more than one timing parameter set (phase splits, cycle, offset, phase modes) for time interval [6, 8] for controller 348 (nodes: ['61950324'])
{
"{'phase_splits': {1: -1, 2: 0, 3: -1, 4: 0, 5: -1, 6: -1, 7: -1, 8: -1}, 'cycle': 0, 'offset': 0, 'phase_modes': {1: -1, 2: 8, 3: -1, 4: 9, 5: -1, 6: -1, 7: -1, 8: -1}}": [
"5:5"
],
"{'phase_splits': {1: -1, 2: 60, 3: -1, 4: 30, 5: -1, 6: -1, 7: -1, 8: -1}, 'cycle': 90, 'offset': 89, 'phase_modes': {1: -1, 2: 1, 3: -1, 4: 7, 5: -1, 6: -1, 7: -1, 8: -1}}": [
"2:1"
]
}
In this case, there are different cycle lengths within this time range, so 6:00 AM to 8:00 AM is not a viable TOD split for this date range.
[22]:
# try a TOD split within the ones generated before
tod_split = [8.5, 9.5]
controller_period_spat = controller_spat.get_controller_spat_period(date_range, tod_split)
[23]:
show_attributes_and_methods(controller_period_spat)
attributes: {'next_phases', 'clearance_intervals', 'yellow_intervals', 'shift', 'controller_id', 'version_history', 'phase_mappings', 'ring_structure_df', 'movement_spat_period_dict', 'fixed_ends', 'date_ls', 'tod_events', 'phase_modes', 'node_id_ls', 'program_weeks', 'ring_phases', 'ped_channels', 'dial_splits', 'cycle', 'timezone', 'movement_mappings', 'fixed_starts', 'concurrent_phases', 'phase_starts', 'offset', 'program_days', 'channels', 'phase_splits', 'min_splits', 'time_range'}
methods: {'get_movement_spat_period'}
[24]:
controller_period_spat.phase_starts
[24]:
{2: 0, 4: 60}