def test_json_back_and_forth(self) -> None: """ Test converting back and forth from and to json """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN intersection = Intersection(**input_dict) # THEN converting back and forth should in the end give the same result intersection_dict = intersection.to_json() intersection_from_json = Intersection.from_json(intersection_dict=intersection_dict) self.assertDictEqual(intersection_dict, intersection_from_json.to_json())
def test_getting_non_existing_signal_group(self): """ Test retrieving signal group by id when this id does not exist """ # GIVEN signalgroup1 = SignalGroup(id="sg1", traffic_lights=[TrafficLight(capacity=1800, lost_time=1)], min_greenyellow=10, max_greenyellow=80, min_red=10, max_red=80) signalgroup2 = SignalGroup(id="sg2", traffic_lights=[TrafficLight(capacity=1800, lost_time=1)], min_greenyellow=10, max_greenyellow=80, min_red=10, max_red=80) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2], conflicts=[], sync_starts=[], offsets=[], greenyellow_leads=[]) with self.assertRaises(ValueError): # WHEN intersection.get_signalgroup(signalgroup_id="sg3")
def test_getting_signal_group(self): """ Test retrieving signal group by id """ # GIVEN signalgroup1 = SignalGroup(id="sg1", traffic_lights=[TrafficLight(capacity=1800, lost_time=1)], min_greenyellow=10, max_greenyellow=80, min_red=10, max_red=80) signalgroup2 = SignalGroup(id="sg2", traffic_lights=[TrafficLight(capacity=1800, lost_time=1)], min_greenyellow=10, max_greenyellow=80, min_red=10, max_red=80) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2], conflicts=[], sync_starts=[], offsets=[], greenyellow_leads=[]) # WHEN/THEN self.assertEqual(intersection.get_signalgroup(signalgroup_id="sg1"), signalgroup1) self.assertEqual(intersection.get_signalgroup(signalgroup_id="sg2"), signalgroup2)
def get_default_intersection( additional_signalgroups: Optional[List[SignalGroup]] = None, additional_conflicts: Optional[List[Conflict]] = None, ) -> Intersection: """ Get a default intersection object with 2 conflicting signal groups "sg1" and "sg2" :param additional_signalgroups: signal groups to add to the intersection (besides signal group 'sg1' and 'sg2') :param additional_conflicts: additional conflicts to add (besides the conflict between signal group 'sg1' and 'sg2') :return: the intersection object """ if additional_signalgroups is None: additional_signalgroups = [] if additional_conflicts is None: additional_conflicts = [] signalgroup1 = TestFTSValidationOfBounds.get_default_signalgroup( name="sg1") signalgroup2 = TestFTSValidationOfBounds.get_default_signalgroup( name="sg2") conflict = Conflict(id1="sg1", id2="sg2", setup12=2, setup21=3) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2] + additional_signalgroups, conflicts=[conflict] + additional_conflicts) return intersection
def test_multiple_relations(self) -> None: """ Test multiple relations being provided for the same pair of signal groups """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN two signal group relations exist for the same signal group pair id1 = "new_id1" id2 = "new_id2" for key1, key2 in product(["conflicts", "sync_starts", "offsets", "greenyellow_leads"], ["conflicts", "sync_starts", "offsets", "greenyellow_leads"]): if key1 == key2: continue if key1 == "conflicts": input_dict[key1][0].id1 = id1 input_dict[key1][0].id2 = id2 else: input_dict[key1][0].from_id = id2 input_dict[key1][0].to_id = id1 if key2 == "conflicts": input_dict[key2][0].id1 = id1 input_dict[key2][0].id2 = id2 else: input_dict[key2][0].from_id = id2 input_dict[key2][0].to_id = id1 with self.subTest(f"Two relations ('{key1}' and '{key2}') for same signalgroup pair"): with self.assertRaises(ValueError): Intersection(**input_dict)
def test_conflict_satisfied(self) -> None: """ test that validations pass if constraints are satisfied :return: """ # GIVEN signalgroup1 = TestFTSConflictValidation.get_default_signalgroup( name="sg1") signalgroup2 = TestFTSConflictValidation.get_default_signalgroup( name="sg2") conflict = Conflict(id1="sg1", id2="sg2", setup12=2, setup21=3) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2], conflicts=[conflict]) fts = FixedTimeSchedule(greenyellow_intervals=dict( sg1=[ GreenYellowInterval(start_greenyellow=90, end_greenyellow=10), GreenYellowInterval(start_greenyellow=33, end_greenyellow=60) ], sg2=[ GreenYellowInterval(start_greenyellow=12, end_greenyellow=30), GreenYellowInterval(start_greenyellow=62, end_greenyellow=87) ]), period=100) for interval_shift in range(2): with self.subTest(f"interval_shift={interval_shift}"): fts._greenyellow_intervals["sg2"] = \ fts._greenyellow_intervals["sg2"][:interval_shift] + \ fts._greenyellow_intervals["sg2"][interval_shift:] # WHEN validating validate_conflicts(intersection=intersection, fts=fts)
def test_successful_validation(self) -> None: """ Test initializing Intersection object with correct input """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN Intersection(**input_dict)
def _expect_valid_fixed_order(self, expect_valid: bool = True, shift: float = 0) -> None: # assume all signalgroups are conflicting for this test conflicts = [] for signalgroup1, signalgroup2 in combinations(self._signal_groups, 2): if signalgroup1 == signalgroup2: continue conflicts.append( Conflict(id1=signalgroup1.id, id2=signalgroup2.id, setup12=1, setup21=1)) intersection = Intersection(signalgroups=self._signal_groups, conflicts=conflicts, periodic_orders=self._fixed_orders) fts_dict = { "period": self._period, "greenyellow_intervals": { name: [[(t + shift) % self._period for t in interval] for interval in intervals] for name, intervals in self._greenyellow_intervals.items() } } fts = FixedTimeSchedule.from_json(fts_dict=fts_dict) valid = True try: validate_fixed_orders(intersection=intersection, fts=fts) except SafetyViolation: valid = False self.assertEqual(valid, expect_valid)
def test_ids_not_unique(self) -> None: """ Test for multiple signal groups having the same id """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN an id is used twice input_dict["signalgroups"][-1].id = "sg1" # other object (of wrong type) to the list with self.assertRaises(ValueError): Intersection(**input_dict)
def evaluate_fixed_time_schedule(print_fixed_time_schedule: bool = False): """ In this example we show how to evaluate a fixed-time schedule. Use case: comparing two fixed-time schedules (perhaps not optimized via this api) on expected performance. NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). You can download this file (example_smd_export.json) from https://github.com/stijnfleuren/SwiftCloudApi/tree/master/swift_cloud_py/examples """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join( os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json( intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json( arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing delay") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, horizon=2) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram) # this should return the same estimated delay. With this functionality we could evaluate the expected performance # of any fixed-time schedule. logging.info(f"Evaluate this schedule") kpis = SwiftMobilityCloudApi.evaluate_fts( intersection=intersection, fixed_time_schedule=fixed_time_schedule, arrival_rates=arrival_rates, horizon=2) logging.info(f"Output: {kpis}")
def test_subsequent_non_conflicting_ids_in_periodic_order(self): # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN input_dict["periodic_orders"] = [PeriodicOrder(["sg1", "sg2", "sg3"])] with self.assertRaises(ValueError): Intersection(**input_dict)
def test_unknown_ids_in_periodic_order(self): # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN input_dict["periodic_orders"] = [PeriodicOrder(["sg1", "sg2", "unknown_id"])] with self.assertRaises(ValueError): Intersection(**input_dict)
def optimize_multiple_schedules(): """ Example showing how to: - retrieve intersection information and arrival rates from a json file exported from Swift Mobility Desktop. - use this information to optimize fixed-time schedules NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). This functionality is tested with Swift Mobility Desktop 0.7.0.alpha. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join( os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json( intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json( arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing average experienced delay") best_fixed_time_schedule, best_phase_diagram, objective_value, warm_start_info = \ SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") logging.info(best_fixed_time_schedule) logging.info(best_phase_diagram) logging.info(f"Finding second best schedule") second_best_fixed_time_schedule, second_best_phase_diagram, objective_value, warm_start_info = \ SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, fixed_time_schedules_to_exclude=[best_fixed_time_schedule], warm_start_info=warm_start_info) logging.info( f"Average experienced delay of second best schedule: {objective_value:.2f} seconds" ) logging.info(second_best_fixed_time_schedule) logging.info(second_best_phase_diagram)
def test_wrong_type_in_list(self) -> None: """ Test providing the wrong type of elements inside the arguments (which are lists) """ for key in TestInputValidation.get_default_inputs(): with self.subTest(f"Wrong type in input '{key}'"): # GIVEN input_dict = TestInputValidation.get_default_inputs() input_dict[key].append(10) # add other object (of wrong type) to the list with self.assertRaises(TypeError): # WHEN initializing the intersection Intersection(**input_dict)
def test_setup_to_small(self) -> None: """ Test for setup time being too small """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN setup12 plus min_greenyellow of signal group sg1 is not strictly positive for setup12 in [-10, -11]: # equal to min_greenyellow or even smaller with self.subTest(f"setup too small: '{setup12}'"): input_dict["conflicts"][0].setup12 = setup12 with self.assertRaises(ValueError): Intersection(**input_dict)
def test_wrong_type(self) -> None: """ Test providing the wrong type of arguments (no list)""" # WHEN an input contains the wrong data type for key in TestInputValidation.get_default_inputs(): with self.subTest(f"Wrong type in input '{key}'"): # GIVEN input_dict = TestInputValidation.get_default_inputs() input_dict[key] = 10 # wrong type (not a list) with self.assertRaises(TypeError): # WHEN initializing the intersection Intersection(**input_dict)
def evaluate_fts( cls, intersection: Intersection, arrival_rates: ArrivalRates, fixed_time_schedule: FixedTimeSchedule, horizon: float = 2.0, initial_queue_lengths: Optional[QueueLengths] = None) -> KPIs: """ Evaluate a fixed-time schedule; returns KPIs (estimated delay experienced by road users and the capacity ( see also KPIs and the SwiftMobilityCloudApi.get_optimized_fts() method for their definition. :param intersection: intersection for which to optimize the fts (contains signal groups, conflicts and more) :param arrival_rates: arrival rates; each arrival rate is specified in personal car equivalent per hour (PCE/h) cyclists per hour or pedestrians per hour :param fixed_time_schedule: :param initial_queue_lengths: initial amount of traffic waiting at each of the traffic lights; if None, then we assume no initial traffic. :param horizon: time period of interest in hours. :return KPIs, which are the estimated """ assert horizon >= 1, HORIZON_LB_EXCEEDED_MSG if initial_queue_lengths is None: # assume no initial traffic initial_queue_lengths = QueueLengths({ signalgroup.id: [0] * len(signalgroup.traffic_lights) for signalgroup in intersection.signalgroups }) check_all_arrival_rates_and_queue_lengths_specified( intersection=intersection, arrival_rates=arrival_rates, initial_queue_lengths=initial_queue_lengths) endpoint = f"{CLOUD_API_URL}/fts-evaluation" headers = SwiftMobilityCloudApi.get_authentication_header() # rest-api call try: # assume that the traffic that is initially present arrives during the horizon. corrected_arrival_rates = arrival_rates + initial_queue_lengths / horizon json_dict = dict(intersection=intersection.to_json(), arrival_rates=corrected_arrival_rates.to_json(), fixed_time_schedule=fixed_time_schedule.to_json()) logging.debug(f"calling endpoint {endpoint}") r = requests.post(endpoint, json=json_dict, headers=headers) logging.debug(f"finished calling endpoint {endpoint}") except requests.exceptions.ConnectionError: raise UnknownCloudException(CONNECTION_ERROR_MSG) # check for errors check_status_code(response=r) return KPIs.from_json(r.json())
def test_unknown_ids(self) -> None: """ Test unknown ids being used in relations between signal groups """ # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN an unknown id is used in a relations between signal groups for key in ["conflicts", "sync_starts", "offsets", "greenyellow_leads"]: if key == "conflicts": input_dict[key][0].id1 = "unknown" else: input_dict[key][0].from_id = "unknown" with self.subTest(f"Unknown id used in input '{key}'"): with self.assertRaises(ValueError): Intersection(**input_dict)
def test_violating_conflict(self) -> None: """ test that validations fails if minimum clearance times are violated. """ # GIVEN signalgroup1 = TestFTSConflictValidation.get_default_signalgroup( name="sg1") signalgroup2 = TestFTSConflictValidation.get_default_signalgroup( name="sg2") conflict = Conflict(id1="sg1", id2="sg2", setup12=2, setup21=3) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2], conflicts=[conflict]) fts = FixedTimeSchedule(greenyellow_intervals=dict( sg1=[ GreenYellowInterval(start_greenyellow=90, end_greenyellow=10), GreenYellowInterval(start_greenyellow=33, end_greenyellow=60) ], sg2=[ GreenYellowInterval(start_greenyellow=12, end_greenyellow=30), GreenYellowInterval(start_greenyellow=62, end_greenyellow=87) ]), period=100) for signalgroup_id, interval_index, interval_shift in product( ["sg1", "sg2"], [0, 1], [0]): with self.subTest( f"signalgroup_id={signalgroup_id}, interval_index={interval_index}, " f"interval_shift={interval_shift}"): # adjusting schedule such that the start of greenyellow interval 'interval_index' of signalgroup_id # violates the minimum clearance time fts_copy = deepcopy(fts) if signalgroup_id == "sg1": fts_copy._greenyellow_intervals["sg1"][interval_index].start_greenyellow = \ (fts_copy._greenyellow_intervals["sg2"][(interval_index + 1) % 2].end_greenyellow + conflict.setup21 - 1) % fts_copy.period if signalgroup_id == "sg2": fts_copy._greenyellow_intervals["sg2"][interval_index].start_greenyellow = \ (fts_copy._greenyellow_intervals["sg1"][interval_index].end_greenyellow + conflict.setup12 - 1) % fts_copy.period fts_copy._greenyellow_intervals["sg2"] = fts_copy._greenyellow_intervals["sg2"][:interval_shift] + \ fts_copy._greenyellow_intervals["sg2"][interval_shift:] with self.assertRaises(SafetyViolation): # WHEN validating validate_conflicts(intersection=intersection, fts=fts_copy)
def get_phase_diagram( cls, intersection: Intersection, fixed_time_schedule: FixedTimeSchedule) -> PhaseDiagram: """ Get the phase diagram specifying the order in which the signal groups have their greenyellow intervals in the fixed-time schedule :param intersection: intersection for which to optimize the fts (contains signal groups, conflicts and more) :param fixed_time_schedule: fixed-time schedule for which we want to retrieve the phase diagram. :return: the associated phase diagram IMPORTANT: we try to start the greenyellow intervals of two signal groups that are subject to a synchronous start or a greenyellow-lead in the same phase; however, if this is not possible for all such pairs, then we try to satisfy it for as many such pairs as possible. For example consider the following theoretical problem where we have three signal groups: sg1, sg2, and sg3. sg1 conflicts with sg2 and sg3. sg2 has a greenyellow_lead(min=30, max=50) w.r.t. sg3. The following schedule is feasible greenyellow_intervals = {"sg1": [[0, 10], [40, 50]], "sg2": [[20, 30]], "sg3": [[60,70]]} period=80 However, it is not possible to find a phase diagram where sg2 and sg3 start in the same phase; only the following phase diagram is possible: [[["sg1", 0]], [["sg2", 0]], [["sg1", 1]], [["sg3", 0]]] """ endpoint = f"{CLOUD_API_URL}/phase-diagram-computation" headers = SwiftMobilityCloudApi.get_authentication_header() # rest-api call try: json_dict = dict( intersection=intersection.to_json(), greenyellow_intervals=fixed_time_schedule.to_json() ["greenyellow_intervals"], period=fixed_time_schedule.to_json()["period"]) logging.debug(f"calling endpoint {endpoint}") r = requests.post(endpoint, json=json_dict, headers=headers) logging.debug(f"finished calling endpoint {endpoint}") except requests.exceptions.ConnectionError: raise UnknownCloudException(CONNECTION_ERROR_MSG) # check for errors check_status_code(response=r) output = r.json() # parse output phase_diagram = PhaseDiagram.from_json(output["phase_diagram"]) return phase_diagram
def minimizing_period_duration(print_fixed_time_schedule: bool = False): """ In this example (given a traffic scenario) we search for the fixed-time schedule with the smallest period duration for which none of the traffic lights are oversatured. NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). This functionality is tested with Swift Mobility Desktop 0.7.0.alpha. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join( os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json( intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json( arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing period duration") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_period) logging.info(f"Minimized period duration: {objective_value:.2f} seconds") if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram)
def load_from_smd_and_run(): """ Example showing how to: - retrieve intersection information and arrival rates from a json file exported from Swift Mobility Desktop. - use this information to optimize fixed-time schedules NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). This functionality is tested with Swift Mobility Desktop 0.7.0.alpha. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join(os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json(intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json(arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing average experienced delay") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") logging.info(fixed_time_schedule) logging.info(phase_diagram) # the following code indicates how to compute a phase diagram from a fixed-time schedule (note that now it makes # no sense to do so as it was already computed above) logging.info("Computing phase diagram from fixed-time schedule. Should be the same as before") phase_diagram = SwiftMobilityCloudApi.get_phase_diagram(intersection=intersection, fixed_time_schedule=fixed_time_schedule) logging.info(phase_diagram)
def test_other_relations(self) -> None: """ Test if the attribute other_relation containers all other relations (sync starts, offsets and greenyellow-leads)""" # GIVEN input_dict = TestInputValidation.get_default_inputs() # WHEN an unknown id is used in a relations between signal groups intersection = Intersection(**input_dict) other_relations = intersection.other_relations # THEN other_relations is the list of all sync_starts, offsets and greenyellow-leads self.assertEqual(len(input_dict["sync_starts"]) + len(input_dict["offsets"]) + len(input_dict["greenyellow_leads"]) + len(input_dict["greenyellow_trails"]), len(other_relations)) for key in ["sync_starts", "offsets", "greenyellow_leads"]: with self.subTest(f"'{key}' in other_relations"): for other_relation in input_dict[key]: self.assertIn(other_relation, other_relations)
def validate_fixed_order(intersection: Intersection, fts: FixedTimeSchedule, periodic_order: PeriodicOrder) -> None: """ Validate that the the signalgroups indeed receive their greenyellow intervals in the requested periodic order (for only the periodic order that is given as argument). :return: - :raises SafetyException: if the requested order is not satisfied""" first_signalgroup = intersection.get_signalgroup( signalgroup_id=periodic_order.order[0]) first_interval_start = fts.get_greenyellow_interval(first_signalgroup, k=0).start_greenyellow prev_switch = 0 for signalgroup in periodic_order.order: for interval in fts.get_greenyellow_intervals(signalgroup): # shift schedule such that first greenyellow interval of the first signalgroup in the order starts at time=0 switch = (interval.start_greenyellow - first_interval_start + EPSILON) % fts.period - EPSILON if switch < prev_switch: raise SafetyViolation( f"Periodic order {periodic_order.to_json()} is violated") prev_switch = switch
def get_default_intersection(additional_signalgroups: Optional[List[SignalGroup]] = None, sync_starts: List[SyncStart] = None, offsets: List[Offset] = None, greenyellow_leads: List[GreenyellowLead] = None, greenyellow_trails: List[GreenyellowTrail] = None, ) -> Intersection: """ Get a default intersection object with 2 conflicting signal groups "sg1" and "sg2" :param additional_signalgroups: signal groups to add to the intersection (besides signal group 'sg1' and 'sg2') :param sync_starts: SyncStarts that must be satisfied :param offsets: Coordinations that must be satisfied :param greenyellow_leads: GreenyellowLeads that must be satisfied :param greenyellow_trails: GreenyellowTrails that must be satisfied :return: the intersection object """ if additional_signalgroups is None: additional_signalgroups = [] if sync_starts is None: sync_starts = [] if offsets is None: offsets = [] if greenyellow_leads is None: greenyellow_leads = [] if greenyellow_trails is None: greenyellow_trails = [] signalgroup1 = TestFTSOtherSGRelationValidation.get_default_signalgroup(name="sg1") signalgroup2 = TestFTSOtherSGRelationValidation.get_default_signalgroup(name="sg2") conflict = Conflict(id1="sg1", id2="sg2", setup12=2, setup21=3) intersection = Intersection(signalgroups=[signalgroup1, signalgroup2] + additional_signalgroups, conflicts=[conflict], sync_starts=sync_starts, offsets=offsets, greenyellow_leads=greenyellow_leads, greenyellow_trails=greenyellow_trails) return intersection
def minimizing_delay(print_fixed_time_schedule: bool = False): """ In this example (given a traffic scenario) we search for the fixed-time schedule that minimizes the average delay that road uses (arriving during an horizon of 2 hours) are expected to experience at the intersection. Use case: This enables real-time optimization of traffic light control allowing truly dynamic and smart traffic light control that automatically adapts to the traffic situation. For example, by periodically computing the optimal fixed-time schedule, and automatically converting it to a vehicle-actuated controller (e.g., using the green times as maximum green times and allowing green times to be terminated prematurely). NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). This functionality is tested with Swift Mobility Desktop 0.7.0.alpha. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join( os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json( intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json( arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing delay") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, horizon=2) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram) # intersection becomes oversaturated scaling_factor = 1.3 logging.info( f"Increasing original amount of traffic with {(scaling_factor - 1) * 100:.2f}%" ) arrival_rates *= scaling_factor initial_queue = 25 logging.info( f"Adding initial queue of {initial_queue: d} PCE/cyclists/pedestrians to each queue" ) id_to_queue_lengths = dict() for signalgroup in intersection.signalgroups: id_to_queue_lengths[signalgroup.id] = [ initial_queue for _ in signalgroup.traffic_lights ] initial_queue_lengths = QueueLengths( id_to_queue_lengths=id_to_queue_lengths) logging.info(f"Minimizing delay") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, horizon=2, initial_queue_lengths=initial_queue_lengths) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram)
def tune_fixed_time_schedule(print_fixed_time_schedule: bool = False): """ In this example we show how to tune a fixed-time schedule. Use case: Traffic situations change throughout the day. This function allows you to quickly adapt the green times of an existing fixed-time schedule to a new traffic situations. This can be used, for example, to adapt the maximum greenyellow times of a smart traffic light controller to the current traffic situation in real-time. NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. In this example, we load an intersection from disk (export of Swift Mobility Desktop). This functionality is tested with Swift Mobility Desktop 0.7.0.alpha. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # absolute path to .json file that has been exported from swift mobility desktop smd_export = os.path.join( os.path.join(os.path.abspath(__file__), os.pardir), "example_smd_export.json") # retrieve the json structure from the file with open(smd_export, "r") as f: json_dict = json.load(f) logging.info(f"Loading intersection and traffic situation from disk") intersection = Intersection.from_json( intersection_dict=json_dict["intersection"]) arrival_rates = ArrivalRates.from_json( arrival_rates_dict=json_dict["arrival_rates"]) logging.info(f"Loaded intersection and traffic situation from disk") logging.info(f"Minimizing delay") fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, horizon=2) logging.info(f"Average experienced delay: {objective_value:.2f} seconds") if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram) # the more the traffic situation changes, the more effect tuning the fixed-time schedule has. In this example, # we only scale the amount of traffic. for scaling_factor in [0.95, 0.9, 0.7, 0.4]: logging.info( f"Evaluating schedule for situation with {(1-scaling_factor)*100 :.1f}% less traffic" ) arrival_rates_scaled = arrival_rates * scaling_factor kpis = SwiftMobilityCloudApi.evaluate_fts( intersection=intersection, fixed_time_schedule=fixed_time_schedule, arrival_rates=arrival_rates_scaled, horizon=2) logging.info( f"Average experienced delay without tuning: {kpis.delay:.2f} seconds" ) logging.info( f"Tuning schedule for situation with {(1-scaling_factor)*100 :.1f}% less traffic" ) tuned_fixed_time_schedule, objective_value = SwiftMobilityCloudApi.get_tuned_fts( intersection=intersection, fixed_time_schedule=fixed_time_schedule, arrival_rates=arrival_rates_scaled, min_period_duration=30, max_period_duration=180, objective=ObjectiveEnum.min_delay, horizon=2) logging.info( f"Average experienced delay after tuning: {objective_value:.2f} seconds" ) if print_fixed_time_schedule: logging.info(fixed_time_schedule) logging.info(phase_diagram)
def create_intersection_and_optimize(): """ Example showing how to: - create traffic lights, signal groups and intersections, ... - optimize a fixed-time schedule for this intersection NOTE: To run the example below you need credentials to invoke the swift mobility cloud api. To this end, you need to specify the following environment variables: - smc_api_key: the access key of your swift mobility cloud api account - smc_api_secret: the secret access key of your swift mobility cloud api account If you do not have such an account yet, please contact [email protected]. """ logging.info(f"Running example '{os.path.basename(__file__)}'") # signal group consisting of two traffic light allowing 1 or 2 greenyellow intervals per repeating period. traffic_light1 = TrafficLight(capacity=1800, lost_time=2.2) traffic_light2 = TrafficLight(capacity=1810, lost_time=2.1) signalgroup1 = SignalGroup(id="2", traffic_lights=[traffic_light1, traffic_light2], min_greenyellow=10, max_greenyellow=100, min_red=10, max_red=100, min_nr=1, max_nr=2) # signal group consisting of one traffic light allowing 1 greenyellow interval (default) per repeating period. traffic_light3 = TrafficLight(capacity=1650, lost_time=3.0) signalgroup2 = SignalGroup(id="5", traffic_lights=[traffic_light3], min_greenyellow=10, max_greenyellow=100, min_red=10, max_red=100) # signal group consisting of one traffic light allowing 1 greenyellow interval (default) per repeating period. traffic_light4 = TrafficLight(capacity=1800, lost_time=2.1) signalgroup3 = SignalGroup(id="8", traffic_lights=[traffic_light4], min_greenyellow=10, max_greenyellow=100, min_red=10, max_red=100) # conflicts & clearance times conflict12 = Conflict(id1=signalgroup1.id, id2=signalgroup2.id, setup12=1, setup21=2) conflict13 = Conflict(id1=signalgroup1.id, id2=signalgroup3.id, setup12=1, setup21=2) conflict23 = Conflict(id1=signalgroup2.id, id2=signalgroup3.id, setup12=2, setup21=3) # initialize intersection object intersection = Intersection( signalgroups=[signalgroup1, signalgroup2, signalgroup3], conflicts=[conflict12, conflict13, conflict23]) # set associated arrival rates arrival_rates = ArrivalRates(id_to_arrival_rates={ "2": [800, 700], "5": [150], "8": [180] }) logging.info(f"Minimizing average experienced delay") # optimize fixed-time schedule fixed_time_schedule, phase_diagram, objective_value, _ = SwiftMobilityCloudApi.get_optimized_fts( intersection=intersection, arrival_rates=arrival_rates, objective=ObjectiveEnum.min_delay) logging.info(f"Average experienced delay {objective_value: .3f} seconds") logging.info(fixed_time_schedule) logging.info(phase_diagram) # the following code indicates how to compute a phase diagram from a fixed-time schedule (note that now it makes # no sense to do so as it was already computed above) logging.info( "Computing phase diagram from fixed-time schedule. Should be the same as before" ) phase_diagram = SwiftMobilityCloudApi.get_phase_diagram( intersection=intersection, fixed_time_schedule=fixed_time_schedule) logging.info(phase_diagram)
def get_optimized_fts( cls, intersection: Intersection, arrival_rates: ArrivalRates, horizon: float = 2.0, min_period_duration: float = 0.0, max_period_duration: float = 180, objective: ObjectiveEnum = ObjectiveEnum.min_delay, initial_queue_lengths: Optional[QueueLengths] = None, fixed_time_schedules_to_exclude: Optional[ List[FixedTimeSchedule]] = None, warm_start_info: Optional[Dict] = None, ) -> Tuple[FixedTimeSchedule, PhaseDiagram, float, dict]: """ Optimize a fixed-time schedule :param intersection: intersection for which to optimize the fts (contains signal groups, conflicts and more) :param arrival_rates: arrival rates; each arrival rate is specified in personal car equivalent per hour (PCE/h) cyclists per hour or pedestrians per hour :param horizon: time period of interest in hours. :param min_period_duration: minimum period duration of the fixed-time schedule in seconds :param max_period_duration: minimum period duration of the fixed-time schedule in seconds :param objective: what kpi (key performance indicator) to optimize. The following options are available: - ObjectiveEnum.min_delay: minimize the delay experienced by road users arriving at the intersection during the next 'horizon' hours. The initially waiting traffic is modeled as implicitly by increasing the arrival rate by initial_queue_length / horizon PCE/h; this implies that we assume that this traffic is arriving (evenly spread) during the horizon. - ObjectiveEnum.min_period: search for the fixed-time schedule with the smallest period duration for which all traffic lights are 'stable', i.e., the greenyellow interval is large enough so that the amount of traffic that can (on average) depart during the horizon exceeds the traffic that arrives during the horizon (+ initially waiting traffic). - ObjectiveEnum.max_capacity: search for the fixed-time schedule that can handle the largest (percentual) increase in traffic (including the initial amount of traffic), i.e., the largest percentual increase in traffic for which all traffic lights are 'stable' (see also ObjectiveEnum.min_period). This objective function disregards the maximum saturation of each traffic light (we assume the maximum saturation is 1 for each traffic light). :param initial_queue_lengths: initial amount of traffic waiting at each of the traffic lights; if None, then we assume no initial traffic. The unit of each queue-length should align with the unit used for the arrival rate; if the arrival rate is specified in PCE/h then the queue-length needs to be specified in PCE. :return: fixed-time schedule, associated phase diagram and the objective value (minimized delay, minimized period, or maximum percentual increase in traffic divided by 100, e.g. 1 means currently at the verge of stability) :param fixed_time_schedules_to_exclude: the fixed-time schedules that we want to exclude; this can be used to find the second best schedule by excluding the best one. :param warm_start_info: each optimization returns some information (usually in the format {"id": "some identification string"}); if you want to compute the second best schedule (by excluding the best schedule), then you can also provide the warm_start_info returned with the best schedule; this will significantly speedup computations when trying to find the second best one. """ assert horizon >= 1, HORIZON_LB_EXCEEDED_MSG if initial_queue_lengths is None: # assume no initial traffic initial_queue_lengths = QueueLengths({ signalgroup.id: [0] * len(signalgroup.traffic_lights) for signalgroup in intersection.signalgroups }) check_all_arrival_rates_and_queue_lengths_specified( intersection=intersection, arrival_rates=arrival_rates, initial_queue_lengths=initial_queue_lengths) if fixed_time_schedules_to_exclude is not None: for fixed_time_schedule in fixed_time_schedules_to_exclude: try: validate_safety_restrictions( intersection=intersection, fixed_time_schedule=fixed_time_schedule) except SafetyViolation as e: logging.error( f"One of the fixed-time schedules in fixed_time_schedules_to_exclude' does not" f"satisfy all safety restrictions. The violation: {e}") raise SafetyViolation(e) endpoint = f"{CLOUD_API_URL}/fts-optimization" headers = SwiftMobilityCloudApi.get_authentication_header() # rest-api call try: # assume that the traffic that is initially present arrives during the horizon. corrected_arrival_rates = arrival_rates + initial_queue_lengths / horizon json_dict = dict( intersection=intersection.to_json(), arrival_rates=corrected_arrival_rates.to_json(), min_period_duration=min_period_duration, max_period_duration=max_period_duration, objective=objective.value, ) if fixed_time_schedules_to_exclude is not None: json_dict["fts_to_exclude"] = [ fts.to_json() for fts in fixed_time_schedules_to_exclude ] if warm_start_info is not None: json_dict["warm_start_info"] = warm_start_info logging.debug(f"calling endpoint {endpoint}") r = requests.post(endpoint, json=json_dict, headers=headers) logging.debug(f"finished calling endpoint {endpoint}") except requests.exceptions.ConnectionError: raise UnknownCloudException(CONNECTION_ERROR_MSG) # check for errors check_status_code(response=r) # parse output output = r.json() objective_value = output["obj_value"] fixed_time_schedule = FixedTimeSchedule.from_json( output["fixed_time_schedule"]) # check if safety restrictions are satisfied; raises a SafetyViolation-exception if this is not the case. validate_safety_restrictions(intersection=intersection, fixed_time_schedule=fixed_time_schedule) phase_diagram = PhaseDiagram.from_json(output["phase_diagram"]) warm_start_info = output.get("warm_start_info", dict()) return fixed_time_schedule, phase_diagram, objective_value, warm_start_info
def get_tuned_fts(cls, intersection: Intersection, arrival_rates: ArrivalRates, fixed_time_schedule: FixedTimeSchedule, horizon: float = 2.0, min_period_duration: float = 0.0, max_period_duration: float = 180, objective: ObjectiveEnum = ObjectiveEnum.min_delay, initial_queue_lengths: Optional[QueueLengths] = None) \ -> Tuple[FixedTimeSchedule, float]: """ Tune a fixed-time schedule; tune the greenyellow times to a new situation but keep the 'structure' of this fixed-time schedule the same (the phase diagram remains the same). :param intersection: intersection for which to optimize the fts (contains signal groups, conflicts and more) :param arrival_rates: arrival rates; each arrival rate is specified in personal car equivalent per hour (PCE/h) cyclists per hour or pedestrians per hour :param fixed_time_schedule: fixed-time schedule to tune. :param horizon: time period of interest in hours. :param min_period_duration: minimum period duration of the fixed-time schedule in seconds :param max_period_duration: minimum period duration of the fixed-time schedule in seconds :param objective: what kpi (key performance indicator) to optimize. The following options are available: - ObjectiveEnum.min_delay: minimize the delay experienced by road users arriving at the intersection during the next 'horizon' hours. The initially waiting traffic is modeled as implicitly by increasing the arrival rate by initial_queue_length / horizon PCE/h; this implies that we assume that this traffic is arriving (evenly spread) during the horizon. - ObjectiveEnum.min_period: search for the fixed-time schedule with the smallest period duration for which all traffic lights are 'stable', i.e., the greenyellow interval is large enough so that the amount of traffic that can (on average) depart during the horizon exceeds the traffic that arrives during the horizon (+ initially waiting traffic). - ObjectiveEnum.max_capacity: search for the fixed-time schedule that can handle the largest (percentual) increase in traffic (including the initial amount of traffic), i.e., the largest percentual increase in traffic for which all traffic lights are 'stable' (see also ObjectiveEnum.min_period). :param initial_queue_lengths: initial amount of traffic waiting at each of the traffic lights; if None, then we assume no initial traffic. The unit of each queue-length should align with the unit used for the arrival rate; if the arrival rate is specified in PCE/h then the queue-length needs to be specified in PCE. :return: fixed-time schedule, associated phase diagram and the objective value (minimized delay, minimized period, or maximum percentual increase in traffic divided by 100, e.g. 1 means currently at the verge of stability) """ assert horizon >= 1, HORIZON_LB_EXCEEDED_MSG if initial_queue_lengths is None: # assume no initial traffic initial_queue_lengths = QueueLengths({ signalgroup.id: [0] * len(signalgroup.traffic_lights) for signalgroup in intersection.signalgroups }) check_all_arrival_rates_and_queue_lengths_specified( intersection=intersection, arrival_rates=arrival_rates, initial_queue_lengths=initial_queue_lengths) endpoint = f"{CLOUD_API_URL}/fts-tuning" headers = SwiftMobilityCloudApi.get_authentication_header() # rest-api call try: # assume that the traffic that is initially present arrives during the horizon. corrected_arrival_rates = arrival_rates + initial_queue_lengths / horizon json_dict = dict(intersection=intersection.to_json(), fixed_time_schedule=fixed_time_schedule.to_json(), arrival_rates=corrected_arrival_rates.to_json(), min_period_duration=min_period_duration, max_period_duration=max_period_duration, objective=objective.value) logging.debug(f"calling endpoint {endpoint}") r = requests.post(endpoint, json=json_dict, headers=headers) logging.debug(f"finished calling endpoint {endpoint}") except requests.exceptions.ConnectionError: raise UnknownCloudException(CONNECTION_ERROR_MSG) # check for errors check_status_code(response=r) # parse output output = r.json() objective_value = output["obj_value"] fixed_time_schedule = FixedTimeSchedule.from_json( output["fixed_time_schedule"]) # check if safety restrictions are satisfied; raises a SafetyViolation-exception if this is not the case. validate_safety_restrictions(intersection=intersection, fixed_time_schedule=fixed_time_schedule) return fixed_time_schedule, objective_value