From dd0b585c03f069e9913168a5fd4d4b74d0d3ea22 Mon Sep 17 00:00:00 2001 From: David Gasinski Date: Thu, 31 Jul 2025 11:58:03 +0100 Subject: [PATCH] completed refactor --- LICENSE => LICENSE Carla | 0 LICENSE Sheffield | 21 +++ aw_scenario_runner.py | 201 +++++++++------------------- config.yaml | 13 +- srunner/objects/ego_vehicle.py | 64 +++++++++ srunner/tools/environment_parser.py | 40 +----- srunner/tools/results_manager.py | 42 ++++-- 7 files changed, 192 insertions(+), 189 deletions(-) rename LICENSE => LICENSE Carla (100%) create mode 100644 LICENSE Sheffield create mode 100644 srunner/objects/ego_vehicle.py diff --git a/LICENSE b/LICENSE Carla similarity index 100% rename from LICENSE rename to LICENSE Carla diff --git a/LICENSE Sheffield b/LICENSE Sheffield new file mode 100644 index 0000000..46cf28c --- /dev/null +++ b/LICENSE Sheffield @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 The University of Sheffield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/aw_scenario_runner.py b/aw_scenario_runner.py index ca8e041..295f00b 100644 --- a/aw_scenario_runner.py +++ b/aw_scenario_runner.py @@ -12,8 +12,7 @@ import carla from srunner.scenariomanager.scenario_manager import ScenarioManager -from srunner.scenario_decoder.json_to_xml_files import XMLToFiles -from srunner.tools.results_manager import ResultsManager +from srunner.tools.results_manager import ScenarioDefinitionManager from srunner.scenarios.route_scenario import RouteScenario from srunner.scenariomanager.carla_data_provider import CarlaDataProvider from srunner.tools.route_parser import RouteParser @@ -23,22 +22,26 @@ RouteScenarioConfiguration, ) +from srunner.objects.ego_vehicle import EgoVehicle + from srunner.tools.log import LogUtil +logger = logging.getLogger("scenario-runner") + class AWScenarioRunner(object): + # flags + DEV_MODE = False + DEBUG = False + + # global class instances ego_vehicles = [] - # world and scenario handlers carla_world = None carla_client = None scenario_manager = None - scenario_decoder = None - results_manager = None - - wait_for_update = False - finished = False + definition_manager = None aw_agent = None @@ -46,6 +49,7 @@ def __init__(self, config: dict) -> None: """ Setup Scenario Manager and the Carla client """ + self._carla_config = config["carla"] self._tm_config = config["traffic_manager"] self._scenario_config = config["scenario_runner"] @@ -53,25 +57,25 @@ def __init__(self, config: dict) -> None: self.carla_client = carla.Client( self._carla_config["host"], int(self._carla_config["port"]) ) + self.carla_client.set_timeout(self._carla_config["timeout"]) - # update the client + # Flags + self.DEV_MODE = self._scenario_config["dev_mode"] + self.DEBUG = self._scenario_config["dev_mode"] + CarlaDataProvider.set_client(self.carla_client) - # load autoware agent - # only load if in docker environment - # debug - if self._scenario_config["in_docker"]: - autoware_agent_path = "srunner/autoagents/autoware_agent" + if not self.DEV_MODE: # only load agents and algorithms in non-dev mode + autoware_agent_path = self._scenario_config["agent"] module_name = os.path.basename(autoware_agent_path).split(".")[0] sys.path.insert(0, os.path.dirname(autoware_agent_path)) self.module_aw_agent = importlib.import_module(module_name) - # load the algorithm of choice - # algorithm = self._scenario_config["algorithm"] # relative path to entry point - # alg_module = os.path.basename(algorithm).split(".")[0] - # sys.path.insert(0, os.path.dirname(algorithm)) - # self.module_algorithm = importlib.import_module(alg_module) + algorithm = self._scenario_config["algorithm"]["path"] + alg_module = os.path.basename(algorithm).split(".")[0] + sys.path.insert(0, os.path.dirname(algorithm)) + self.module_algorithm = importlib.import_module(alg_module) # main class to execute scenarios self.scenario_manager = ScenarioManager( @@ -80,9 +84,9 @@ def __init__(self, config: dict) -> None: self._carla_config["timeout"], ) - self.results_manager = ResultsManager() + self.results_manager = ScenarioDefinitionManager() - # Create signal handler for SIGINT + # capture SIGINT for cleanp self._shutdown_requested = False if sys.platform != "win32": signal.signal(signal.SIGHUP, self._signal_handler) @@ -91,33 +95,14 @@ def __init__(self, config: dict) -> None: self._start_wall_time = datetime.datetime.now() - # parse the JSON scenario file - if self.scenario_decoder is None: - self.scenario_decoder = XMLToFiles() - scenario_name = os.path.split( os.path.splitext(self._scenario_config["json"])[0] )[1] - self._parse_json(self._scenario_config["json"], scenario_name, "0") - - def _parse_json(self, json: str, scenario: str, iteration: str) -> None: - """Parses a given JSON Scenario definition. Outputs two XML files used by scenario runner - - Args: - json (str): filepath to JSON scenario definition - scenario (str): Name of the scenario - iteration (str): ID of the scenario. Can be anything, but must be unique - """ - # create run directory if doesn't exist - if not self.results_manager.results_path: - self.results_manager.create_run_folder() - self.results_manager.create_scenario_folder( - scenario, iteration, self.results_manager.results_path + self.results_manager.parse_json( + self._scenario_config["json"], scenario_name, "0" ) - self.scenario_decoder.parse_scenario(json, self.results_manager.last_scenario) - def _signal_handler(self, signum, frame) -> None: """ Handle shutdown signal, do cleanup @@ -132,83 +117,58 @@ def _signal_handler(self, signum, frame) -> None: def run_scenario( self, route_config: RouteScenarioConfiguration, env_config: EnvironmentConfig ) -> bool: - # find the ego vehicle by name - # only supports one ego - - # setup world based on env config - # make sure to reload self.carla_world = self.carla_client.get_world() self.carla_client.load_world(env_config.town) - # update carla provider CarlaDataProvider.set_world(self.carla_world) - # replace with spawning ego - # ego_missing = True - # while ego_missing: - # self.ego_vehicles = [] - # for ego in route_config.ego_vehicles: - # carla_vehicles = ( - # self.carla_client.get_world().get_actors().filter("vehicle.*") - # ) - # - # for carla_vehicle in carla_vehicles: - # if carla_vehicle.attributes["role_name"] == ego: - # self.ego_vehicles.append(carla_vehicle) - # ego_missing = False - # break - # print("Can't find ego, waiting...") - # time.sleep(1) - # ego_missing = False - - print("Spawning ego...") - self._spawn_ego(env_config) - - print("Spawned ego...") + logger.info("Spawning ego...") + ego = EgoVehicle(env_config) + self.ego_vehicles.append(ego.spawn()) + logger.info("Spawned ego...") self.carla_world.wait_for_tick() - if self._scenario_config["in_docker"]: - print("Loading Autoware agent") + logger.info("Setting up sensort configuration...") + ego.setup_sensors() + + if not self.DEV_MODE: + logger.info("Loading Autoware agent") agent_class_name = self.module_aw_agent.__name__.title().replace("_", "") try: - print(getattr(self.module_aw_agent, agent_class_name)) - # call the agent method to notify the bridge of the ego spawn - # this will loop until the bridge is ready + logger.info(getattr(self.module_aw_agent, agent_class_name)) self.aw_agent = getattr(self.module_aw_agent, agent_class_name)( env_config ) route_config.agent = self.aw_agent except Exception as e: # Forces the simulation to run synchronously # pylint: disable=broad-except - traceback.print_exc() - print("Could not setup required agent due to {}".format(e)) + logger.error("Could not setup required agent due to {}".format(e)) # self._cleanup() return False - # only set synchronous mode once bridge is ready + ego.prepare_ego() + + logger.info("Updating world settings:") + # tick asynchronously until then settings = CarlaDataProvider.get_world().get_settings() settings.synchronous_mode = True settings.fixed_delta_seconds = self._carla_config["fixed_delta_seconds"] CarlaDataProvider.get_world().apply_settings(settings) - # ADD TRAFFIC MANAGER SEED TO CONFIG - tm_port = int(self._tm_config["port"]) # type: ignore - CarlaDataProvider.set_traffic_manager_port(tm_port) - tm = self.carla_client.get_trafficmanager(tm_port) - tm.set_random_device_seed(1) # ADD TO CONFIG + logger.info(f"{settings.__str__()}") - tm.set_synchronous_mode(self._tm_config["sync"]) + if self._tm_config["active"]: + logger.info("Loading Traffic Manager...") + tm_port = int(self._tm_config["port"]) # type: ignore + CarlaDataProvider.set_traffic_manager_port(tm_port) + tm = self.carla_client.get_trafficmanager(tm_port) - print("Preparing ego...") + tm.set_random_device_seed(int(self._tm_config["seed"])) # ADD TO CONFIG + tm.set_synchronous_mode(self._tm_config["sync"]) - # update ego position to one specified in route - self.ego_vehicles[0].set_transform(env_config.ego_spawn) - self.ego_vehicles[0].set_target_velocity(carla.Vector3D()) - self.ego_vehicles[0].set_target_angular_velocity(carla.Vector3D()) - CarlaDataProvider.register_actor(self.ego_vehicles[0], env_config.ego_spawn) + logger.info("Loading route...") - print("Loading route...") try: scenario = RouteScenario( world=self.carla_world, @@ -216,64 +176,28 @@ def run_scenario( debug_mode=self._carla_config["debug"], ) except Exception: - print("Could not load Route Scenario") + logger.info("Could not load Route Scenario") traceback.print_exc() return False - print("Starting scenario...") + logger.info("Starting scenario...") try: self.scenario_manager.load_scenario(scenario, self.aw_agent) self.scenario_manager.run_scenario() result = True except Exception: traceback.print_exc() - print("It doesn't work") + logger.info("It doesn't work") result = False return result - def _load_scenario_config(self) -> EnvironmentConfig: - return EnvironmentParser.parse_scenario_env( + def run(self) -> bool: + env_config = EnvironmentParser.parse_scenario_env( os.path.join(self.results_manager.last_scenario, "scenario.xml") ) - - def _spawn_ego(self, env_config: EnvironmentConfig) -> None: - ego = CarlaDataProvider.request_new_actor( - model=env_config.ego_model, - spawn_point=env_config.ego_spawn, - rolename=env_config.ego_name, - ) - self.ego_vehicles.append(ego) - - CarlaDataProvider.get_world().wait_for_tick() # wait for tick - - bp_library = self.carla_world.get_blueprint_library() - # setup sensors - for sensor in env_config.sensor_config: - sensor._spawn(bp_library, ego) - print(f"Spawned sensor type {sensor.type}") - - def _load_route_scenario( - self, env_config: EnvironmentConfig - ) -> RouteScenarioConfiguration: route_config = RouteParser.parse_routes_file( self.results_manager.last_scenario, env_config - ) # type: ignore - - return route_config[0] - - def run(self) -> bool: - # load the original JSON file - # load the route config - # load the scenario config - # run scenario - # get the metrics - # call the algorithm callback - # save using ResultsManager static class - - # repeat for iterations - - env_config = self._load_scenario_config() - route_config = self._load_route_scenario(env_config) + )[self._scenario_config["route_id"]] scenario_result = self.run_scenario(route_config, env_config) return scenario_result @@ -296,7 +220,7 @@ def _cleanup(self) -> None: try: # Reset to asynchronous mode self.carla_client.get_trafficmanager( - int(self._args.traffic_port) + int(self._tm_config["port"]) ).set_synchronous_mode(False) except RuntimeError: sys.exit(-1) @@ -308,7 +232,9 @@ def _cleanup(self) -> None: for i, _ in enumerate(self.ego_vehicles): if self.ego_vehicles[i]: if self.ego_vehicles[i] is not None and self.ego_vehicles[i].is_alive: - print("Destroying ego vehicle {}".format(self.ego_vehicles[i].id)) + logger.info( + "Destroying ego vehicle {}".format(self.ego_vehicles[i].id) + ) self.ego_vehicles[i].destroy() self.ego_vehicles[i] = None self.ego_vehicles = [] @@ -319,6 +245,8 @@ def _cleanup(self) -> None: def main(): + # single argument of configuration file + # configure logger config = None with open("config.yaml", "r") as stream: @@ -326,7 +254,6 @@ def main(): log_config = config["log"] - logger = logging.getLogger("scenario-runner") logger.setLevel(logging.INFO) log_path = LogUtil.create_log_file(log_config["path"]) @@ -338,7 +265,7 @@ def main(): try: scenario_runner = AWScenarioRunner(config) results = scenario_runner.run() - print(results) + logger.info(results) except Exception: # NOT GOOD PRACTICE PROBABLY CHANGE traceback.print_exc() finally: diff --git a/config.yaml b/config.yaml index 84bc835..c9a3ac3 100644 --- a/config.yaml +++ b/config.yaml @@ -7,17 +7,18 @@ carla: timeout: 20 sync: true fixed_delta_seconds: 0.05 # update rate 1 / FPS + dev_mode: false debug: false traffic_manager: active: false sync: true # must be the same as carla sync + seed: 0 port: 8000 scenario_runner: json: ./example_scenario.json - debug: false route_id: 0 - in_docker: true # used for dev -algorithm: - iterations: 1000 - path: /srunner/ # relative path - hyperparmaters: # will be passed into the algorithm class + agent: srunner/autoagents/autoware_agent + algorithm: + iterations: 1000 + path: /srunner/ # relative path + hyperparmaters: # will be passed into the algorithm class diff --git a/srunner/objects/ego_vehicle.py b/srunner/objects/ego_vehicle.py new file mode 100644 index 0000000..a2fdece --- /dev/null +++ b/srunner/objects/ego_vehicle.py @@ -0,0 +1,64 @@ +from srunner.scenariomanager.carla_data_provider import CarlaDataProvider +from srunner.tools.environment_parser import EnvironmentConfig + +import carla +import logging + +logger = logging.getLogger("scenario-runner") + + +class EgoVehicle(object): + """ + A basic class to encompass the definition of an ego_vehicle controlled by ADS + """ + + ego_name = "" + ego_model = "" + world = None + + def __init__(self, env_config: EnvironmentConfig) -> None: + self._env = env_config + self.ego_model = self._env.ego_model + self.ego_name = self._env.ego_name + self.ego_spawn = self._env.ego_spawn + self.sensor_config = self._env.sensor_config + + self._actor = None + + def spawn(self) -> carla.Actor: + """Spawns an actor to act as ego_vehicle + + Returns: + carla.Actor: EgoVehicle class + """ + + self._actor = CarlaDataProvider.request_new_actor( + self.ego_model, self.ego_spawn, self.ego_name + ) + + if self._actor is None: + logger.warning( + "Failed to spawn EgoVehicle. This is likely an issue with CARLA." + ) + + return self._actor + + def setup_sensors(self) -> None: + """Spawns and attatches the necessary sensors to the EgoVehcicle.""" + if self._actor is None: + logger.error( + "EgoVehicle has no carla.Actor, it probably failed to spawn. Check the spawn coordinates, and ensure you call EgoVehicle.spawn() first." + ) + + bp_library = CarlaDataProvider.get_world().get_blueprint_library() + + for sensor in self._env.sensor_config: + sensor._spawn(bp_library, self._actor) + logger.info(f"Spawned {sensor.type} attatched to {self.ego_name}") + + def prepare_ego(self) -> None: + """Reset the position and velocity of the ego. Register the actor""" + self._actor.set_transform(self._env.ego_spawn) + self._actor.set_target_velocity(carla.Vector3D()) + self._actor.set_target_angular_velocity(carla.Vector3D()) + CarlaDataProvider.register_actor(self._actor, self._env.ego_spawn) diff --git a/srunner/tools/environment_parser.py b/srunner/tools/environment_parser.py index 1970a8f..e425128 100644 --- a/srunner/tools/environment_parser.py +++ b/srunner/tools/environment_parser.py @@ -34,41 +34,13 @@ def __init__(self) -> None: def _spawn(self, bp_library, vehicle) -> None: sensor_bp = bp_library.find(str(self.type)) - ignored_params = [ - "__class__", - "__delattr__", - "__dict__", - "__dir__", - "__doc__", - "__eq__", - "__format__", - "__ge__", - "__getattribute__", - "__gt__", - "__hash__", - "__init__", - "__init_subclass__", - "__le__", - "__lt__", - "__module__", - "__ne__", - "__new__", - "__reduce__", - "__reduce_ex__", - "__repr__", - "__setattr__", - "__sizeof__", - "__str__", - "__subclasshook__", - "__weakref__", - "_spawn", - "serealize", - "id", - "spawn", - "type", - ] + ignored_params = ["serealize", "id", "spawn", "type"] - sensor_params = [attr for attr in dir(self) if attr not in ignored_params] + sensor_params = [ + attr + for attr in dir(self) + if attr not in ignored_params or not attr.startswith("_") + ] for param in sensor_params: sensor_bp.set_attribute(param, str(getattr(self, param))) diff --git a/srunner/tools/results_manager.py b/srunner/tools/results_manager.py index c8a2a4f..f7f6286 100644 --- a/srunner/tools/results_manager.py +++ b/srunner/tools/results_manager.py @@ -1,19 +1,22 @@ import os import datetime +from srunner.scenario_decoder.json_to_xml_files import XMLToFiles -class ResultsManager(object): + +class ScenarioDefinitionManager(object): """Class to manage the file structure of the results. Creates folders for entire experiments (runs) and individual scenario executions (scenarios) Holds the file path of the last scenario executes, and the results. - + """ - - def __init__(self, output_dir: str = 'results') -> None: + + def __init__(self, output_dir: str = "results") -> None: self.output_dir = output_dir - self.last_scenario = '' - self.results_path = '' - - def create_run_folder(self, base_path: str = ''): + self.last_scenario = "" + self.results_path = "" + self._scenario_decoder = XMLToFiles() + + def _create_run_folder(self, base_path: str = ""): """Creates a experiment folder under base_path. The naming convention is as follows ``` run-{yy-mm-dd-h-m-s} @@ -27,7 +30,7 @@ def create_run_folder(self, base_path: str = ''): """ if not base_path: base_path = self.output_dir - + now = datetime.datetime.now() full_path = os.path.join(base_path, f"run-{now.strftime('%Y-%m-%d-%H-%M-%S')}") # type: ignore @@ -35,7 +38,7 @@ def create_run_folder(self, base_path: str = ''): self.results_path = full_path return full_path - def create_scenario_folder( + def _create_scenario_folder( self, scenario: str, iteration: str, results_folder: str ): """Creates a folder to hold the results of a individual scenario execution. @@ -52,9 +55,24 @@ def create_scenario_folder( Returns: _type_: _description_ """ - + full_path = os.path.join(results_folder, f"{scenario}-{iteration}") - os.makedirs(full_path, exist_ok=True) # exist_ok=True, no need to error handle + os.makedirs(full_path, exist_ok=True) # exist_ok=True, no need to error handle self.last_scenario = full_path return full_path + + def parse_json(self, json: str, scenario: str, iteration: str): + """Parses a given JSON Scenario definition. Outputs two XML files used by scenario runner + + Args: + json (str): filepath to JSON scenario definition + scenario (str): Name of the scenario + iteration (str): ID of the scenario. Can be anything, but must be unique + """ + + if not self.results_path: + self._create_run_folder() + + self._create_scenario_folder(scenario, iteration, self.results_path) + self._scenario_decoder.parse_scenario(json, self.last_scenario)