#!/usr/bin/env python # Copyright (c) 2018-2020 Intel Corporation # # This work is licensed under the terms of the MIT license. # For a copy, see . """ Welcome to CARLA scenario_runner This is the main script to be executed when running a scenario. It loads the scenario configuration, loads the scenario and manager, and finally triggers the scenario execution. """ from __future__ import print_function import glob import traceback import argparse from argparse import RawTextHelpFormatter from datetime import datetime from distutils.version import LooseVersion import importlib import inspect import os import signal import sys import time import json import pkg_resources import carla from srunner.scenarioconfigs.openscenario_configuration import OpenScenarioConfiguration from srunner.scenariomanager.carla_data_provider import CarlaDataProvider from srunner.scenariomanager.scenario_manager import ScenarioManager from srunner.scenarios.open_scenario import OpenScenario from srunner.scenarios.route_scenario import RouteScenario from srunner.tools.scenario_parser import ScenarioConfigurationParser from srunner.tools.route_parser import RouteParser # Version of scenario_runner VERSION = '0.9.13' class ScenarioRunner(object): """ This is the core scenario runner module. It is responsible for running (and repeating) a single scenario or a list of scenarios. Usage: scenario_runner = ScenarioRunner(args) scenario_runner.run() del scenario_runner """ ego_vehicles = [] # Tunable parameters client_timeout = 10.0 # in seconds wait_for_world = 20.0 # in seconds frame_rate = 20.0 # in Hz # CARLA world and scenario handlers world = None manager = None finished = False additional_scenario_module = None agent_instance = None module_agent = None def __init__(self, args): """ Setup CARLA client and world Setup ScenarioManager """ self._args = args if args.timeout: self.client_timeout = float(args.timeout) # First of all, we need to create the client that will send the requests # to the simulator. Here we'll assume the simulator is accepting # requests in the localhost at port 2000. self.client = carla.Client(args.host, int(args.port)) self.client.set_timeout(self.client_timeout) dist = pkg_resources.get_distribution("carla") if LooseVersion(dist.version) < LooseVersion('0.9.12'): raise ImportError("CARLA version 0.9.12 or newer required. CARLA version found: {}".format(dist)) # Load agent if requested via command line args # If something goes wrong an exception will be thrown by importlib (ok here) if self._args.agent is not None: module_name = os.path.basename(args.agent).split('.')[0] sys.path.insert(0, os.path.dirname(args.agent)) self.module_agent = importlib.import_module(module_name) # Create the ScenarioManager self.manager = ScenarioManager(self._args.debug, self._args.sync, self._args.timeout) # Create signal handler for SIGINT self._shutdown_requested = False if sys.platform != 'win32': signal.signal(signal.SIGHUP, self._signal_handler) signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) self._start_wall_time = datetime.now() def destroy(self): """ Cleanup and delete actors, ScenarioManager and CARLA world """ self._cleanup() if self.manager is not None: del self.manager if self.world is not None: del self.world if self.client is not None: del self.client def _signal_handler(self, signum, frame): """ Terminate scenario ticking when receiving a signal interrupt """ self._shutdown_requested = True if self.manager: self.manager.stop_scenario() def _get_scenario_class_or_fail(self, scenario): """ Get scenario class by scenario name If scenario is not supported or not found, exit script """ # Path of all scenario at "srunner/scenarios" folder + the path of the additional scenario argument scenarios_list = glob.glob("{}/srunner/scenarios/*.py".format(os.getenv('SCENARIO_RUNNER_ROOT', "./"))) scenarios_list.append(self._args.additionalScenario) for scenario_file in scenarios_list: # Get their module module_name = os.path.basename(scenario_file).split('.')[0] sys.path.insert(0, os.path.dirname(scenario_file)) scenario_module = importlib.import_module(module_name) # And their members of type class for member in inspect.getmembers(scenario_module, inspect.isclass): if scenario in member: return member[1] # Remove unused Python paths sys.path.pop(0) print("Scenario '{}' not supported ... Exiting".format(scenario)) sys.exit(-1) def _cleanup(self): """ Remove and destroy all actors """ if self.finished: return self.finished = True # Simulation still running and in synchronous mode? if self.world is not None and self._args.sync: try: # Reset to asynchronous mode settings = self.world.get_settings() settings.synchronous_mode = False settings.fixed_delta_seconds = None self.world.apply_settings(settings) self.client.get_trafficmanager(int(self._args.trafficManagerPort)).set_synchronous_mode(False) except RuntimeError: sys.exit(-1) self.manager.cleanup() CarlaDataProvider.cleanup() for i, _ in enumerate(self.ego_vehicles): if self.ego_vehicles[i]: if not self._args.waitForEgo and self.ego_vehicles[i] is not None and self.ego_vehicles[i].is_alive: print("Destroying ego vehicle {}".format(self.ego_vehicles[i].id)) self.ego_vehicles[i].destroy() self.ego_vehicles[i] = None self.ego_vehicles = [] if self.agent_instance: self.agent_instance.destroy() self.agent_instance = None def _prepare_ego_vehicles(self, ego_vehicles): """ Spawn or update the ego vehicles """ if not self._args.waitForEgo: for vehicle in ego_vehicles: self.ego_vehicles.append(CarlaDataProvider.request_new_actor(vehicle.model, vehicle.transform, vehicle.rolename, color=vehicle.color, actor_category=vehicle.category)) else: ego_vehicle_missing = True while ego_vehicle_missing: self.ego_vehicles = [] ego_vehicle_missing = False for ego_vehicle in ego_vehicles: ego_vehicle_found = False carla_vehicles = CarlaDataProvider.get_world().get_actors().filter('vehicle.*') for carla_vehicle in carla_vehicles: if carla_vehicle.attributes['role_name'] == ego_vehicle.rolename: ego_vehicle_found = True self.ego_vehicles.append(carla_vehicle) break if not ego_vehicle_found: ego_vehicle_missing = True break for i, _ in enumerate(self.ego_vehicles): self.ego_vehicles[i].set_transform(ego_vehicles[i].transform) CarlaDataProvider.register_actor(self.ego_vehicles[i]) # sync state if CarlaDataProvider.is_sync_mode(): self.world.tick() else: self.world.wait_for_tick() def _analyze_scenario(self, config): """ Provide feedback about success/failure of a scenario """ # Create the filename current_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) junit_filename = None json_filename = None config_name = config.name if self._args.outputDir != '': config_name = os.path.join(self._args.outputDir, config_name) if self._args.junit: junit_filename = config_name + current_time + ".xml" if self._args.json: json_filename = config_name + current_time + ".json" filename = None if self._args.file: filename = config_name + current_time + ".txt" if not self.manager.analyze_scenario(self._args.output, filename, junit_filename, json_filename): print("All scenario tests were passed successfully!") else: print("Not all scenario tests were successful") if not (self._args.output or filename or junit_filename): print("Please run with --output for further information") def _record_criteria(self, criteria, name): """ Filter the JSON serializable attributes of the criterias and dumps them into a file. This will be used by the metrics manager, in case the user wants specific information about the criterias. """ file_name = name[:-4] + ".json" # Filter the attributes that aren't JSON serializable with open('temp.json', 'w', encoding='utf-8') as fp: criteria_dict = {} for criterion in criteria: criterion_dict = criterion.__dict__ criteria_dict[criterion.name] = {} for key in criterion_dict: if key != "name": try: key_dict = {key: criterion_dict[key]} json.dump(key_dict, fp, sort_keys=False, indent=4) criteria_dict[criterion.name].update(key_dict) except TypeError: pass os.remove('temp.json') # Save the criteria dictionary into a .json file with open(file_name, 'w', encoding='utf-8') as fp: json.dump(criteria_dict, fp, sort_keys=False, indent=4) def _load_and_wait_for_world(self, town, ego_vehicles=None): """ Load a new CARLA world and provide data to CarlaDataProvider """ if self._args.reloadWorld: self.world = self.client.load_world(town) else: # if the world should not be reloaded, wait at least until all ego vehicles are ready ego_vehicle_found = False if self._args.waitForEgo: while not ego_vehicle_found and not self._shutdown_requested: vehicles = self.client.get_world().get_actors().filter('vehicle.*') for ego_vehicle in ego_vehicles: ego_vehicle_found = False for vehicle in vehicles: if vehicle.attributes['role_name'] == ego_vehicle.rolename: ego_vehicle_found = True break if not ego_vehicle_found: print("Not all ego vehicles ready. Waiting ... ") time.sleep(1) break self.world = self.client.get_world() if self._args.sync: settings = self.world.get_settings() settings.synchronous_mode = True settings.fixed_delta_seconds = 1.0 / self.frame_rate self.world.apply_settings(settings) CarlaDataProvider.set_client(self.client) CarlaDataProvider.set_world(self.world) # Wait for the world to be ready if CarlaDataProvider.is_sync_mode(): self.world.tick() else: self.world.wait_for_tick() map_name = CarlaDataProvider.get_map().name.split('/')[-1] if map_name not in (town, "OpenDriveMap"): print("The CARLA server uses the wrong map: {}".format(map_name)) print("This scenario requires to use map: {}".format(town)) return False return True def _load_and_run_scenario(self, config): """ Load and run the scenario given by config """ result = False if not self._load_and_wait_for_world(config.town, config.ego_vehicles): self._cleanup() return False if self._args.agent: agent_class_name = self.module_agent.__name__.title().replace('_', '') try: self.agent_instance = getattr(self.module_agent, agent_class_name)(self._args.agentConfig) config.agent = self.agent_instance except Exception as e: # pylint: disable=broad-except traceback.print_exc() print("Could not setup required agent due to {}".format(e)) self._cleanup() return False CarlaDataProvider.set_traffic_manager_port(int(self._args.trafficManagerPort)) tm = self.client.get_trafficmanager(int(self._args.trafficManagerPort)) tm.set_random_device_seed(int(self._args.trafficManagerSeed)) if self._args.sync: tm.set_synchronous_mode(True) # Prepare scenario print("Preparing scenario: " + config.name) try: self._prepare_ego_vehicles(config.ego_vehicles) if self._args.openscenario: scenario = OpenScenario(world=self.world, ego_vehicles=self.ego_vehicles, config=config, config_file=self._args.openscenario, timeout=100000) elif self._args.route: scenario = RouteScenario(world=self.world, config=config, debug_mode=self._args.debug) else: scenario_class = self._get_scenario_class_or_fail(config.type) scenario = scenario_class(self.world, self.ego_vehicles, config, self._args.randomize, self._args.debug) except Exception as exception: # pylint: disable=broad-except print("The scenario cannot be loaded") traceback.print_exc() print(exception) self._cleanup() return False try: if self._args.record: recorder_name = "{}/{}/{}.log".format( os.getenv('SCENARIO_RUNNER_ROOT', "./"), self._args.record, config.name) self.client.start_recorder(recorder_name, True) # Load scenario and run it self.manager.load_scenario(scenario, self.agent_instance) self.manager.run_scenario() # Provide outputs if required self._analyze_scenario(config) # Remove all actors, stop the recorder and save all criterias (if needed) # scenario.remove_all_actors() if self._args.record: self.client.stop_recorder() self._record_criteria(self.manager.scenario.get_criteria(), recorder_name) result = True except Exception as e: # pylint: disable=broad-except traceback.print_exc() print(e) result = False self._cleanup() return result def _run_scenarios(self): """ Run conventional scenarios (e.g. implemented using the Python API of ScenarioRunner) """ result = False # Load the scenario configurations provided in the config file scenario_configurations = ScenarioConfigurationParser.parse_scenario_configuration( self._args.scenario, self._args.configFile) if not scenario_configurations: print("Configuration for scenario {} cannot be found!".format(self._args.scenario)) return result # Execute each configuration for config in scenario_configurations: for _ in range(self._args.repetitions): self.finished = False result = self._load_and_run_scenario(config) self._cleanup() return result def _run_route(self): """ Run the route scenario """ result = False if self._args.route: routes = self._args.route[0] scenario_file = self._args.route[1] single_route = None if len(self._args.route) > 2: single_route = self._args.route[2] # retrieve routes route_configurations = RouteParser.parse_routes_file(routes, scenario_file, single_route) for config in route_configurations: for _ in range(self._args.repetitions): result = self._load_and_run_scenario(config) self._cleanup() return result def _run_openscenario(self): """ Run a scenario based on OpenSCENARIO """ # Load the scenario configurations provided in the config file if not os.path.isfile(self._args.openscenario): print("File does not exist") self._cleanup() return False openscenario_params = {} if self._args.openscenarioparams is not None: for entry in self._args.openscenarioparams.split(','): [key, val] = [m.strip() for m in entry.split(':')] openscenario_params[key] = val config = OpenScenarioConfiguration(self._args.openscenario, self.client, openscenario_params) result = self._load_and_run_scenario(config) self._cleanup() return result def run(self): """ Run all scenarios according to provided commandline args """ result = True if self._args.openscenario: result = self._run_openscenario() elif self._args.route: result = self._run_route() else: result = self._run_scenarios() print("No more scenarios .... Exiting") return result def main(): """ main function """ description = ("CARLA Scenario Runner: Setup, Run and Evaluate scenarios using CARLA\n" "Current version: " + VERSION) # pylint: disable=line-too-long parser = argparse.ArgumentParser(description=description, formatter_class=RawTextHelpFormatter) parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + VERSION) parser.add_argument('--host', default='127.0.0.1', help='IP of the host server (default: localhost)') parser.add_argument('--port', default='3000', help='TCP port to listen to (default: 3000)') parser.add_argument('--timeout', default="10.0", help='Set the CARLA client timeout value in seconds') parser.add_argument('--trafficManagerPort', default='8000', help='Port to use for the TrafficManager (default: 8000)') parser.add_argument('--trafficManagerSeed', default='0', help='Seed used by the TrafficManager (default: 0)') parser.add_argument('--sync', action='store_true', help='Forces the simulation to run synchronously') parser.add_argument('--list', action="store_true", help='List all supported scenarios and exit') parser.add_argument( '--scenario', help='Name of the scenario to be executed. Use the preposition \'group:\' to run all scenarios of one class, e.g. ControlLoss or FollowLeadingVehicle') parser.add_argument('--openscenario', help='Provide an OpenSCENARIO definition') parser.add_argument('--openscenarioparams', help='Overwrited for OpenSCENARIO ParameterDeclaration') parser.add_argument( '--route', help='Run a route as a scenario (input: (route_file,scenario_file,[route id]))', nargs='+', type=str) parser.add_argument( '--agent', help="Agent used to execute the scenario. Currently only compatible with route-based scenarios.") parser.add_argument('--agentConfig', type=str, help="Path to Agent's configuration file", default="") parser.add_argument('--output', action="store_true", help='Provide results on stdout') parser.add_argument('--file', action="store_true", help='Write results into a txt file') parser.add_argument('--junit', action="store_true", help='Write results into a junit file') parser.add_argument('--json', action="store_true", help='Write results into a JSON file') parser.add_argument('--outputDir', default='', help='Directory for output files (default: this directory)') parser.add_argument('--configFile', default='', help='Provide an additional scenario configuration file (*.xml)') parser.add_argument('--additionalScenario', default='', help='Provide additional scenario implementations (*.py)') parser.add_argument('--debug', action="store_true", help='Run with debug output') parser.add_argument('--reloadWorld', action="store_true", help='Reload the CARLA world before starting a scenario (default=True)') parser.add_argument('--record', type=str, default='', help='Path were the files will be saved, relative to SCENARIO_RUNNER_ROOT.\nActivates the CARLA recording feature and saves to file all the criteria information.') parser.add_argument('--randomize', action="store_true", help='Scenario parameters are randomized') parser.add_argument('--repetitions', default=1, type=int, help='Number of scenario executions') parser.add_argument('--waitForEgo', action="store_true", help='Connect the scenario to an existing ego vehicle') arguments = parser.parse_args() # pylint: enable=line-too-long if arguments.list: print("Currently the following scenarios are supported:") print(*ScenarioConfigurationParser.get_list_of_scenarios(arguments.configFile), sep='\n') return 1 if not arguments.scenario and not arguments.openscenario and not arguments.route: print("Please specify either a scenario or use the route mode\n\n") parser.print_help(sys.stdout) return 1 if arguments.route and (arguments.openscenario or arguments.scenario): print("The route mode cannot be used together with a scenario (incl. OpenSCENARIO)'\n\n") parser.print_help(sys.stdout) return 1 if arguments.agent and (arguments.openscenario or arguments.scenario): print("Agents are currently only compatible with route scenarios'\n\n") parser.print_help(sys.stdout) return 1 if arguments.openscenarioparams and not arguments.openscenario: print("WARN: Ignoring --openscenarioparams when --openscenario is not specified") if arguments.route: arguments.reloadWorld = True if arguments.agent: arguments.sync = True scenario_runner = None result = True try: scenario_runner = ScenarioRunner(arguments) result = scenario_runner.run() except Exception: # pylint: disable=broad-except traceback.print_exc() finally: if scenario_runner is not None: scenario_runner.destroy() del scenario_runner return not result if __name__ == "__main__": sys.exit(main())