-
Aditya Prakash authoredAditya Prakash authored
scenario_runner.py 22.40 KiB
#!/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 <https://opensource.org/licenses/MIT>.
"""
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.9'
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
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.8'):
raise ImportError("CARLA version 0.9.8 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()
self._cleanup()
if not self.manager.get_running_status():
raise RuntimeError("Timeout occured during scenario execution")
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
"""
# Simulation still running and in synchronous mode?
if self.manager is not None and self.manager.get_running_status() \
and self.world is not None and self._args.sync:
# Reset to asynchronous mode
settings = self.world.get_settings()
settings.synchronous_mode = False
settings.fixed_delta_seconds = None
self.world.apply_settings(settings)
self.manager.cleanup()
CarlaDataProvider.cleanup()
for i, _ in enumerate(self.ego_vehicles):
if self.ego_vehicles[i]:
if not self._args.waitForEgo:
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
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"
filename = None
if self._args.file:
filename = config_name + current_time + ".txt"
if not self.manager.analyze_scenario(self._args.output, filename, junit_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') 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') 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)
CarlaDataProvider.set_traffic_manager_port(int(self._args.trafficManagerPort))
# Wait for the world to be ready
if CarlaDataProvider.is_sync_mode():
self.world.tick()
else:
self.world.wait_for_tick()
if CarlaDataProvider.get_map().name != town and CarlaDataProvider.get_map().name != "OpenDriveMap":
print("The CARLA server uses the wrong map: {}".format(CarlaDataProvider.get_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
# 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):
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
config = OpenScenarioConfiguration(self._args.openscenario, self.client)
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='2000',
help='TCP port to listen to (default: 2000)')
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('--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(
'--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('--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.route:
arguments.reloadWorld = True
if arguments.agent:
arguments.sync = True
scenario_runner = None
result = True
try:
scenario_runner = ScenarioRunner(arguments)
result = scenario_runner.run()
finally:
if scenario_runner is not None:
scenario_runner.destroy()
del scenario_runner
return not result
if __name__ == "__main__":
sys.exit(main())