Defining Your Own Oracle

In this tutorial, we will discuss how you can easily add a new oracle into our framework.

After DoppelTest executes a scenario, each ADS instance will produce its own record file. In the ADS we currently focused on (Baidu Apollo), you can either use CyberRT Developer Tools to replay the record file and visualize it in Dreamview, or use a Python library cyber_record to parse and analyze it.

root@in-dev-docker:/apollo# cyber_recorder -h
usage: cyber_recorder -h [options]
Unknown command: -h
usage: cyber_recorder <command> [<args>]
The cyber_recorder commands are:
        info    Show information of an exist record.
        play    Play an exist record.
        record  Record same topic.
        split   Split an exist record.
        recover Recover an exist record.

The record file consists messages published by each of the ADS modules at run time. For example, messages from topic /apollo/planning corresponds to output of the Planning module, messages from topic /apollo/localization/pose corresponds to output of the Localization module, etc.

Consider yourself trying to implement an oracle checking if the ADS is speeding at any point, we will call it SpeedingOracle.

Step 1: What does the oracle need?

The first step is to think about what the oracle does and what information does the oracle need. To know whether the ADS is speeding, we need 2 pieces of information: 1) the ADS’s current speed, 2) the speed limit of the lane which the ADS is currently traveling in. After understanding the need of the oracle, we need to figure out what messages from a record file should this oracle be looking at.

Luckily, localization messages alone are sufficient because each of them includes the position and velocity of the ADS.

Step 2: Laying out the structure

We have formally defined an interface for implementing a new oracle, named framework.oracles.OracleInterface. Each new oracle should implement 3 abstract functions defined in this interface.

class SpeedingOracle(OracleInterface):
    def get_interested_topics(self):
        pass

    def get_results(self):
        pass

    def on_new_message(self, topic, message, t):
        pass

Step 3: What Topics?

As discussed earlier, the speeding oracle only needs to know where the ADS is and what is its speed, therefore this oracle is only interested in messages from the localization topic.

def get_interested_topics(self):
    return ['/apollo/localization/pose']

Step 4: What to do?

on_new_message will get called each time a new message from the interested topic is received. The oracle should compute the ADS’s speed based on the message and check if it violated the speed limit.

def on_new_message(self, topic, message, t):
    current_speed = calculate_speed(message)
    current_position = get_position(message)

    # check which lane the ADS is currently in
    current_lane = get_current_lane(current_position)

    speed_limit = current_lane.speed_limit

    if current_speed > speed_limit:
        # a violation occurred
        pass

Note

Here we used some fake functions such as calculate_speed, get_position, and get_current_lane to illustrate the logic behind this oracle. The complete optimized implementation is provided at framework.oracles.impl.SpeedingOracle.py.

Step 5: What to return?

Now we have the first 2 parts completed, all that is left is to produce the result. In our current implementation, the oracle should produce a tuple describing what is the violation. Therefore we can modify on_new_message to store that violation, so we can return it later.

def on_new_message(self, topic, message, t):
    # ......
    if current_speed > speed_limit:
        self.violation = (
            'SpeedViolation',
            f'{current_speed} violated speed limit {speed_limit}'
        )
def get_result(self):
    return self.violation

Note

self.violation may be undefined if a violation never occurred. Try to implement this oracle and think about what to do about this!

Recap: 3 functions to define an oracle

The interface design provides an abstracted view of an obstacle: which record messages should the oracle look at? what should the oracle do with the messages? and at the end, what are the violations detected?