Coding week19 10/07-10/13

Parsing and generating driving test cases in CARLA Town02 map with OpenDRIVE

This blog was previously documented in LaTeX, so some formatting discrepancies might occur during the migration. You can view the PDF source file via Overleaf link.

Building on last week’s insights, we recognized that LLMs alone may not accurately get specific distance, which prompted us to explore alternative methods. This week, we experimented with parsing CARLA’s Town02 map in OpenDRIVE format to automate test case generation. By extracting roads and junctions, we defined structured test cases with precise start and end points, route lengths, and directional commands (Left, Right, Straight). Additionally, we implemented an action-distance aware control approach, pairing each action with a specified distance to enhance the precision of driving commands in simulation.

Introduction

This experiment involves parsing the route map from CARLA (Town02 as an example), provided in the OpenDRIVE (.xodr) format, and generating test cases that define driving scenarios. The map file is available in Github repo.

These test cases include start and end indices, route lengths, and high-level commands such as Left, Right, and Straight.

The goal is to automate test case generation based on the map structure, which includes roads, junctions, and their connections. This report details the steps of the experiment, including code, results, and analysis.

Parsing OpenDRIVE Map

The Town02 map file is provided in OpenDRIVE format. This map contains essential elements such as roads and junctions, which define the road network’s topology. The OpenDRIVE file contains:

The Python code used to parse the OpenDRIVE map file:

import xml.etree.ElementTree as ET

tree = ET.parse(file_path)
root = tree.getroot()

# Extract roads and junctions
roads = []
junctions = []

for element in root:
    if element.tag == 'road':
        roads.append(element)
    elif element.tag == 'junction':
        junctions.append(element)

# Function to extract road details
def parse_road(road_element):
    road_id = road_element.attrib['id']
    road_length = float(road_element.attrib.get('length', 0))
    return {{'id': road_id, 'length': road_length}}

parsed_roads = [parse_road(road) for road in roads]

def parse_junction(junction_element):
    junction_id = junction_element.attrib['id']
    connections = []
    for connection in junction_element.findall('connection'):
        incoming_road = connection.attrib['incomingRoad']
        connecting_road = connection.attrib['connectingRoad']
        connections.append({
            'incoming_road': incoming_road,
            'connecting_road': connecting_road
        })
    return {{'id': junction_id, 'connections': connections}}

parsed_junctions = [parse_junction(junction) for junction in junctions]

From the OpenDRIVE map, we parsed 68 roads, with lengths such as:

[
  {{
    "id": "20",
    "connections": [
      {{
        "incoming_road": "13",
        "connecting_road": "21"
      }}
    ]
  }},
  {{
    "id": "55",
    "connections": [
      {{
        "incoming_road": "8",
        "connecting_road": "66"
      }}
    ]
  }}
]

Action-aware Route Generation

Using the parsed roads and junctions, we generate routes between start and endpoints. Additionally, high-level driving commands such as Left, Right, and Straight are determined based on the relative orientation of consecutive road segments.

The following code was used to generate random test cases:

import random

def generate_test_case(parsed_roads, parsed_junctions):
    start_road = random.choice(parsed_roads)
    end_road = random.choice(parsed_roads)

    route_length = start_road['length'] + end_road['length'] + random.uniform(20, 100)

    # Please note here it's randomly generated
    possible_commands = ['Left', 'Right', 'Straight']
    commands = [random.choice(possible_commands) for _ in range(2)]

    return {{
        'start_index': int(start_road['id']),
        'end_index': int(end_road['id']),
        'route_length': round(route_length, 2),
        'commands': commands
    }}

test_cases = [generate_test_case(parsed_roads, parsed_junctions) for _ in range(10)]
Start Index End Index Route Length (m) High-Level Commands
124 70 93.71 Right, Straight
110 119 89.68 Straight, Straight
288 229 113.14 Straight, Left
9 4 107.76 Left, Straight
111 298 89.86 Straight, Right

Below is the JSON representation of the generated test cases:

[
    {{
        "start_index": 124,
        "end_index": 70,
        "route_length": 93.71,
        "commands": ["Right", "Straight"]
    }},
    {{
        "start_index": 110,
        "end_index": 119,
        "route_length": 89.68,
        "commands": ["Straight", "Straight"]
    }},
    {{
        "start_index": 288,
        "end_index": 229,
        "route_length": 113.14,
        "commands": ["Straight", "Left"]
    }},
    {{
        "start_index": 9,
        "end_index": 4,
        "route_length": 107.76,
        "commands": ["Left", "Straight"]
    }},
    {{
        "start_index": 111,
        "end_index": 298,
        "route_length": 89.86,
        "commands": ["Straight", "Right"]
    }}
]

Action-Distance Aware Control

In our previous approach, the control system was designed to be action-aware. This means that between any two junctions (e.g., Junction1 to Junction2), we defined the driving actions such as Left, Right, or Straight. However, this method does not take into account the distance required to perform these actions.

This motivates us to redefine the control system as action-distance aware, where both the driving action and the associated distance are considered as part of the control strategy. By integrating distance awareness, we aim to control the precise execution of actions based on the distance travelled.

In the new approach, each control instruction is associated with both an action and a distance. This means that for each route segment, we define not only the type of action to be taken but also the precise distance over which the action must be performed.

New Action-Distance Aware Test Case

{{
    "start_index": 124,
    "end_index": 70,
    "route_length": 93.71,
    "commands": [
        {{
            "action": "Right",
            "distance": 50
        }},
        {{
            "action": "Straight",
            "distance": 43.71
        }}
    ]
}}

In this new test case, the vehicle will:

This new action-distance aware approach allows for a more flexible and accurate driving behavior, with both actions and distances specified for each route segment.

We integrated distance measurement and action switching into model control. The video shows the model adjusting high-level commands based on distance, though some cases still need improvement.