Skip to content

Movement-related modifiers

Movement-related scenario modifiers specify or constrain attributes of movement scenarios such as vehicle.drive(). They must appear either as members of other scenario modifiers or as members of a movement scenario after with:. For example:

OSC2 code: placement of movement modifiers
    do serial:
        car1.drive() with:
            speed([30..70]kph)

See complete example.

Some parameters can be passed by order (without the parameter name). If no arguments are passed, the defaults are applied. For example, the following three lane() invocations are supported and have the same effect.

OSC2 code: passing parameters by order
lane(lane: 1)
lane(1)
lane() # the default is 1

at

All movement modifiers have an optional at: parameter, with the following possible values:

  • all – the modifier is enforced throughout this phase (default)
  • start – the modifier is enforced at the start of the phase
  • end – the modifier is enforced at the end of the phase

run_mode

Most movement modifiers have an optional run_mode parameter that controls how modifier constraints are enforced during scenario execution. If execution deviates from the original plan due to unexpected SUT behavior, for example, enforcing movement modifier constraints during execution can cause the scenario to exit before completion. Using the run_mode parameter, you can relax enforcement in order to complete execution.

To avoid an incomplete-scenario error, you can specify in the modifier invocation that the behavior is only requested and should not fail the whole scenario if it is not met. To do this, add the following parameter to the invocation: run_mode: best_effort.

In the following example, car1 drives for three seconds at the speed of 50kph. However, the test does not fail even if car1 actually moves at a different speed:

OSC2 code: use of run_mode: best effort
car1.drive(duration: 3sec) with:
     speed(50kph, run_mode: best_effort)

You can also control the enforcement of a modifier -- or a scenario -- from outside of the invocation using the override() modifier.

tolerance

Tolerance is an expression of type speed, length, time or acceleration specifying the maximum allowed deviation from the planned speed, length, time or acceleration. This value is not used for planning or generation but extends the valid range used in monitoring and test execution. Generation always gives a value within the original range and tolerance is used only if execution cannot keep the original range because it is deviating too much from the plan. The default value for tolerance depends on the modifier.

Absolute or relative movement modifiers

The scenario modifiers that set a speed, position, or lane can be either absolute or relative. For example:

OSC2 code: absolute and relative movement modifiers
    do parallel(duration:[1.5..3]second, overlap:equal):
        sut.car.drive()
        car1.drive() with:
            position([10..20]meter, at: start) # Absolute distance from start of path
            speed([10..15]kph, faster_than: sut.car) # Relative
            lane(same_as: sut.car) # Relative)

See complete example.

The relative versions require two vehicles moving in parallel. They may also have multiple parameters such as faster_than and slower_than, but at most you can specify only one. These two constraints are checked at compile time.

These modifiers have a track parameter that specifies whether the vehicle reacts to the actual behavior of the referenced vehicle or to its expected behavior. By default, the vehicle reacts to the actual behavior of the referenced vehicle, but there may be cases where the referenced vehicle behaves in ways that you want the vehicle to ignore. In those cases, you can require the vehicle to track the projected behavior of the referenced vehicle.

For example, consider the l2: lane() and the p2: position() modifiers in the phase2 phase of the following scenario.

OSC2 code: track actual movement of referenced vehicle
    do serial():
        phase1: parallel(duration:[1.5..3]second, overlap:equal):
            d1: sut.car.drive()
            d2: car1.drive()
        phase2: parallel(duration:[1.5..3]second, overlap:equal):
            d3: sut.car.drive()
            d4: car1.drive() with:
                l1: lane(side_of: sut.car, at: start)
                p1: position(time: [0.5..1]second, ahead_of: sut.car, at: start)
                l2: lane(same_as: sut.car, at: end)
                p2: position(time: [1.5..2]second, ahead_of: sut.car, at: end)

See complete example.

Because of these modifiers, at the end of phase2:

  • car1 ends on the same lane as sut.car, even if sut.car has changed lanes.
  • car1 ends ahead of sut.car, even if it has accelerated.

This behavior may be exactly what you intended. But perhaps what you intended is:

  • car1 ends in the lane where sut.car was at the start of the p2 phase.
  • car1 ends [1.5..2]second ahead of where sut.car would have been, if it had continued at the same speed it was at the start of p2.

In this case, change the two lines to:

OSC2 code: track projected movement of referenced vehicle
                l2: lane(same_as: sut.car, at: end, track: projected)
                p2: position(time: [1.5..2]second, ahead_of: sut.car, at: end, track: projected)

See complete example.

vehicle.acceleration() modifier

Note

The acceleration modifier is currently supported only during scenario generation, not at runtime. As a result, the modifier may not be applied correctly during execution. We strongly recommend using the speed modifier instead. For example, rather than defining acceleration for a phase, specify the speed at both the beginning and end of the phase, and calculate the resulting acceleration. The acceleration modifier will be deprecated in the near future.

Purpose

Specify the rate of acceleration of a vehicle actor.

Category

Modifier

Syntax

acceleration([acceleration: ] <acceleration-exp> [,
              run_mode: <run_mode>] [,
              tolerance: <tolerance>])

Parameters

[acceleration: ]<acceleration-exp>

Is either a single value or a range appended with an acceleration unit. This value or range is used in planning. The unit is kphps (kph per second) or mpsps (meter per second per second).

run_mode: <run-mode>
See Common parameters.
tolerance: <tolerance>
The default is 1mpsps. See Common parameters for more information.

Example

In the following example, car1 accelerates by 5kph every second, for example, from 0 to 100kph in 50 seconds. The value of 5kph is uses during generation. However, because of the default tolerance of 1mpsps, the actual acceleration may be 1mpsps more or less than 5kph.

OSC2 code: acceleration() modifier
    do serial:
        car1.drive() with:
            acceleration(5kphps)

See complete example.

vehicle.avoid_collisions()

Purpose

Allow or disallow a vehicle actor to collide with another object.

Category

Modifier

Syntax

avoid_collisions([avoid:] <bool> [,
                 run_mode: <run_mode>])

Parameters

avoid: <bool>
Is either true or false.
run_mode: <run_mode>
See Common parameters.

Description

By default, all actors avoid collisions (avoid_collisions(true)). This means that the runtime mechanism for collision avoidance is on for the drive() scenario specified by the modifier. When set to false, the actor moves regardless of surrounding traffic and may collide.

Example

OSC2 code: avoid_collisions()
    do serial:
        car1.drive() with:
            avoid_collisions(false)

See complete example.

vehicle.change_lane()

Purpose

Specify that a vehicle actor change lane.

Category

Modifier

Syntax

change_lane([[lane: ]<value>] [,
             [side: ]<av-side>] [,
             run_mode: <run_mode>])

Parameters

[lane: ]<value>
Is the number of lanes to change from, either a single value or a range. The default is 1.
[side: ]<av-side>
Is left or right. <av-side> is randomized if not specified.
run_mode: <run_mode>
See Common parameters.

Example

OSC2 code: change_lane()
    do serial:
        car1.drive() with:
            # Change lane one lane to the left
            change_lane(side: left)


    do serial:
        car1.drive() with:
            # Change the lane 1, 2 or 3 lanes to the right
            change_lane([1..3], right)

See complete example 1 and complete example 2.

vehicle.change_speed()

Purpose

Change the speed of a vehicle actor for the current context.

Category

Modifier

Syntax

change_speed([speed: ]<speed-exp> [,
              run_mode: <run_mode>] [,
              tolerance: <tolerance>])

Parameters

speed: <speed-exp>
Is either a single value or a range. This value or range is used for planning. You must specify a speed unit.
run_mode: <run_mode>
See Common parameters.
tolerance: <tolerance>
The default is 2kph. See Common parameters for more information.

Example

In the following example, the specified range of [-20..20]kph is used to generate the value for the run. However, because of the default tolerance value of 2 kph, the valid range during runtime validation is [-22..22].

OSC2 code: change_speed()
    do serial:
        car1.drive() with:
            change_speed([-20..20]kph)

See complete example.

vehicle.distance() modifier

Purpose

Constrain the distance traveled by a vehicle actor during a movement

Category

Modifier

Syntax

distance([distance:] <distance> [,
          run_mode: <run_mode> ] [,
          tolerance: <tolerance>])

Parameters :

[distance:] <distance>
Is the distance the actor should travel in the current movement.
run_mode: <run_mode>
See Common parameters.
tolerance: <tolerance>
The default is 1m. See Common parameters for more information.

Description

Constrains the distance traveled by an actor during a movement such as drive().

Example

In the following example, the distance traveled by car1 during the d1 phase should be within 30 to 50 meters.

OSC2 code: distance()
    do serial():
        d1: car1.drive() with: distance([30..50]m)

See complete example.

vehicle.driver_controls()

Purpose

Enable or disable throttle, brake and steering commands of a vehicle actor

Category

Modifier

Syntax

driver_controls(<control>: <av_driver_control_state> [,
                <control>: <av_driver_control_state>, ...])

Parameters

<control>
Is one of throttle, brake or steer.
<av_driver_control_state>
Is one of enabled or disabled. Initially, all parameters are set to undefined_driver_control_state.

Description

When an Advanced Driver Assistance System (ADAS) function such as Adaptive Cruise Control (ACC) or Emergency Steering Assist (ESA) is engaged, the SUT typically controls either the longitudinal movement (throttle and brake) or the lateral movement (steering) while the human driver (played by Foretify) controls the other.

In a scenario where an ADAS function has been activated, you must use driver_controls() to enable the use of modifiers that effectively change the speed or lateral direction of the drive().

Example

OSC2 code: driver_controls()
import "$FTX/env/basic/adas_hlm_imps/ACC/acc_hlm_imp.osc"

extend test_config:
    set map = "$FTX_PACKAGES/maps/M81_FTX_highway_straight_5km_3lane_oneway.xodr"
    set implicits_kind = none
    set test_drain_time = 0second

scenario sut.cancel_acc_with_cancel_button_press:

    # How much slower the ftx_driver should drive at after ACC cancellation
    # (verifying that controls have been passed back to ftx_driver properly).
    speed_delta_to_brake: speed with:
        keep(it == 5mps)

    # Desired cruise speed.
    ACC_cruise_speed: speed with:
        keep(default it == 11mps)

    # ACC actual speed should be, at the "steady state" phase, in the range of:
    # ACC_cruise_speed - ACC_tolerance < actual speed < ACC_cruise_speed + ACC_tolerance
    ACC_tolerance: speed with:
        keep(default it == 1mps)

    # "Grace" time for ACC to reach the target cruise speed.
    ACC_transition_drive_duration: time with:
        keep(default it == [2..4]second)

    # The duration of the "steady state" phase.
    ACC_cruise_speed_drive_duration: time with:
        keep(default it == [5..7]second)

    do serial():

        # Turn on ACC.
        sut.car.press_ACC_main_button()

        # Some intermediate drive.
        after_main_button_press: sut.car.drive(duration: [1..5]second) with:
            position(distance: 50meter, at: start)
            speed(speed:  ACC_cruise_speed, run_mode: best_effort)

        # Start ACC.
        sut.car.press_ACC_set_button(cruise_speed: ACC_cruise_speed)

        # Transitioning into ACC's cruise speed.
        transitioning_into_cruise_speed: sut.car.drive(duration: ACC_transition_drive_duration)

        # ACC "steady state" drive.
        acc_drive_with_cruise_speed: sut.car.drive(duration: ACC_cruise_speed_drive_duration)

        # Cancel ACC with cancel button press.
        sut.car.press_ACC_cancel_button()

        # Decelerating.
        decelerating_after_cancel: sut.car.drive(duration: [1..5]second) with:
            s: speed(ACC_cruise_speed - speed_delta_to_brake, at: end, run_mode: best_effort)

See complete example.

vehicle.driver_model()

Purpose

Override the default simulator model of an NPC vehicle driver for a specific drive()

Category

Modifier

Syntax

driver_model([driver_model:] any_driver_model)

Parameters

driver_model: <any_driver_model>
Is any driver model supported by Foretify and the simulator you are using for the test.

Description

The types of driver models supported by simulators vary, but in general you can use these models to define default behavior, such as speed and lane changes, by setting the vehicle.driver_model field. You can then override this default for a specific drive() using the driver_model() modifier. Driver model settings are sticky, meaning that if you set it on a specific drive, all following drives use the same model.

vehicle.duration()

Purpose

Specify the duration of a movement or a scenario

Category

Modifier

Syntax

duration([time: ]<time> [,
          run_mode: <run_mode>][,
          tolerance: <tolerance>])

Parameters

[time: ]<time>
Is a value of type time specifying the expected duration of the movement or the scenario.
run_mode: <run_mode>
See Common parameters.
tolerance: <tolerance>
The default is 0s. See Common parameters for more information.

Example

OSC2 code: duration()
scenario top.my_drive:
  car1: vehicle

  duration([6..9]sec, tolerance: 1sec)

  do serial:
      car1.drive()
      log("start drive $(top.time)")
      car1.drive()  with:
        speed([50..70]kph)
        duration([3..5]sec, tolerance: 1sec)
      log("end drive $(top.time)")

extend top.main:
  do serial:
    log("start scenario $(top.time)")
    my_drive()
    log("end scenario $(top.time)")

See complete example.

Here are the results of the run.

Foretify output:
Starting the test ...
Running the test ...
Loading configuration ... done.
[0.000] [MAIN] Executing plan for top.all@1 (top__all)
[0.000] [PLANNER] Planned scenario time: 14.42 seconds
[7.440] [MAIN] start scenario 7.44second
[9.660] [MAIN] start drive 9.66second
[12.520] [MAIN] end scenario 12.52second
[12.520] [MAIN] end drive 12.52second
[17.520] [MAIN] Run finished
[17.520] [MAIN] Ending the run
[17.520] [SIMULATOR] stop_run()
Run completed

vehicle.keep_lane() modifier

Purpose

Specify that a vehicle actor stay in the current lane.

Category

Modifier

Syntax

keep_lane([run_mode: <run_mode>])

Parameters

run_mode: <run_mode>
See Common parameters.

Example

OSC2 code: keep_lane()
    do serial:
        car1.drive() with:
            keep_lane()

See complete example.

vehicle.keep_position()

Purpose

Maintain absolute position of a vehicle actor for the current context.

Category

Modifier

Syntax

keep_position([run_mode: <run_mode>])

Parameters

run_mode: <run_mode>
See Common parameters.

Example

OSC2 code: keep_position()
    do serial:
        car1.drive() with:
            keep_position()

See complete example.

vehicle.keep_speed()

Purpose

Maintain absolute speed of a vehicle actor for the current period.

Category

Modifier

Syntax

keep_speed([run_mode: <run_mode>] [,
            tolerance: <speed>])

Parameters

run_mode: <run_mode>
See Common parameters.
tolerance: <tolerance>
The default is 2kph. See Common parameters for more information.

Example

In the following example, the valid range during the second drive for car1 is [38..62]kph because of the default tolerance of 2kph.

OSC2 code: keep_speed()
    do serial:
        car1.drive() with:
            speed([40..60]kph)
        car1.drive() with:
            keep_speed()

See complete example.

Note: The keed_speed() maintains the speed as it was at the beginning of the relevant phase. If for some reason the speed was diverted from the plan (could happen when using best_effort or extreme tolerance), the driver will maintain that diverted runtime speed. If the target speed of the phase is an important factor of the scenario intent, consider using speed() modifier instead with an explicit speed target.

vehicle.lane() modifier

Purpose

Set the lane in which a vehicle actor moves.

Category

Modifier

Syntax

lane([[lane: ]<lane>]
    [, right_of | left_of | same_as: <ref-vehicle >] |
    [, side_of: <ref-vehicle > [, side: <av-side>]] |
    [leftmost: <bool> | rightmost: <bool> | innermost: <bool> | outermost: <bool>| middle: <bool>]
    [, from: <road-side>]
    [, at: <event>]
    [, track: <track>]
    [, run_mode: <run_mode>])

Parameters

[lane: ]<lane>

Is a single integer or range of integers indicating a required lane or range of lanes for a drive:

  • If no lane parameter is specified, the default is 1.
  • If no other vehicle is referenced, the parameter values are treated as absolute values referring to specific lanes. In this case a lane parameter of [-1..1] refers to lane 1, by default, the lane closest to the center of the road.
  • If another vehicle is referenced, the parameter values are treated as values relative to the lane of the other vehicle. In this case a lane parameter of [-1..1] refers to these lanes:

    • One lane to the left of the referenced vehicle (by default, 1)
    • The same lane as the referenced vehicle (0)
    • One lane to the right of the referenced vehicle (by default, -1)
right_of, left_of, same_as: <ref-vehicle >
Specify that the vehicle must be in the same or different lane from the referenced vehicle and hard code the position relative to the referenced vehicle. <ref-vehicle> is a named instance of the vehicle actor, for example car2.
side_of: <ref-vehicle >
Specifies that the vehicle must be in a different lane from the referenced vehicle and lets you randomize the position relative to the referenced vehicle.
side: <av-side>
Specifies which side of the vehicle the referenced vehicle is on. <av-side> is right or left.
leftmost, rightmost, innermost, outermost, middle: <bool>
Specify the lane farthest to the left or right, the innermost or the outermost, or any lane except those lanes (middle). <bool> is true or false.
from: <road-side>
Change the default numbering of the lanes in a road. The default <road-side> value, center, specifies that the lanes are numbered starting with the lane closest to the center of the road. curb specifies that the numbering starts with the lane closest to the side of the road. You can use from: curb when, for example, you want a vehicle close to the curb because of the presence of a pedestrian. See also the Description below.
at: <event>
See Common parameters.
<track>: <track>
Is actual or projected. The default is actual, meaning that the vehicle reacts to the behavior of the referenced vehicle.
run_mode: <run_mode>
See Common parameters.

Description

Regardless of whether the traffic_side_request option is set to right_hand or left_hand the lane numbering is the same. The default numbering is from the center of the road. Using from: curb reverses the numbering.

Example

OSC2 code: lane()
    do serial:
        car1.drive() with:
            # Drive in lane closest to the center of the road
            lane(1)


    do serial:
        car1.drive() with:
            # Drive in lane closest to the curb
            lane(1, from: curb)


    do parallel(overlap:equal):
        car2.drive()
        car1.drive() with:
            # Drive one lane left of car2
            lane(left_of: car2)


    do parallel(overlap:equal):
        car2.drive()
        car1.drive() with:
            # At the end of this phase, be either one or two lanes
            # to the right of car2
            lane([1..2], right_of: car2, at: end)


    do parallel(overlap:equal):
        car2.drive()
        car1.drive() with:
            # Be either one left, one right or the same as car2
            lane([-1..1], right_of: car2)


    do parallel(overlap:equal):
        car2.drive()
        car1.drive() with:
            # Be in the same lane as car2
            lane(same_as: car2)

See complete example 1, complete example 2, complete example 3, complete example 4, complete example 5, and complete example 6.

vehicle.lateral() modifier

Purpose

Specify the lateral offset of a vehicle actor from a reference line.

Category

Modifier

Syntax

lateral([distance: <distance>] [,
         line: <line>] [,
         at: <event>] [,
         run_mode: <run_mode>] [,
         tolerance: <tolerance>])

Parameters

distance: <distance>

Is the lateral offset of the vehicle from the reference line. This value or range is used for planning purposes. The default is [-10.0..10.0] centimeters.

The distance parameter value cannot be greater than the distance between the reference line and the road boundary, ensuring that the vehicle's center line is always within the road, including the boundary.

Notes

The planned position of the vehicle depends on the reference line, the lane width, and the vehicle width.

If the reference line is center, calculating the position of the vehicle relative to the lane requires knowing the lane width, especially to determine how far the vehicle can shift without leaving the lane.

If the reference line is left or right, you're using a lane boundary as your base, but the lane width (along with the vehicle width) may still be needed to ensure the vehicle stays within the lane or road boundary.

For example:

  • If the reference line is left, is the distance between the left side of the vehicle and the left boundary of the lane.
  • If the reference line is center, is the distance between the center of the vehicle and the center of the lane.
line: <line>
Is the reference line from which the lateral offset is measured: right (the right lane boundary), left (the left lane boundary) or center (the center of the lane). The default is center.
at: <event>
See Common parameters.
run_mode: <run_mode>
See Common parameters.
tolerance: <tolerance>
The default is 1m. See Common parameters for more information.

Description

The following diagram depicts how distance is measured when line is left, center, or right.

Figure 1: vehicle.lateral with left, center, or right reference line

See also Foretify's Coordinate System.

Example

In the following example, the lateral() modifier specifies that car1 should be positioned with its right side 1.5m to the left of the right lane marking. However, because of the default tolerance of 1m, car1 may actually be positioned with its right side [0.5..2.5]m to the left of the right lane marking.

OSC2 code: lateral()
    do serial:
        car1.drive() with:
            speed([40..60]kph)
        car1.drive() with:
            lateral(distance: 1.5meter, line: right, at: start)

See complete example.

vehicle.position() modifier

Purpose

Set the position of a vehicle actor along the x (longitude) dimension

Category

Modifier

Syntax

position([distance: ]<distance> | time: <time> [,
    ahead_of: <vehicle > | behind: <vehicle >] [,
    measure_by: av_measure_by] [,
    at: <event>] [,
    track: <track>] [,
    run_mode: <run_mode>] [,
    tolerance: <length> | time_tolerance: <time>])

Parameters

[distance:] <distance>
Is a single value or a range with a length unit. This value or range is used for planning.
time: <time>
Is a single value or a range with a time unit. This value or range is used for planning. If you specify ahead_of, time is calculated based on the speed of the reference car. If you specify behind, time is calculated based on this vehicle's speed.
ahead_of, behind: <vehicle>
Is a named instance of the vehicle actor, for example car2.
measure_by: <av_measure_by>

Defines whether distance between cars is measured from the center of their bounding boxes (center_to_center) or from the nearest edge of their bounding boxes (nearest). The default is center_to_center.

When used with ahead_of, nearest is measured from the front bumper of the reference vehicle to the back bumper of this vehicle. A positive value means this vehicle is in front of the reference vehicle, whereas a negative value means this vehicle can actually be behind or overlapping the reference vehicle.

When used with behind, nearest is measured from the back bumper of the reference vehicle to the front bumper of this vehicle. A positive value means this vehicle is behind the reference vehicle, whereas a negative value means this vehicle can actually be in front or overlapping the reference vehicle.

These measurements use road coordinates.

at: <event>
Is start, end or all. The default is all, meaning that the specified position is maintained throughout the current period unless time is specified along with ahead_of or behind_of. In that case, the physical distance between the actors may vary during the time period. See Description below for more details.
track: <track>
Can be actual or projected. The default is track: actual, meaning that the vehicle reacts to the behavior of the reference vehicle. Use track: projected to predict how the vehicle will react to the reference vehicle's future movements. For more information about the differences, see Position track: projected.
run_mode: <run_mode>
See Common parameters.
tolerance: <length>
The default is 1m. This parameter can be used only if the distance parameter is set. See Common parameters for more information.
time_tolerance: <time>
The default is 0.1sec. This parameter can be used only if the time parameter is set. See Common parameters for more information.

Description

The position() modifier lets you specify the position of an actor relative to the start of the path or relative to another actor. You can specify the position by distance or time (but not both).

When ahead_of is specified for a vehicle, it must be ahead of the referenced vehicle by the specified value in the relevant period. Since behind contradicts ahead_of, you cannot use them together.

When time is specified along with ahead_of or behind_of, the physical distance is calculated using the speed of the vehicle that is behind (irrespective of whether it's the scenario's actor or the referenced vehicle ) and the location at that moment of the vehicle that is ahead. The speed of the vehicle that is ahead is not taken into the calculation.

If <event> is all, the physical distance at any point in time refers to the speed of the vehicle that is behind at that moment and the location of the vehicle that is ahead at that same moment (meaning that the physical distance may vary during the time period). The following two examples are equivalent; in both cases the physical distance is calculated according to the speed of car2.

OSC2 code: equivalent use of position() example 1
    do parallel(overlap:equal):
        car1.drive() with:
            speed(speed: 30kph, at: end)
        sut.car.drive() with:
            position(time: 3second, behind: car1, at: end)

See complete example.

OSC2 code: equivalent use of position() example 2
    do parallel(overlap:equal):
        car1.drive() with:
            position(time: 3second, ahead_of: sut.car, at: end)
        sut.car.drive() with:
            speed(speed: 40kph, at: end)

See complete example.

Example

OSC2 code: position()
    do serial:
        car1.drive() with:
            # Absolute from the start of the path
            position([10..20]meter)


    do parallel(overlap:equal):
        car1.drive()
        car2.drive() with:
            # 40 meters ahead of car1 at end
            position(40meter, ahead_of: car1, at: end)


    do parallel(overlap:equal):
        car1.drive()
        car2.drive() with:
            # Behind car1 throughout
            position([20..30]meter, behind: car1)


    do parallel(overlap:equal):
        sut.car.drive()
        car1.drive() with:
            # Behind sut.car, measured by time
            position(time: [2..3]second, behind: sut.car)

See complete example 1, complete example 2, complete example 3, and complete example 4.

Example with specified tolerance

In the following scenario, when validating the position() modifier, the relative position is checked according to the distance of the vehicle ahead of sut.car. The distance range excluding tolerance is set to [5..10]m. With tolerance, the checked distance range is [3..12]m.

OSC2 code: position() with tolerance
    do car1.drive(duration: 5s) with:
        position(distance: [5..10]m, ahead_of: sut.car, tolerance: 2m)

See complete example.

vehicle.speed() modifier

Purpose

Set the speed of a vehicle actor for the current period.

Category

Modifier

Syntax

speed([speed: ]<speed-exp> [,
    faster_than: <vehicle > | slower_than: <vehicle >] [,
    at: <event>] [,
    track: <track>] [,
    run_mode: <run-mode>] [,
    tolerance:<tolerance>])

Parameters

[speed: ]<speed-exp>
Is either a single value or a range. This value or range is used for planning. You must specify a speed unit.
faster_than, slower_than: <vehicle>
Is the instance name of the vehicle actor, for example car2.
at: <event>
See Common parameters.
track: <track>
Is actual or projected. The default is actual, meaning that the vehicle reacts to the behavior of the referenced vehicle.
run_mode: run_mode
See Common parameters.
tolerance: <tolerance>
The default value is 2kph. See Common parameters.

Description

When faster_than is specified, the context vehicle must be faster than the referenced vehicle by the specified value in the relevant period. slower_than contradicts faster_than, so you cannot use them together.

Example

OSC2 code: speed()
    do serial:
        sut.car.drive() with:
            # Absolute speed range
            speed([10..20]kph)


    do parallel(overlap:equal):
        car1.drive()
        car2.drive() with:
            # Faster than car1 by [1..5]kph
            speed([1..5]kph, faster_than: car1)


    do serial:
        car1.drive() with:
            # Have that speed at end of the phase
            speed(5kph, at: end)


    do parallel(overlap:equal):
        car1.drive()
        car2.drive() with:
            # Really either slower or faster than car1
            speed([-20..20]kph, faster_than: car1)

See complete example 1, complete example 2, complete example 3, and complete example 4.

Example with specified tolerance

In the following scenario, generation uses [40..60]kph range, but during runtime validation, the valid range is [35..65]kph.

OSC2 code: speed() with tolerance
    do car1.drive(duration: 5s) with:
        speed([40..60]kph, tolerance: 5kph)

See complete example.

vehicle.shape()

You can constrain the attributes of a vehicle's movement, such as its position, by defining an arbitrary two-dimensional shape, such as a curve. Then, in a movement scenario such as vehicle.drive(), you can constrain the attributes of the vehicle's position to conform to the curve.

To do this, you implement a compute() function that describes a position along a curve by setting two parameters, a longitude (X) and a latitude (Y), as shown in Figure 1. This function can then be called repeatedly to describe the position of an actor as a function of time.

Figure 2: The position of an actor as a function of time

The tasks required to constrain a movement attribute to a shape are:

  • Creating an arbitrary shape.
  • Using the shape in a movement scenario such as vehicle.drive().

These steps are described in the sections below. If you are using a shape created by someone else, it is not necessary to understand how to create a shape. It is sufficient to know the type of the required parameters.

Note

vehicle.shape() is not supported for actors other than vehicle.

Using an arbitrary shape

Using a shape has two tasks:

  • Declaring an instance of the shape in a scenario.
  • Constraining the movement scenario with the shape() modifier.

The shape instance declaration

To use a shape in a scenario, you declare an instance of the shape struct and constrain any generatable parameters required as input to the shape’s compute() function. The compute() function produces the output fields, such as lon and lat for any_position_shape. Every shape also has a duration() function that returns a parameter limiting the maximum time required to move an actor along the entire shape.

The number of parameters required to define a shape varies according to the shape. For example, while a sinusoidal shape can be defined with three parameters, a clothoid shape requires more. In this example, two parameters specify the radius and the angle of an arc shape.

OSC2 code: define arc shape
scenario vehicle.drive_with_shape:
    c: arc_shape with (radius: 15m, angle: 10deg)

See complete example.

Depending on how the shape is implemented, you may be able to constrain additional parameters, such as:

  • transform specifies whether the longitudinal and lateral values output by the compute() function must be transformed:

    • relative specifies that the values are relative to the actual value of the attribute when the drive starts. This is the default for most shapes.
    • lane-relative transforms the values to be relative but takes into account the shape of the lane.
    • absolute does not transform the values. Thus any previous or subsequent drive() actions by the same actor must adjust to this drive() action. The previous drive() action cannot specify an exact attribute value (position, speed or so on) at end, and the subsequent drive() action cannot specify an exact attribute at start.
  • direction specifies the orientation of the vehicle to the direction of the shape. For example, reverse specifies that the vehicle face backwards to the direction of the shape.

  • time specifies the maximum time to complete movement along the shape. More typically, this is determined by the duration() function.

If the input parameters to the compute() function are not constrained, the corresponding fields get a generated value during the planning phase and then are fixed throughout the calls to compute().

The shape() modifier

Purpose

Constrain the position attributes of a vehicle actor's movement to conform to a shape

Category

Modifier

Syntax

shape([quantity: <shape-quantity>][,
    request: <request>, ]
    shape_object: <shape-instance> [,
    run_mode: <run_mode>])

Parameters

quantity: <shape-quantity>
Specifies the kind of attribute calculated by the compute() function of the shape. position is the default and specifies shapes inheriting from any_position_shape. position is the only supported value for this release.
request: <request>
Specifies whether the longitudinal parameter, the lateral parameter, or both are constrained in this scenario. both is the default, meaning that both longitudinal and lateral parameters are constrained. both is the only supported value for this release.
shape_object: <shape-instance>
Is the name of the shape instance in the scenario. The type of this instance must match the type of <shape-quantity>.
run_mode: <run-mode>
See Common parameters.

Description

This modifier constrains the attributes of a movement, such as the longitudinal and lateral parameters of a vehicle's position, to a shape defined by a shape instance in the scenario.

By default, shape() does not take into account the time allocated to the drive() movement. The drive() conforms to the specified shape until the result returned by the shape’s duration() function is reached, unless a condition specified by until() is reached first. If you specify, for example, an angle of 30deg for an arc shape but the drive() ends after only 10deg because the maximum duration is reached, it is not considered a scenario failure.

Example

This example declares an instance of arc_shape with the name arc1 and constrains the two parameters of the shape. In phase 1 of the scenario, sut drives for 10 seconds. In phase 2, sut drives along arc1 until the maximum duration is reached.

OSC2 code: constrain a movement with shape()
scenario vehicle.drive_with_shape:
    arc1: arc_shape with (radius: 15m, angle: 10deg)
    hw: highway
    car1: vehicle

    do serial:
        p1: car1.drive(duration: 10s) with: along(hw)
        p2: car1.drive() with: shape(shape_object: arc1)

Creating an arbitrary shape

Creating a shape has two required tasks:

  • Declaring a struct in OSC2 with the necessary parameters.
  • Implementing the calculations as native (OSC2) or external (not OSC2) methods.

Optionally, you can also implement on_start() to perform parameter checking. You should also document the parameters for the users, specifying, for example, if they use a negative radius, the arc bends to the right rather than to the left of the starting direction of the actor.

any_shape and any_position_shape declaration

The struct any_position_shape inherits from any_shape, as shown below.

OSC2 code: any_shape and any_position_shape
struct any_shape:                                         # line 1
    var t: time                                           # line 2
    var direction: direction                              # line 3
    set direction = straight                              # line 4
                                                          # line 5
    def compute() is empty                                # line 6
    def duration(): time is empty                         # line 7
                                                          # line 8
struct any_position_shape inherits any_shape:             # line 9
    var lon: length                                       # line 10
    var lat: length                                       # line 11
    var yaw: angle                                        # line 12
    var has_explicit_yaw: bool = false                    # line 13
    transform: shape_transform with:                      # line 14
        keep(soft it==relative)                           # line 15
    ref_actor: vehicle with:                              # line 16
        keep(default it == null)                          # line 17

Notes

  • line 1 declares the any_shape type.

  • line 2 defines the time required to execute an incremental movement along this shape. This parameter is limited by the duration() function. If you want to allow users to force the duration from the outside, add a constrainable field to the shape object such as expected_duration, and make the duration() method return it.

  • line 3 declares a direction parameter and line 4 sets it to straight. If you prefer, you can specify a default constraint and allow users to constrain it to backward. This parameter specifies the orientation of the vehicle to the direction of the shape. See the direction parameter under Parametric API.

  • line 6 declares a compute() function implemented in a foreign language or OSC2.

  • line 7 declares a duration() function implemented in a foreign language or OSC2.

  • line 9 uses unconditional inheritance to create the any_position_shape subtype.

  • lines 10 and 11 are the relative longitude and latitude of a location on the shape. These values are set and updated by repeated calls to the shape's compute() function.

  • line 12 is the yaw (heading) angle at the current position on the shape. This is useful for Smart Replay scenarios where the original recorded orientation should be preserved.

  • line 13 is a flag indicating whether the shape provides an explicit yaw value. When has_explicit_yaw is true, the driver uses the yaw value from the shape directly instead of computing it from the trajectory direction using Ackerman correction. This is essential for Smart Replay where the recorded vehicle heading must be preserved exactly.

  • Lines 14 and 15 specify that the time, lon, and lat values returned by the compute() function should be transformed relative to the initial values of the position attributes at the start of the drive. The constraint on transform is soft, meaning it can be overridden if needed.

  • Lines 16 and 17 specify the ref_actor when a shape is defined relative to another actor. If ref_actor is not null, the lon and lat values are interpreted relative to the position of the ref_actor on the road (supported only in lane-relative transform).

Using attribute-specific structs like these, you can declare a new subtype for the shape you want to create. The number of required parameters depends on the specific shape.

For example:

OSC2 code: struct arc_shape implementation
struct arc_shape inherits any_position_shape:                               # line 1
    angle: angle with: keep(soft it == 0deg)                                # line 2
    radius: length with: keep(soft it == 0m)                                # line 3
    var time: time                                                          # line 4
    var request: shape_request                                              # line 5
                                                                            # line 6
    def duration()-> time is also external cpp("duration", "arc_shape.so")  # line 7
    def compute() is also external cpp("compute", "arc_shape.so")           # line 8

Notes

  • line 1 uses unconditional inheritance to create the arc_shape subtype.

  • lines 2 and 3 declare the generatable parameters that the user must specify. It is recommended but not required to set unreasonable constraints and implement an on_start() method to check whether the user has set the parameters appropriately. These fields serve as input to compute().

  • line 4 declares the time parameter that is set by the shape logic to the relative time from the start of this drive.

  • line 5 is the type of shape request (both) as specified in the shape() modifier.

  • lines 7 and 8 redefine the duration() and compute() functions defined in any_shape with logic defined in C++.

External functions

Two C++ functions are required: duration() and compute().

The duration() function returns a parameter that sets the maximum time required to move an actor along the entire shape such that:

C++ code: duration() function
0 <= t <= duration()

For example:

C++ code: duration() function
time_t duration() {
    time_t total_duration = 10_sec ;
    return total_duration ;
};
OSC2 code: struct arc_shape implementation
struct arc_shape inherits any_position_shape:                               # line 1
    angle: angle with: keep(soft it == 0deg)                                # line 2
    radius: length with: keep(soft it == 0m)                                # line 3
    var time: time                                                          # line 4
    var request: shape_request                                              # line 5
                                                                            # line 6
    def duration()-> time is also external cpp("duration", "arc_shape.so")  # line 7
    def compute() is also external cpp("compute", "arc_shape.so")           # line 8

The shape logic sets time to the relative time from the start of the drive and then calls the compute() function.

The compute() function assigns the longitudinal and lateral parameters. For example, in C++ this should look something like:

C++ code: compute() function
void arc_shape_t::compute() {
    this->set_lon( Some computation using this->time() and other params);
    this->set_lat( Some computation using this->time() and other params);
}

Notes

  • The compute() function can be called any number of times, not necessarily with sequential times. Thus, it should depend only on its parameters and have no side effects.

  • The shape must be continuous and smooth so that a vehicle is able to move along it. Unnatural shapes are not allowed.

  • The shape must be defined so that at the starting point (at t=0), lon = 0 , lat = 0 and lat_speed = 0. At the starting point, the speed of the shape must equal the speed at the end of the previous drive.

Shape behavior (ShapeBehavior)

This section provides details about trajectory shape behavior. Trajectory shape behavior is used to instruct the driver to execute a predefined trajectory. This behavior receives as input a shape object that holds the trajectory description; the object is passed to the behavior under the active move_op.

Trajectory shape object API

All trajectory shape objects inherit from the any_position_shape struct and are required to implement the functional API described below. This API is used by the trajectory shape behavior to calculate the lon-lat positions dictated by the shape. In addition, any_position_shape also holds a set of parameters that instruct the trajectory shape behavior on how the shape should be executed.

Functional API

Following are the shape object methods that are called by the trajectory shape behavior when it constructs the trajectory. These methods are implemented by the shape object creator.

Function Input Return Description Supported Comments
duration() - time_t Returns the overall duration of the trajectory. Yes Used by the Planner to allocate the relevant drive duration.
set_t(T) time_t - Sets the time T for which the lon-lat values will be calculated after calling compute(). Yes
compute() - - Makes the shape object calculate the lon-lat values for the provided time T. Yes
lon() - distance_t Returns the longitudinal positon for time T. Yes Should adhere to the continuity requirements.
lat() - distance_t Returns the lateral positon for time T. Yes Should adhere to the continuity requirements.

duration() and compute() must be implemented for any shape object.

  • duration() - Returns the overall duration of the shape trajectory. This value is used to execute the trajectory, as well as for planning the move.
  • compute() - Implementation of shape trajectory. This method should assign a value to lon and lat for a given value of t. (See Continuity requirements for an example.)
Parametric API

The following parameters define how to interpret the lon() and lat() computed in the shape object.

Parameter Type Description Default Comments
shape_transform enum [relative] - The lon-lat values are interpreted in the vehicle-relative coordinate frame, with the origin at the vehicle's geometric center, the lon axis pointing forward, and the lat axis pointing left

[lane_relative] - The lon-lat values are projected on the lane geometry. The lon axis starts at the vehicle's geometric center and follows the lane geometry. The lat axis is perpendicular to the lon direction at every point.

[absolute] - The lon-lat values are interpreted as x-y position on the map. The positions are taken as-is with no additional processing (This mode is intended for use with Smart-Replay tests).
relative
direction enum [straight] - The shape should be interpreted as describing forward driving.

[backward] - The shape should be interpreted as describing backward driving (reverse).
straight The direction enum defines other options but only straight and backward are supported.
Behavioral API

The following parameters define the behavior and execution of the shape.

Parameter Type Description Default Comments
tracking_mode enum [exact] - The trajectory defined by the shape is used as is by the ftx_driver (overriding any other behaviors)

[behavioral] - The trajectory will be followed while allowing other ftx_driver behavior to override.
exact
ref_actor_tracking_mode enum actual - Uses the current actual state of the ref_actor to position the relative shape.

projected - Uses a previously saved state of the ref_actor, assuming it moves at a constant speed, to position the relative shape.
actual

By default, the tracking_mode is set to exact. Use the following constraint to change the tracking_mode in a scenario:

OSC2 code: setting behavioral tracking_mode
keep(lead_vehicle.ftx_driver.shape_behavior.tracking_mode == behavioral)

Note

Using tracking_mode == behavioral may introduce discontinuities in the trajectory. To ensure continuous vehicle motion, use with ftx_driver.kinematic_executor_params.bicycle_tracker.enable == true.

By default, the ref_actor_tracking_mode is set to actual. Use the following constraint to change it in a scenario:

OSC2 code: setting behavioral tracking_mode
keep(lead_vehicle.ftx_driver.shape_behavior.ref_actor_tracking_mode == projected)

In projected tracking mode, the state of the ref_actor is assumed to remain constant from the shape's start time, based on the assumption of constant speed.

Note

Switching from ref_actor_tracking_mode == projected back to actual is not supported, as it may introduce discontinuities in the trajectory.

Continuity requirements

To ensure a smooth transition from following the regular plan objective to following the shape, the shape should continue the kinematic state from the Preceding Drive End (PDE). This is especially important when the vehicle is controlled kinematically. To achieve this, consider the following constraints when defining the shape object.

\[ lon(T = 0) == 0 \]
\[ lat(T = 0) == 0 \]
\[ lon(T=t\_step) == PDE.speed\_lon * t\_step \]
\[ lat(T=t\_step) == PDE.speed\_lat * t\_step \]

These constraints are derived from the forward derivation formulas for speed and acceleration.

The following example demonstrates how this continuity can be implemented in a scenario.

OSC2 code: continuity implemented in a scenario
scenario sut.native_lane_change:
    init_lon_speed: speed with:
        keep(it == 20kph)
    init_lat_pos: length with:
        keep(it == 0m)

    lane_change: lane_change_shape with:
        keep(it.speed_lon == init_lon_speed)
        keep(it.total_duration == 10s)
        keep(it.lateral_distance == 3m)
        keep(it.lateral_acceleration == 0.5mpsps)
        keep(it.lane_change_direction == left)
        keep(it.transform == lane_relative)

    do serial:
        pre_shape: sut.car.drive(duration: 2s) with:
            speed(init_lon_speed, at: end)
            lateral(init_lat_pos, line: center, at: all)
        shape_drive: sut.car.drive() with:
            shape(quantity:position, request:both, shape_object: lane_change)

struct lane_change_shape inherits any_position_shape:
    speed_lon: speed with:
      keep (default speed_lon == 5mps)
    total_duration: time with:
      keep (default total_duration == 10s)
    lateral_distance: length with:
        keep(default lateral_distance == 3.5m)
    lateral_acceleration: acceleration with:
        keep(default lateral_acceleration == 0.5mpsps)
    lane_change_direction: av_side with:
        keep(default lane_change_direction == left)

    def duration()-> time is only:
        return total_duration

    def compute() is only:
        lon = speed_lon * t
        lat = 0m

        var acc_rl : float = lateral_acceleration/1millimeter_per_sec_sqr
        var lat_dist_rl :float = lateral_distance/1mm
        var t1 :=  math.sqrt(lat_dist_rl / acc_rl) * 1s

        if t > 0s:
            var tau : time
            if t > t1:
                tau = t1
            else:
                tau = t
            lat = lat + 0.5 * lateral_acceleration * tau * tau

        if t > t1:
            var tau : time
            if t > 2 * t1:
                tau = t1
            else:
                tau = t - t1
            var v_m := lateral_acceleration * t1
            lat = lat + v_m * tau - 0.5 * lateral_acceleration * tau * tau

        if lane_change_direction == right:
            lat = -lat

See complete example.

Note

The lateral movement continuity is assured by keeping a constant lateral position before the shape phase, and in doing so, zeroing the lateral speed and acceleration.

Trajectory shape behavior implementation

The following sections describe implementation details and assumptions for different trajectory shape behavior modes.

Relative shape transform

At the start of the shape drive, the behavior calculates the (X-Y) trajectory starting at T = 0 until T = duration(). The trajectory starts at the current position of the vehicle (X0, Y0, θ0), and steps from there in the vehicle coordinate system directions:

\[ traj\_X(T) = X_0 + shape\_lon(T) * cos(θ_0) - shape\_lat(T) * sin(θ_0) \]
\[ traj\_Y(T) = Y_0 + shape\_lon(T) * sin(θ_0) - shape\_lat(T) * cos(θ_0) \]

To preserve the lon-lat trajectory continuity, the (traj_X, traj_Y) points are later converted to (traj_lon, traj_lat).

Lane relative shape transform

At the start of the shape drive, the behavior calculates the (lon-lat) trajectory starting at T = 0 until T = duration(). The trajectory starts at the current position of the vehicle in route coordinates (lon0, lat0), and steps from there in the lane coordinate system directions:

\[ traj\_lon(T) = lon0 + shape\_lon(T) \]
\[ traj\_lat(T) = lat0 + shape\_lat(T) \]

The (traj_lon, traj_lat) points are later converted to (traj_X, traj_Y), which follows the road.

Note

To ensure the continuity of the vehicle’s yaw angle and the smoothness of the lateral speed, the car's lateral speed in the preceding drive must be zero. This way, the vehicle coordinates are aligned to the direction of the lane coordinates at the start of the shape.

driving relatively to a reference actor

When a ref_actor is specified in a lane_relative shape, the shape's position is interpreted as a relative distance from the reference actor. Given the future time T, the final absolute shape will be: $$ absolute_traj_lon(T) = ref_actor_lon(T) + relative_traj_lon(T) $$

\[ absolute\_traj\_lat(T) = ref\_actor\_lat(T) + relative\_traj\_lat(T) \]

Note

During the entire duration of the relative shape, the ref_actor must be located on the actor’s planned path.

Absolute shape transform

The values set in the lon and lat fields are interpreted as x and y, respectively. They are placed in the trajectory directly as-is with no additional manipulation.

To preserve the lon-lat trajectory continuity, the traj_X and traj_Y points are converted to traj_lon and traj_lat.

Note

Trajectory continuity is not enforced for absolute shape transforms. Writers must ensure that the vehicle's position at the start of the drive matches the x-y coordinates at the beginning of the shape. In Smart Replay, this means the creation point must align with the start of the recording.

Backward shape direction

To make the shape go backward, the shape_lon() value is inverted by multiplying it by -1 when calculating the (traj_lon, traj_lat) points.

\[ traj\_lon(T) = lon0 + (-1) * shape\_lon(T) \]

In addition, the traj_yaw angle is flipped by 180deg to make the backward motion.

Notes

  1. The traj_lat is not flipped.
  2. The flip to the backward direction works the same way as for the relative shape transform.
  3. To maintain continuity of the motion (especially in kinematic drives), backward shapes must start and end with zero speed.

Exact tracking_mode

In exact tracking mode, the behavior sets the calculated trajectory directly into the action.trajectory and sets the action.driver_controls for the trajectory recalculation to false:

action.driver_controls.trajectory_lon = false
action.driver_controls.trajectory_lat = false

In addition, for the purpose of visual debugging, the behavior sets the action.objective based on the last shape trajectory point. Other behaviors need to check the action.driver_controls and not intervene in case the trajectory recalculation is false for both axes.

Behavioral tracking_mode

In behavioral tracking mode, the behavior searches on the calculated trajectory for a point that is closest to the current lon position of the vehicle. The objective is created based on a point that is look_ahead_time ahead of the closest point to the vehicle. The objective is updated every update_period or when the vehicle reaches the existing objective.

This objective is evaluated by other behavior in the driver’s behavioral stack (e.g., speed-to-curvature adaptation, collision avoidance, etc.).

When reaching less than look_ahead_time from the end of the shape trajectory, the last point is used as the objective.

Note

This model does not apply to backward-direction shapes.