diff --git a/examples/scenarios/Mcity_corner_case_generation.yaml b/examples/scenarios/Mcity_corner_case_generation.yaml new file mode 100755 index 0000000..228956c --- /dev/null +++ b/examples/scenarios/Mcity_corner_case_generation.yaml @@ -0,0 +1,192 @@ +simulation_module: "my_simulations" +simulation_class: "CarSimulation" +parameters: + num_cars: 10 + simulation_time: 500 + +# Simulation type: Choose between "safetest_mcity_av" or "safetest_mcity" +simulation_type: "safetest_mcity_av" + + +environment: + # Module containing the environment class + module: "terasim_nde_nade.envs" # Use "terasim_nde_nade.envs.safetest_nade" for non-AV simulations + # Environment class name + class: "SafeTestNADE" # Use "SafeTestNADE" for non-AV simulations + parameters: + # Vehicle factory class + vehicle_factory: "terasim_nde_nade.vehicle.nde_vehicle_factory.NDEVehicleFactory" + # Info extractor class + info_extractor: "terasim.logger.infoextractor.InfoExtractor" + log_flag: true + warmup_time_lb: 100 # Lower bound for warmup time + warmup_time_ub: 500 # Upper bound for warmup time + run_time: 500 # Simulation run time (use 30 for non-AV simulations) + + MOBIL_lc_flag: True + stochastic_acc_flag: False + drive_rule: "righthand" + + adversity_sampling_probability: 0.0 + adversity_cfg: + vehicle: + roundabout_cutin: + _target_: terasim_nde_nade.adversity.vehicles.lanechange_adversity.LanechangeAdversity + _convert_: 'all' + location: 'roundabout' + ego_type: 'vehicle' + probability: 2.3123152310029683e-05 + predicted_collision_type: "roundabout_cutin" + + # roundabout_rearend: + # _target_: terasim_nde_nade.adversity.vehicles.leader_adversity.LeaderAdversity + # _convert_: 'all' + # location: 'roundabout' + # ego_type: 'vehicle' + # probability: 1.8383121748678325e-10 + # predicted_collision_type: "roundabout_rearend" + + roundabout_fail_to_yield: + _target_: terasim_nde_nade.adversity.vehicles.trafficrule_adversity.TrafficRuleAdversity + _convert_: 'all' + location: 'roundabout' + ego_type: 'vehicle' + probability: 0.0005025411072000003 + predicted_collision_type: "roundabout_fail_to_yield" + + # roundabout_neglect_conflict_lead: + # _target_: terasim_nde_nade.adversity.roundabout_cutin_adversity.RoundaboutCutinAdversity + # _convert_: 'all' + # location: 'roundabout' + # ego_type: 'vehicle' + # probability: 0.00027119028871789013 + + highway_cutin: + _target_: terasim_nde_nade.adversity.vehicles.lanechange_adversity.LanechangeAdversity + _convert_: 'all' + location: 'highway' + ego_type: 'vehicle' + probability: 3.1996225726187704e-05 + predicted_collision_type: "highway_cutin" + + # highway_rearend: + # _target_: terasim_nde_nade.adversity.vehicles.leader_adversity.LeaderAdversity + # _convert_: 'all' + # location: 'highway' + # ego_type: 'vehicle' + # probability: 0 # 0.8783674048511999 + # predicted_collision_type: "highway_rearend" + + intersection_cutin: + _target_: terasim_nde_nade.adversity.vehicles.lanechange_adversity.LanechangeAdversity + _convert_: 'all' + location: 'intersection' + ego_type: 'vehicle' + probability: 6.694592721399203e-05 + predicted_collision_type: "intersection_cutin" + + # intersection_rearend: + # _target_: terasim_nde_nade.adversity.vehicles.leader_adversity.LeaderAdversity + # _convert_: 'all' + # location: 'intersection' + # ego_type: 'vehicle' + # probability: 0.00017365940078885788 + # predicted_collision_type: "intersection_rearend" + + intersection_headon: + _target_: terasim_nde_nade.adversity.vehicles.headon_adversity.HeadonAdversity + _convert_: 'all' + location: 'intersection' + ego_type: 'vehicle' + probability: 1.0663168272664859e-08 + predicted_collision_type: "intersection_headon" + + intersection_tfl: + _target_: terasim_nde_nade.adversity.vehicles.trafficrule_adversity.TrafficRuleAdversity + _convert_: 'all' + location: 'intersection' + ego_type: 'vehicle' + probability: 0.010141902545438387 + predicted_collision_type: "intersection_tfl" + + # intersection_neglect_conflict_lead: + # _target_: terasim_nde_nade.adversity.roundabout_cutin_adversity.RoundaboutCutinAdversity + # _convert_: 'all' + # location: 'roundabout' + # ego_type: 'vehicle' + # probability: 0.015050715723004704 + + vulnerable_road_user: + jaywalking: + _target_: terasim_nde_nade.adversity.vru.jaywalking_adversity.JaywalkingAdversity + _convert_: 'all' + location: 'crosswalk' + ego_type: 'vulnerable_road_user' + # probability: 0.001 + probability: 0.0 + predicted_collision_type: "intersection_jaywalking" + + runningredlight: + _target_: terasim_nde_nade.adversity.vru.runningredlight_adversity.RunningRedLightAdversity + _convert_: 'all' + location: 'crosswalk' + ego_type: 'vulnerable_road_user' + # probability: 0.001 + probability: 0.0 + predicted_collision_type: "intersection_runningredlight" + + stopcrossing: + _target_: terasim_nde_nade.adversity.vru.stopcrossing_adversity.StopCrossingAdversity + _convert_: 'all' + location: 'crosswalk' + ego_type: 'vulnerable_road_user' + # probability: 0.001 + probability: 0.0 + predicted_collision_type: "intersection_stopcrossing" + + AV_cfg: + route: ["EG_35_1_14", "EG_1_3_1", "EG_1_3_1.61", "EG_1_3_1.136", "EG_34_1_24", "EG_34_1_3", "gneE0", "EG_4_1_1", "EG_10_1_1", "EG_15_1_17", "EG_15_1_1", "EG_16_45_1", "EG_16_23_1", "EG_17_1_1", "EG_14_2_1", "EG_9_1_1", "EG_21_1_1", "EG_21_1_5", "EG_20_1_11", "EG_29_1_1", "EG_35_1_14"] # list of SUMO edges (example for Mcity) + type: "NDE_URBAN" + cache_radius: 100 + control_radius: 100 + warmup_control: + enabled: true # Enable AV warmup (false keeps original behavior) + + # Trigger condition (choose one) + trigger_type: "time" # Options: "position", "time", "zone", "edge" + + # Position trigger configuration + time_trigger: + duration: 3.0 # Warmup duration in seconds +simulator: + module: "terasim.simulator" + class: "Simulator" + parameters: + num_tries: 10 + gui_flag: false + realtime_flag: false # Only applicable for AV mode + sumo_output_file_types: + - "fcd_all" + - "collision" + - "tripinfo" + +# Path resolution mode: +# - "config_relative": Relative paths are resolved from the config file's directory +# - "cwd_relative": Relative paths are resolved from the current working directory +path_resolution: "config_relative" + +# New section for file paths +input: + sumo_net_file: "../maps/Mcity/mcity.net.xml" + sumo_config_file: "../maps/Mcity/mcity.sumocfg" + +output: + dir: "outputs" # Output directory (use "output" for non-AV simulations) + name: "Mcity" # Experiment name + nth: "0_0" # Experiment number + aggregated_dir: "aggregated" # Directory for aggregated logs + +logging: + levels: + - "TRACE" # Log level for the main log file + - "INFO" # Log level for the aggregated log file diff --git a/packages/terasim-nde-nade/terasim_nde_nade/envs/nade.py b/packages/terasim-nde-nade/terasim_nde_nade/envs/nade.py index 225f73c..a343a78 100644 --- a/packages/terasim-nde-nade/terasim_nde_nade/envs/nade.py +++ b/packages/terasim-nde-nade/terasim_nde_nade/envs/nade.py @@ -347,6 +347,25 @@ def update_distance(self): distance_info_dict[veh_id] = traci.vehicle.getDistance(veh_id) return distance_info_dict + + def save_conflict_info(self, conflict_veh_info): + sim_time = utils.get_time() + + conflict_info_file = self.simulator.output_path / "conflict_info.jsonl" + + vehicle_conflict_info = conflict_veh_info.get(AgentType.VEHICLE, {}) + if not vehicle_conflict_info: + return + + record = { + "timestamp": sim_time, + "conflict_veh_info": vehicle_conflict_info + } + # write JSON Lines file in append mode + with open(conflict_info_file, "a") as f: + f.write(json.dumps(record) + "\n") + return + @profile def NADE_decision(self, env_command_information, env_observation): """NADE decision here. @@ -369,13 +388,17 @@ def NADE_decision(self, env_command_information, env_observation): ) # Step 2. Get maneuver challenge and mark the conflict vehicles and vrus. - env_maneuver_challenge, env_command_information = get_environment_maneuver_challenge( + env_maneuver_challenge, env_command_information, conflict_veh_info = get_environment_maneuver_challenge( env_future_trajectory, env_observation, env_command_information, centered_agent_set=self.excluded_agent_set, ) + # Step 2.5. Save the conflict vehicle information + if conflict_veh_info: + self.save_conflict_info(conflict_veh_info) + # Step 3. Add collision avoidance and acceptance command for the victim vehicles. env_future_trajectory, env_command_information = add_avoid_accept_collision_command( env_future_trajectory, diff --git a/packages/terasim-nde-nade/terasim_nde_nade/envs/nade_with_av.py b/packages/terasim-nde-nade/terasim_nde_nade/envs/nade_with_av.py index 8a97af6..81ee302 100644 --- a/packages/terasim-nde-nade/terasim_nde_nade/envs/nade_with_av.py +++ b/packages/terasim-nde-nade/terasim_nde_nade/envs/nade_with_av.py @@ -1,5 +1,6 @@ from addict import Dict import copy +import json from loguru import logger import numpy as np import random @@ -995,3 +996,21 @@ def collect_drl_obs(self, env_command_information): ) total_obs_for_DRL = np.clip(total_obs_for_DRL, -5, 5) return np.array(total_obs_for_DRL).astype(float) + + def save_conflict_info(self, conflict_veh_info): + sim_time = utils.get_time() + + conflict_info_file = self.simulator.output_path / "conflict_info.jsonl" + + vehicle_conflict_info = conflict_veh_info.get(AgentType.VEHICLE, {}) + if not vehicle_conflict_info: + return + + record = { + "timestamp": sim_time, + "conflict_veh_info": vehicle_conflict_info + } + # write JSON Lines file in append mode + with open(conflict_info_file, "a") as f: + f.write(json.dumps(record) + "\n") + return \ No newline at end of file diff --git a/packages/terasim-nde-nade/terasim_nde_nade/utils/nade/maneuver_challenge.py b/packages/terasim-nde-nade/terasim_nde_nade/utils/nade/maneuver_challenge.py index 4b313dd..f95486d 100644 --- a/packages/terasim-nde-nade/terasim_nde_nade/utils/nade/maneuver_challenge.py +++ b/packages/terasim-nde-nade/terasim_nde_nade/utils/nade/maneuver_challenge.py @@ -166,6 +166,7 @@ def get_environment_maneuver_challenge(env_future_trajectory, env_observation, e Returns: dict: Environment maneuver challenge information. dict: Updated environment command information. + dict: Conflict vehicle information. """ normal_future_trajectory_veh = Dict( { @@ -276,5 +277,4 @@ def get_environment_maneuver_challenge(env_future_trajectory, env_observation, e AgentType.VEHICLE: maneuver_challenge_veh, AgentType.VULNERABLE_ROAD_USER: maneuver_challenge_vru, } - return env_maneuver_challenge, env_command_information - + return env_maneuver_challenge, env_command_information, conflict_vehicle_info \ No newline at end of file diff --git a/scripts/gen_case_NADE.py b/scripts/gen_case_NADE.py new file mode 100755 index 0000000..f115565 --- /dev/null +++ b/scripts/gen_case_NADE.py @@ -0,0 +1,99 @@ +import argparse +import random +import hydra +from datetime import datetime +from loguru import logger +from omegaconf import DictConfig, OmegaConf +from pathlib import Path +from tqdm import tqdm +from terasim.logger.infoextractor import InfoExtractor +from terasim.simulator import Simulator + +from terasim_nde_nade.envs import NADE, NADEWithAV +from terasim_nde_nade.vehicle import NDEVehicleFactory +from terasim_nde_nade.vru import NDEVulnerableRoadUserFactory + +# Import resolve_config_paths function +from terasim_service.utils.base import resolve_config_paths + +# Add packages directory to sys path if needed +# sys.path.append(str(Path(__file__).resolve().parent.parent)) + + + +def main(config_path: str) -> None: + config = OmegaConf.load(config_path) + + # Convert OmegaConf to dict for path resolution + config_dict = OmegaConf.to_container(config, resolve=True) + + # Resolve all paths in config + config_dict = resolve_config_paths(config_dict, config_path) + + # Convert back to OmegaConf for attribute access + config = OmegaConf.create(config_dict) + + # Time (format: 2025-09-15_16-05-30) + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + base_dir = Path(config.output.dir) / config.output.name / "raw_data" / timestamp + base_dir.mkdir(parents=True, exist_ok=True) + + env = NADE( + vehicle_factory=NDEVehicleFactory(cfg=config.environment.parameters), + vru_factory=NDEVulnerableRoadUserFactory(cfg=config.environment.parameters), + info_extractor=InfoExtractor, + log_flag=True, + log_dir=base_dir, + warmup_time_lb=config.environment.parameters.warmup_time_lb, + warmup_time_ub=config.environment.parameters.warmup_time_ub, + run_time=1200, + configuration=config.environment.parameters, + ) + + # Paths already resolved in config + sumo_net_file = config.input.sumo_net_file + sumo_config_file = config.input.sumo_config_file + + sim = Simulator( + sumo_net_file_path=sumo_net_file, + sumo_config_file_path=sumo_config_file, + num_tries=10, + # gui_flag=config.simulator.parameters.gui_flag, + gui_flag=True, + realtime_flag=config.simulator.parameters.realtime_flag, + output_path=base_dir, + sumo_output_file_types=["fcd_all"], + traffic_scale=config.simulator.parameters.traffic_scale if hasattr(config.simulator.parameters, "traffic_scale") else 1, + additional_sumo_args=[ + "--device.bluelight.explicit","true", + ], + ) + sim.bind_env(env) + + terasim_logger = logger.bind(name="terasim_nde_nade") + terasim_logger.info(f"terasim_nde_nade: Experiment started") + + sim.run() + + +if __name__ == "__main__": + # Get all yaml files in examples/scenarios directory + config_dir = Path(__file__).parent / "examples" / "scenarios" + # yaml_files = sorted(config_dir.glob("*.yaml"), key=lambda x: int(''.join(filter(str.isdigit, x.stem)) or '0')) + # yaml_files = ["examples/scenarios/cutin.yaml"] + yaml_files = [Path("examples/scenarios/Mcity_corner_case_generation.yaml")] + # Randomly shuffle yaml files + random.shuffle(yaml_files) + + # Run experiments for each yaml file + for yaml_file in tqdm(yaml_files): + print(yaml_file) + logger.info(f"Running experiment with config: {yaml_file}") + main(str(yaml_file)) + # try: + # main(str(yaml_file)) + # except Exception as e: + # logger.error(f"Error running {yaml_file}: {e}") + # # yaml_file.unlink() # Delete the yaml file + # continue diff --git a/scripts/gen_case_NADE_with_av.py b/scripts/gen_case_NADE_with_av.py new file mode 100755 index 0000000..e0e3140 --- /dev/null +++ b/scripts/gen_case_NADE_with_av.py @@ -0,0 +1,100 @@ +import argparse +import random +import hydra +from datetime import datetime +from loguru import logger +from omegaconf import DictConfig, OmegaConf +from pathlib import Path +from tqdm import tqdm +from terasim.logger.infoextractor import InfoExtractor +from terasim.simulator import Simulator + +from terasim_nde_nade.envs import NADE, NADEWithAV +from terasim_nde_nade.vehicle import NDEVehicleFactory +from terasim_nde_nade.vru import NDEVulnerableRoadUserFactory + +# Import resolve_config_paths function +from terasim_service.utils.base import resolve_config_paths + +# Add packages directory to sys path if needed +# sys.path.append(str(Path(__file__).resolve().parent.parent)) + + + +def main(config_path: str) -> None: + config = OmegaConf.load(config_path) + + # Convert OmegaConf to dict for path resolution + config_dict = OmegaConf.to_container(config, resolve=True) + + # Resolve all paths in config + config_dict = resolve_config_paths(config_dict, config_path) + + # Convert back to OmegaConf for attribute access + config = OmegaConf.create(config_dict) + + # Time (format: 2025-09-15_16-05-30) + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + base_dir = Path(config.output.dir) / config.output.name / "raw_data" / timestamp + base_dir.mkdir(parents=True, exist_ok=True) + + env = NADEWithAV( + av_cfg = config.environment.parameters.AV_cfg, + vehicle_factory=NDEVehicleFactory(cfg=config.environment.parameters), + vru_factory=NDEVulnerableRoadUserFactory(cfg=config.environment.parameters), + info_extractor=InfoExtractor, + log_flag=True, + log_dir=base_dir, + warmup_time_lb=config.environment.parameters.warmup_time_lb, + warmup_time_ub=config.environment.parameters.warmup_time_ub, + run_time=1200, + configuration=config.environment.parameters, + ) + + # Paths already resolved in config + sumo_net_file = config.input.sumo_net_file + sumo_config_file = config.input.sumo_config_file + + sim = Simulator( + sumo_net_file_path=sumo_net_file, + sumo_config_file_path=sumo_config_file, + num_tries=10, + # gui_flag=config.simulator.parameters.gui_flag, + gui_flag=True, + realtime_flag=config.simulator.parameters.realtime_flag, + output_path=base_dir, + sumo_output_file_types=["fcd_all"], + traffic_scale=config.simulator.parameters.traffic_scale if hasattr(config.simulator.parameters, "traffic_scale") else 1, + additional_sumo_args=[ + "--device.bluelight.explicit","true", + ], + ) + sim.bind_env(env) + + terasim_logger = logger.bind(name="terasim_nde_nade") + terasim_logger.info(f"terasim_nde_nade: Experiment started") + + sim.run() + + +if __name__ == "__main__": + # Get all yaml files in examples/scenarios directory + config_dir = Path(__file__).parent / "examples" / "scenarios" + # yaml_files = sorted(config_dir.glob("*.yaml"), key=lambda x: int(''.join(filter(str.isdigit, x.stem)) or '0')) + # yaml_files = ["examples/scenarios/cutin.yaml"] + yaml_files = [Path("examples/scenarios/Mcity_corner_case_generation.yaml")] + # Randomly shuffle yaml files + random.shuffle(yaml_files) + + # Run experiments for each yaml file + for yaml_file in tqdm(yaml_files): + print(yaml_file) + logger.info(f"Running experiment with config: {yaml_file}") + main(str(yaml_file)) + # try: + # main(str(yaml_file)) + # except Exception as e: + # logger.error(f"Error running {yaml_file}: {e}") + # # yaml_file.unlink() # Delete the yaml file + # continue diff --git a/scripts/run_vis_gen.sh b/scripts/run_vis_gen.sh new file mode 100644 index 0000000..0eeae6c --- /dev/null +++ b/scripts/run_vis_gen.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Define base data directory +DATA_DIR="outputs/Mcity/raw_data/2025-11-03_19-35-25" +BASE_CONFIG="configs/visulation/example.yaml" +VISUALIZE_SCRIPT="scripts/visualize_fcd.py" + +# Define paths relative to the data directory +CONFLICT_FILE="$DATA_DIR/conflict_info.jsonl" +OUTPUT_DIR="$DATA_DIR/visualization" + +# Ensure output directory exists +mkdir -p "$OUTPUT_DIR" + +# Track processed pairs to avoid duplicates +declare -A PROCESSED_PAIRS + +# Temporary config file +TEMP_CONFIG="$OUTPUT_DIR/temp_config.yaml" + +# Read conflict scenarios from JSONL file +while IFS= read -r line; do + # Extract timestamp and conflict vehicle info + TIMESTAMP=$(echo "$line" | jq -r '.timestamp') + CONFLICTS=$(echo "$line" | jq -c '.conflict_veh_info | to_entries') + + # Iterate over all ego-adversary pairs in the conflict_veh_info + for conflict in $(echo "$CONFLICTS" | jq -c '.[]'); do + ADV_VEHICLE=$(echo "$conflict" | jq -r '.key') + EGO_VEHICLE=$(echo "$conflict" | jq -r '.value[0]') + + # Generate a unique key for the ego-adv pair + PAIR_KEY="${EGO_VEHICLE}_${ADV_VEHICLE}" + + # Log the pair being processed + echo "Checking pair: $PAIR_KEY" + + # Skip if this pair has already been processed + if [[ -n "${PROCESSED_PAIRS[$PAIR_KEY]}" ]]; then + echo "Skipping already processed pair: $PAIR_KEY" + continue + fi + + # Mark this pair as processed + PROCESSED_PAIRS[$PAIR_KEY]=1 + echo "Processing pair: $PAIR_KEY" + + # Copy base config to temporary config file + cp "$BASE_CONFIG" "$TEMP_CONFIG" + + # Update the config file with scenario-specific details using sed + sed -i "s|^fcd:.*|fcd: \"$DATA_DIR/fcd_all.xml\"|" "$TEMP_CONFIG" + sed -i "s|^net:.*|net: \"examples/maps/Mcity/mcity.net.xml\"|" "$TEMP_CONFIG" + sed -i "s|^ego_vehicle_id:.*|ego_vehicle_id: \"$EGO_VEHICLE\"|" "$TEMP_CONFIG" + sed -i "s|^adv_vehicle_id:.*|adv_vehicle_id: \"$ADV_VEHICLE\"|" "$TEMP_CONFIG" + sed -i "s|^video_name:.*|video_name: \"scenario_${TIMESTAMP}_${EGO_VEHICLE}_vs_${ADV_VEHICLE}\"|" "$TEMP_CONFIG" + sed -i "s|^start_time:.*|start_time: $TIMESTAMP|" "$TEMP_CONFIG" + sed -i "s|^end_time:.*|end_time: $(echo "$TIMESTAMP + 6" | bc)|" "$TEMP_CONFIG" + sed -i "s|^output_dir:.*|output_dir: \"$OUTPUT_DIR\"|" "$TEMP_CONFIG" + sed -i "/^max_frames:/d" "$TEMP_CONFIG" + + # Run the visualization script + python3 "$VISUALIZE_SCRIPT" "$TEMP_CONFIG" + done +done < "$CONFLICT_FILE" + +echo "All scenarios processed. Videos saved to $OUTPUT_DIR." diff --git a/scripts/visualize_fcd.py b/scripts/visualize_fcd.py index 53c57b6..5919b5c 100644 --- a/scripts/visualize_fcd.py +++ b/scripts/visualize_fcd.py @@ -5,6 +5,7 @@ from pathlib import Path import sys import yaml +import argparse from terasim_vis import Net, Trajectories @@ -309,6 +310,12 @@ def animate_with_follow(frame): return anim +def parse_arguments(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Visualize SUMO traffic simulation data.") + parser.add_argument("config", type=str, help="Path to the configuration YAML file.") + return parser.parse_args() + def main(path_to_config): """Main function.""" config = load_config(path_to_config) @@ -367,15 +374,6 @@ def main(path_to_config): if __name__ == "__main__": - # path_to_config = "vis_configs/France_Paris_PedestrianCrossing.yaml" - # path_to_config = "vis_configs/Germany_Rossfeld_AggressiveMerge.yaml" - # path_to_config = "vis_configs/US_Arizona_HighwayCutin.yaml" - # path_to_config = "vis_configs/US_AnnArbor_RoundaboutFailToYield.yaml" - path_to_config = "vis_configs/US_SanDiego_RunRedLight.yaml" - # path_to_config = "vis_configs/US_Chicago_UncoordinatedLeftTurn.yaml" - # path_to_config = "vis_configs/Mcity_CyclistCrossing.yaml" - # path_to_config = "vis_configs/Mcity_PedestrianCrossing.yaml" - # path_to_config = "vis_configs/Mcity_UnprotectedLeftTurn.yaml" - # path_to_config = "vis_configs/US_AnnArbor_RoundaboutFailToYield.yaml" - # path_to_config = "vis_configs/Demo_AnnArborRoundabout.yaml" + args = parse_arguments() + path_to_config = args.config main(path_to_config) \ No newline at end of file