Skip to content

OSC2 expressions

Expressions are used within type members, for example:

  • Constraints
  • Expression method implementations
  • Event definitions

Each expression produces a value. Because the language is strongly typed, each expression has a type that is known at compile time. Foretify recognizes the following data types:

  • The numeric, physical, Boolean and enumerated types are scalar types that hold one value at a time.
  • The string type holds a sequence of ASCII characters enclosed in quotes.
  • The struct, actor, action, scenario, and modifier types are compound types that hold multiple values of multiple types.
  • The list type is an aggregate type that holds an ordered collection of values of one type.
  • The external_data type holds a pointer to an external (not OSC2) entity, such as a C++ class. This type is most often used by external methods.

Expressions are composed of atoms. Atoms are identifiers, keywords or constant literals of various types. Atoms are combined using operators and method application to create compound expressions.

Identifiers

User-defined identifiers (names) in OSC2 code consist of a case-sensitive combination of any length, containing the characters A–Z, a-z, 0-9, and underscore (_). User-defined identifiers beginning with a digit or an underscore are not allowed.

The following identifiers are predefined in some contexts:

  • self refers to the current type (struct, actor, action or scenario).
  • it exists in a with context, referring to the with subject.
  • actor exists in scenario declarations, and refers to the related actor instance.
  • outer exists in an in context, and refers to the type in which in is declared.

Example of predefined identifiers

OSC2 code: predefined identifiers
scenario top.C:
    c_val: int

scenario top.B:
    b_val: int
    do c1: C()

scenario top.A:
     a_val: int
     do b1: B()

# Example1:
extend top.A:
    in1: in b1.c1 with:
        keep(outer.a_val == self.b_val)
        keep(self.b_val == it.c_val)

# In the above example:
#    outer: is the A scenario
#    self: is the B scenario
#    it: is the c1 scenario inside the B scenario

# Example2:
extend top:
    top_val: int

extend top.A:
    in2: in b1 with:
        keep(it.actor.top_val == it.b_val)
        keep(self.a_val == it.b_val)

# In the above example:
#    actor: is top
#    self: is the A scenario
#    it: is the b1 scenario

See complete example.

Scoping rules for accessing objects within a with block

A with block starts with with: which defines an implicit variable called it. What it represents is depending on the context of the with block, see below for more details. To access an object, such as a field within it, use it.<field-name>. This rule applies to all other attributes of the object, such as methods and events. In addition, all regular scoping rules apply within such blocks.

The it context is defined as follows:

  • In field declarations, the it represents the field.
  • In on blocks, the it represents the event data associated with the on block.
  • In watcher declaration, the it represents the watcher instance.
  • In in blocks, the it represents the in object.
  • In scenario/modifier invocation, the it represents the invocation.
OSC2 code: 'self' field shadowed by 'it' field
scenario vehicle.x:
    do serial():
        a()
        b()
    with:
        keep(it.duration == self.duration)

See complete example.

Note: The special syntax of path expression starting with a dot ('.') is not allowed. For example, keep(.duration == self.duration) is not allowed.

Keywords

The following are keywords, and cannot be used as names:

actor action and as bool call checker collect
const cover def default do elapsed else embed
emit empty enum event every expression extend external
fall float first_of for global if hard import
in inherits init int is is also is first is only
it keep label list of match modifier multi_match new
not null of on one_of only or override
parallel pass post-plan previous_do properties range record remove_default
repeat rise SI sample scenario serial set soft
string struct synchronize top trace true type uint
undefined unit until var wait watcher with person

pass keyword

The keyword pass has no semantic meaning. You can place the pass statement inside any block as a placeholder for future code. This includes empty definitions that are being inherited and implemented later. The execution of the pass statement has no impact but it allows you to have empty code blocks where it is not allowed, without getting an error.

This includes struct, modifier, and scenario definitions, with blocks, native code blocks, scenario do blocks, and constraints.

Examples:

OSC2 code: pass keyword
actor my_actor:
  pass

scenario top.some_scenario:
  do serial:
    pass
    another_scenario() with:
       pass

See complete example.

You can also use the pass statement in FRun template files.

Literals

Boolean literals

The Boolean literals true and false are of type bool. They represents truth (logical) values.

OSC2 code: Boolean literals
    true_value: bool with:
        keep(it == true)

See complete example.

Enumerated type literals

Enumerated types represent a set of explicitly named values. Enumerated types are assigned integer values, either automatically or within the declaration. However, assigning an integer value to a field or variable of an enumerated type requires the as() casting operator.

OSC2 code: enumerated type literals
enum rgb_color: [red, green, blue]
enum cmyk_color: [cyan = 1, magenta = 2, yellow, black]

extend top.main:
   #Example enumeration fields
   var my_rgb_color: rgb_color = green
   var my_cmyk_color: cmyk_color = black

   # Conversion to integer
   var x: int = my_rgb_color.as(int) # x == 1
   var y: uint = my_cmyk_color.as(uint) # y == 4

   # Conversion from integer
   var my_car_color: cmyk_color = 3.as(cmyk_color) # my_car_color == yellow

See complete example.

Numeric literals

Numeric literals include signed and unsigned integers of 64 bits in size as well as floats, represented as 64-bit floating point numbers. Floats are equivalent to C++ double.

Name Type
int 64-bit integer
uint 64-bit unsigned integer
float 64-bit floating point number

You must specify integers in decimal format without commas or underscores. For floats, a decimal point may be used, as well as an exponent notation, either positive or negative. The compiler currently supports, for example:

OSC2 code: numeric literals
-123.45
123.45e+6
123.45E-6
0.45e-03

Physical type literals

Physical types are used to characterize physical movement in space, including speed, length, angle and so on. When you specify a value for one of these types in an expression, or when you define coverage for it, you must use a unit. The unit must be appended to the value without spaces. As shown in the table below, you have a choice of units for the predefined types.

Physical constants have implied types. For example, 12.5km has an implied type of length.

Physical expressions that use fields rather than constants, such as start_speed-1kph are allowed.

Arithmetic expressions involving physical types are resolved using dimensional analysis. For example, length/time resolves to speed.

Note

Physical types are stored internally as double-precision floating-point values in SI units. When comparing two physical values using the equality operator ('=='), a small tolerance (approximately 1e-9 in SI units) is applied to account for floating-point precision. For explicit tolerance-based comparisons, use the math.nearly_eq() family of methods.

Examples:

OSC2 code: physical type literals
2meter
1.5s
[30..50]kph
[30kph..50kph]
[(start_speed-1kph)..(start_speed+1kph)]
6m/3s

The following tables show the predefined physical types and their units.

length

type length is SI(m: 1)

Unit Factor
nanometer 0.000000001
nm 0.000000001
millimeter 0.001
mm 0.001
centimeter 0.01
cm 0.01
meter 1.0
m 1.0
kilometer 1000
km 1000
inch 0.0254
feet 0.3048
mile 1609.344
mi 1609.344

time

type time is SI(s: 1)

Unit Factor
millisecond 0.001
ms 0.001
second 1.0
sec 1.0
s 1.0
minute 60
min 60
hour 3600
h 3600

speed

type speed is SI(m: 1, s: -1)

Unit Factor
meter_per_second 1.0
mps 1.0
kilometer_per_hour 0.277777778
kmph 0.277777778
kph 0.277777778
mile_per_hour 0.447038889
mph 0.447038889
miph 447038889

acceleration

type acceleration is SI(m: 1, s: -2)

Unit Factor
meter_per_sec_sqr 1.0
mpsps 1.0
mile_per_hour_per_sec 0.447038889
kmphps 0.277777778

jerk

type jerk is SI(m: 1, s: -3)

Unit Factor
meter_per_sec_cubed 1.0
mpspsps 1.0
mile_per_sec_cubed 1609.344
mipspsps 1609.344

angle

type angle is SI(deg: 1)

Unit Factor
degree 1.0
deg 1.0
radian 57.295779513

angular_rate

type angular_rate is SI(deg: 1, s: -1)

Unit Factor
degree_per_second 1.0
degps 1.0
radian_per_second 57.295779513
radps 57.295779513

angular_acceleration

type angular_acceleration is SI(deg: 1, s: -2)

Unit Factor
degree_per_second_sqr 1.0
degpsps 1.0
radian_per_second_sqr 57.295779513
radpsps 57.295779513

mass

type mass is SI(kg: 1)

Unit Factor
gram 0.001
kilogram 1.0
kg 1.0
ton 1000
pound 0.45359237
lb 0.45359237

temperature

type temperature is SI(K: 1)

Unit Factor Offset
K 1.0
kelvin 1.0
celsius 1.0 273.15
c 1.0 273.15
fahrenheit 0.555555556 255.372222222
f 0.555555556 255.372222222

luminous_flux

type luminous_flux is SI(lm: 1)

Unit Factor
lm 1.0
lumen 1.0

force ( Newton )

type force is SI(kg: 1, m: 1, s: -2)

Unit Factor
1N 1.0

torque ( Newton * meter )

type torque is SI(kg: 1, m: 2, s: -2)

Unit Factor
1Nm 1.0

The null constant

null is the default value for fields and variables of type struct.

The __file__ expression

The __file__ expression returns the current file path as a constant string.

OSC2 code: __file__ usage
def m() -> string is:
     return __file__   

Boolean operators

These operators compare two expressions (operands) and return a Boolean value. The table shows the operators in order of precedence from highest to lowest.

Operator type Operator Description Example
Negation not The expression is true if the operand is false. not x
Conjunction and The expression is true if both operands are true. x and y
Disjunction or The expression is true if one of the operands is true. x or y
Implication => The expression is true if either the first operand is false or both operands are true. x => y

Relational operators (numeric expressions)

These operators compare two integer, float or physical type expressions (operands) and return a Boolean value.

Operator type Operator Description Example
Equality == Returns true if the two operands have an equal value. x == y
Inequality != Returns true if the two operands do not have an equal value. x != y
Less than < Returns true if the first operand has a lower value than the second. x < y
Less than or equal to <= Returns true if the first operand is not greater than the second. x <= y
Greater than or equal to >= Returns true if the first operand is not less than the second. x >= y
Greater than > Returns true if the first operand has a greater value than the second. x > y
Membership in <range> Returns true if the value of the first operand is in the specified range. For physical types, the unit type must be specified. x in [i..j]
x in [i..]
x in [..j]
x in range(i, j)
Membership in <list> Returns true if the first operand is in the specified list. For physical types, the unit type must be specified. x in [i, j]
Elapsed time elapsed(<time>) This operator is allowed only in qualified events. It returns true if the time from the beginning of the context of the qualified event to the current time is equal or greater than the <time> parameter.

Relational operators (non-numeric expressions)

These operators compare two enumerated, string or Boolean type expressions (operands) and return a Boolean value.

Operator type Operator Description Example
Equality == Returns true if the two operands have an equal value. x == y
Inequality != Returns true if the two operands do not have an equal value. x != y
Membership in <list> Returns true if the first operand is a member of the specified list i in [i, j]

Arithmetic operators

Operator type Operator Description Example
Unary minus - Returns the opposite of a single operand. -x
Multiplication * Multiplies the two operands. x * y
Division / Divides the first operand by the second. x / y
Modulus % Divides the first operand by the second and returns the remainder. x % y
Addition + Adds the two operands. x + y
Subtraction - Subtracts the second operand from the first. x - y

Temporal operators

Temporal operators can be used only inside a qualified event.

The rise() operator

The rise operator is a Boolean operator that returns true if and only if an expression’s value changed from false to true.

Syntax

rise(<expression>)

Parameters

<expression>
(Required) A Boolean expression.

Example

OSC2 code: rise operator
extend top.main:
    do serial:
        accelerate: sut.car.drive(duration: 10s) with:
            speed(20kph, at:start)
            speed(50kph, at:end)
        decelerate: sut.car.drive(duration: 10s) with:
            speed(50kph, at:start)
            speed(20kph, at:end)
        accelerate_again: sut.car.drive(duration: 10s) with:
            speed(20kph, at:start)
            speed(50kph, at:end)

    on rise(sut.car.state.speed > 30kph):
        logger.log_info("SUT accelerated beyond 30kph")

This example results in the following output:

[7.860] [MAIN] SUT accelerated beyond 30kph
...
[27.680] [MAIN] SUT accelerated beyond 30kph

See complete example.

The fall() operator

The fall operator is a Boolean operator that returns true if and only if an expression’s value changed from true to false.

Syntax

fall(<expression>)

Parameters

<expression>
(Required) A Boolean expression.

Example

OSC2 code: fall operator
extend top.main:
    do serial:
        accelerate: sut.car.drive(duration: 10s) with:
            speed(20kph, at:start)
            speed(50kph, at:end)
        decelerate: sut.car.drive(duration: 10s) with:
            speed(50kph, at:start)
            speed(20kph, at:end)
        accelerate_again: sut.car.drive(duration: 10s) with:
            speed(20kph, at:start)
            speed(50kph, at:end)

    on fall(sut.car.state.speed > 30kph):
        logger.log_info("SUT speed dropped below 30kph")

This example results in the following output:

[19.960] [MAIN] SUT speed dropped below 30kph

See complete example.

The elapsed() operator

Expressions with the elapsed() operator can act as either:

  • A qualified event.

    In this case, the elapsed operator is used to determine when the event is emitted.

  • The condition of a qualified event.

    In this case, the elapsed operator is used to emit the defined event based on some other condition that may occur.

Whether used as a qualified event or as a condition of a qualified event, elapsed(<time>) returns true if the time from the beginning of the context of the event to the current time is equal to or greater than the <time> parameter. The context of the event is determined as follows:

  • If the event is used in a wait action, elapsed time is calculated from the start of the wait action.

  • If the event is declared in a scenario or modifier, elapsed time is calculated from the start of that scenario or modifier.

  • If the event is declared in a struct or actor, elapsed time is calculated from the start of the test, in other words, the start of top.all.

Example with elapsed()

The following elapsed() example demonstrates events emitted from within a scenario and from top.main.

OSC2 code: use of elapsed() in events
import "$FTX_BASIC/exe_platforms/sumo_ssp/config/sumo_config.osc"

extend test_config:
    set map = "$FTX_PACKAGES/maps/M73_FTX_highway.xodr"

scenario timeout:
    event d
    event e is @d if elapsed(2s)
    event f is elapsed(5s)

    on @d:
        call logger.log_info("Event d emitted: Only explicitly")
    on @e:
        call logger.log_info("Event e emitted: Event d was emitted more than 2 seconds from 'do' start")
    on @f:
        call logger.log_info("Event f emitted: More than 5 seconds passed from 'do' start - re-emitted every cycle (20 ms by default) as condition is true")

    do serial:
        wait elapsed(3s)
        emit d
        wait elapsed(3s)

extend top.main:
    event a
    event b is @a if elapsed(4s)
    event c is elapsed(6s)

    on @a:
        call logger.log_info("Event a emitted: Only explicitly")
    on @b:
        call logger.log_info("Event b emitted: Event a was emitted more than 4 seconds from top.main start")
    on @c:
        call logger.log_info("Event c emitted: More than 6 seconds passed from top.main start - re-emitted every cycle (20 ms by default) as condition is true")

    do serial:
        wait elapsed(1s)
        emit a
        timeout()
        emit a

The following events are emitted in the example.

top.main events

Event Behavior
event a Emitted only explicitly.
event b is @a if elapsed(4s) Emitted if event a was emitted more than 4 seconds from the start of top.main.
event c is elapsed(6s) Emitted if more than 6 seconds have passed from the start of top.main. As condition holds true after 6 seconds, re-emitted every cycle (20 ms by default).

timeout scenario events

Event Behavior
event d Emitted only explicitly.
event e is @d if elapsed(2s) Emitted if event d was emitted more than 2 seconds from the start of the timeout scenario.
event f is elapsed(5s) Emitted if more than 5 seconds passed from the start of the timeout scenario. As condition holds true after 5 seconds, re-emitted every cycle (20 ms by default).

Following is the output from the elapsed example:

Foretify output
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[1.000] [MAIN] Event a emitted: Only explicitly
[4.000] [MAIN] Event e emitted: Event d was emitted more than 2 seconds from 'do' start
[4.000] [MAIN] Event d emitted: Only explicitly
[6.000] [MAIN] Event f emitted: More than 5 seconds passed from 'do' start - re-emitted every cycle (20 ms by default) as condition is true
[6.000] [MAIN] Event c emitted: More than 6 seconds passed from top.main start - re-emitted every cycle (20 ms by default) as condition is true
[6.020] [MAIN] Event f emitted: More than 5 seconds passed from 'do' start - re-emitted every cycle (20 ms by default) as condition is true
[6.020] [MAIN] Event c emitted: More than 6 seconds passed from top.main start - re-emitted every cycle (20 ms by default) as condition is true
...
[6.980] [MAIN] Event f emitted: More than 5 seconds passed from 'do' start - re-emitted every cycle (20 ms by default) as condition is true
[6.980] [MAIN] Event c emitted: More than 6 seconds passed from top.main start - re-emitted every cycle (20 ms by default) as condition is true
[7.000] [MAIN] Event b emitted: Event a was emitted more than 4 seconds from top.main start
[7.000] [MAIN] Event a emitted: Only explicitly

Type operators

Operator type Operators Description Example
Type check is(<type>) Check whether an object is a specified type
Type cast <path-expression>.as(<type-name>) Cast an object to the specified type

Type casting

Casting is a way to convert an object of a certain type into another type. If the original element is of scalar type, a new instance is created with the required type. If the original element is of non scalar type, it is recognized as being of the required type after the cast.

Foretify supports two types of casting:

  • Casting from one struct type to another.
  • Casting from one scalar type to another.

These types of casting are described in more detail below.

Casting of one struct type to another:

The casting expression type has to have the same base type as the target expression type.

If during runtime the actual instance does not have the required type, an exception is issued. After the cast, the original expression is recognized as having the type requested in the cast operation.

In the following example, after the cast, the car is treated as a truck and therefore the trailer that belongs to the truck type can be accessed:

OSC2 code: as() type casting operator
extend box_truck:
    num_of_wheels: int with:
        keep(soft it == 6)

extend top.main:
    keep(sut.car.vehicle_category == box_truck)

    do serial:
        sut.car.drive(duration: 10sec)
        log("Truck SUT has $(sut.car.as(box_truck).num_of_wheels) wheels")

See complete example.

Casting from one scalar type to another:

In this kind of casting a new instance is created with the required type. For example:

OSC2 code: casting scalar types
extend top.main:
    var x: string
    set x = 5.as(string)

See complete example.

The following table shows the possible scalar castings. The columns represent the type cast from; the row represents the target type. Note the limitations listed below the table for certain types.

int uint float bool string enum physical
int yes yes yes yes yes - yes no
uint yes yes yes yes yes - yes no
float yes yes yes yes yes - no no
bool yes yes yes yes yes - no no
string yes yes yes yes yes yes yes
enum yes - yes - no no yes - yes* no
physical no no no no yes** no yes*

* only for the same type

** using the display units

- the casting may fail at runtime. An exception is issued in that case.

List operators

Operator type Operators Description Example
Equality == Compares two lists and returns true if every member of the two lists are considered equal. x == y
Inequality != Compares two lists and returns true if a member of the first list is considered not equal to the corresponding member of the second list. x != y
Membership in <list> Compares two lists and returns true if each member of the first list is found in the second list, otherwise false. x in [i, j]
List indexing [<list>[<int>]] Reference an item in a list my_list[3]
List size <list>.size() Returns the size of a list (the number of members) as an unsigned integer. my_list.size()

See also for item in list.

A list is a way to describe an ordered collection of similar values in OSC2. A list can contain any number of elements from a single data type, including:

  • Calls to methods that return the same data type as that of the list.
  • The results of an operation, such as the evaluation of a Boolean expression.

For example, you can declare a convoy to contain a list of car actors, or a shape as a list of points.

List literals are defined as a comma-separated list of items, for example:

OSC2 code: list literals
[point1, point2]

The [n..n] notation is not allowed for lists; it is reserved for ranges.

Example lists

OSC2 code: list declaration
    convoy: list of vehicle
    distances: list of length

See complete example.

Example list constraint

OSC2 code: list constraint
    distances: list of length with:
        keep(it == [12km, 13.5km, 70km])

See complete example.

String operators

Operator type Operators Description Example
Escape \ Escape a character. \t
Concatenate + Concatenate two strings. "Hello" + "world"
String continuation + \ Concatenate a string continued over multiple lines.
Interpolation $() Used within string literals to include the value of a variable.

The OSC2 predefined type string is a sequence of ASCII characters enclosed in quotes.

OSC2 code: string constraint
struct data:
    text: string with:
        keep(it == "Absolute speed of ego at start (in km/h)")

See complete example.

The default value of a field of type string is an empty string,“”.

Use string interpolation (embedding $() in a string) to construct strings out of non-string values. The $() operator converts an expression to a string and inserts it in place. For example:

OSC2 code: string interpolation
    do serial:
        log_info("There are $(cars.size()) cars.")

See complete example.

You can concatenate multiple strings with the + character. For example:

OSC2 code: string concatenation
"a string" + " with concatenation"

You can continue strings onto multiple lines with the + character and \ character:

OSC2 code: string continuation
"a string" + \
" with continuation"

Within a string, the backslash is an escape character, for example:

OSC2 code: use of backslash in strings
"My name is: \tJoe"

The new operator

A new expression allocates space for and initializes a variable of type actor or struct. It has the form new [type-name].

Each field in the new object is initialized with its default value:

  • For int - 0
  • For bool - false
  • For string - empty string
  • For struct - null
  • For list - an empty list

However, if a field in the object was declared with var and initialized, then it gets the initialized value.

new expressions are supported either in native methods or in declarative code except in the following contexts:

  • Constraints, such as keep(z == new w), are not supported.
  • Scenario invocations, such as do car1.my_new_ints(new my_ints), are not supported.
  • Set actions, such as set z = new, are not supported.
  • var fields in var instances of conditional inheritance types are not properly initialized. For example:

    • If struct a has a var field x initialized to 3, and
    • Struct b conditionally inherits from struct a and has a var field y initialized to 4,
    • In an instance b1 of struct b generated with new, var b1.x is 3 and var b1.y is 0 (instead of 4).
OSC code: Allocate variable in field declaration
struct my_ints:
    x: int
    y: int

extend top.main:
    var z: my_ints = new

    do call logger.log_info("z.x = $(z.x), z.y = $(z.y)")
OSC code: Allocate variable in native method
struct my_ints:
    x: int
    y: int

extend top.main:

    def new_ints() -> my_ints is:
        var w: my_ints = new
        return w

Watcher operators

Watchers are objects that emit interesting slices of time as intervals. Watcher operators let you create new watcher instances by either combining other watchers or by creating watchers that emit intervals based on other data.

The following sections describe the watcher operators. For an example that illustrates the operators, see Example: watcher operators.

passive_w operator

The passive_w operator does nothing. One must call start_interval() or end_interval() procedurally:

OSC code: passive_w definition
watcher modifier passive_w

not_w operator

The not_w operator creates intervals when an input watcher doesn't have an active interval.

OSC code: not_w definition
watcher modifier not_w:
    x: any_watcher_behaviour

For example, assume you have a custom watcher that creates intervals when a vehicle is inside a junction. You can create a watcher that will create an interval whenever the vehicle is outside of a junction, as in the following example:

OSC pseudocode: not_w example
watcher in_junction is ...
watcher not_in_junction is not_w(in_junction)

and_w operator

The and_w operator creates intervals when both input watchers have an active interval.

OSC code: and_w definition
watcher modifier and_w:

    x: any_watcher_behaviour

    y: any_watcher_behaviour

or_w operator

The or_w operator creates intervals when any of the input watchers have an active interval.

OSC code: or_w definition
watcher modifier or_w:

    x: any_watcher_behaviour

    y: any_watcher_behaviour

between_w operator

The between_w creates intervals between the two input events, x and y. The operator starts an interval when x is emitted, and ends the interval when y is emitted.

OSC code: between_w definition
watcher modifier between_w:

    event x

    event y

while_w operator

The while_w operator accepts a boolean expression and creates a new interval when the expression evaluates to true. The interval ends when the expression changes to false.

OSC code: while_w definition
watcher modifier while_w:

    condition: bool

above_w operator

The above_w watcher creates an interval representing the timeframe when a sampled value exceeds a threshold.

Syntax

OSC2 code: above_w operator syntax
watcher <watcher name> is above_w(sample_type: <numeric type>, sample_expression: <expression>, threshold: <expression>[, tolerance: <expression>] [, sample_event: <qualified event>])

Parameters

  • sample_type: The type of the value to sample must be numeric. It can be of type 'int' or any physical type.
  • sample_expression: The value to sample; must be of type sample_type.
  • threshold: The interval opening threshold; must be of type sample_type. The threshold expression is re-evaluated on every clock cycle.
  • tolerance (optional): The interval closing tolerance; must be of type sample_type.
  • sample_event (optional): A qualified event on which to evaluate the sample_expression. Default: @top.clk.

Description

The above_w watcher samples an expression and opens an interval when the value exceeds a threshold. It closes the interval when the value drops below the threshold. If tolerance is provided, the interval is closed when the value drops below threshold - tolerance.

The above_w watcher has interval data of type above_<sample type>_w_data. The interval data struct contains the following fields, which are filled automatically:

max_value: The highest sample_expression value during the interval. max_value_time: The simulation time at which sample_expression reached its highest value during the interval.

Example

In the following example, the watcher opens an interval when my_car.state.speed is more than 50 kph and closes it when the speed drops below 45 kph.

OSC code: above_w definition
    watcher above_50 is above_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: 50kph, tolerance: 5kph)

See complete example.

The recorded minimum value and the minimum value time are shown in the following example:

OSC code: watcher operators
extend top.main:
    watcher above_50(my_interval_data) is above_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: 50kph, tolerance: 5kph)

struct my_interval_data inherits above_w_speed_data:
    record(max, expression: max_value, unit: kph)
    record(max_time, expression: max_value_time, unit: second)

See complete example.

To change the threshold dynamically:

OSC code: dynamically changing the threshold
    var speed_threshold := 50kph

    watcher my_watcher is above_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: speed_threshold, tolerance: 5kph)

    on @drive3.start:
        speed_threshold = 100kph

See complete example.

below_w operator

The below_w watcher creates an interval representing the timeframe when a sampled value drops below a threshold.

Syntax

OSC2 code: below_w operator syntax
watcher <watcher name> is below_w(sample_type: <numeric type>, sample_expression: <expression>, threshold: <expression>[, tolerance: <expression>] [, sample_event: <qualified event>])

Parameters

  • sample_type: The type of the value to sample must be numeric. It can be of type 'int' or any physical type.
  • sample_expression: The value to sample; must be of type sample_type.
  • threshold: Interval opening threshold; must be of type sample_type. The threshold expression is re-evaluated on every clock cycle.
  • tolerance (optional): Interval closing tolerance; must be of type sample_type.
  • sample_event (optional): A qualified event on which to evaluate sample_expression. Default: @top.clk.

Description

The below_w watcher samples an expression and opens an interval when the value is less than the threshold. It closes the interval when the value exceeds the threshold. If tolerance is provided, then the interval is closed when the value exceeds threshold + tolerance.

The below_w watcher has interval data of type below_<sample_type>_w_data. The interval data struct contains the following fields, which are filled automatically:

min_value: The lowest sample_expression value during the interval. min_value_time: The simulation time at which sample_expression reached its lowest value during the interval.

Example

In the following example, the watcher opens an interval when my_car.state.speed is less than 50 kph and closes it when the speed exceeds 55 kph.

OSC code: below_w definition
    watcher below_50 is below_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: 50kph, tolerance: 5kph)

See complete example.

The recorded maximum value and the maximum value time are shown in the following example:

OSC code: watcher operators
extend top.main:
    watcher below_50 is below_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: 50kph, tolerance: 5kph)

struct my_interval_data inherits below_w_speed_data:
    record(min, expression: min_value, unit: kph)
    record(min_time, expression: min_value_time, unit: second)

See complete example.

To change the threshold dynamically:

OSC code: changing the threshold dynamically
    var speed_threshold := 50kph

    watcher my_watcher is below_w(sample_type: speed, sample_expression: my_car.state.speed, threshold: speed_threshold, tolerance: 5kph)

    on @drive3.start:
        speed_threshold = 0kph

See complete example.

upon_w operator

The upon_w operator creates zero-time intervals when the ev event is emitted.

OSC code: upon_w definition
watcher modifier upon_w:

    event ev

The watcher operators shows how to use the watcher operators.

See complete example.

Other operators and special characters

Foretify supports the use of the following operators in expressions.

Operator type Operators Description Example
Ternary ? : Returns the value of the second operand if the first operand is true, else returns the value of the third operand. x ? y : z
Parentheses () Returns the value of the expression in parentheses. (x -y)
Event trigger @<event-name> Specify the pathname of an event. @main_car.arrived
Line continuation \ Continue an entity over multiple lines.

Path operator and name resolution

Foretify defines a global scope that includes a number of predefined actors, including the actor top. top and any of its fields are members of the global scope.

When an OSC2 program is executed, the top-level actor's main scenario, top.main, is invoked. Extending this top-level scenario to invoke a scenario creates an instance of that scenario in the invoking scenario, as well as instances of all the scenario’s members. The hierarchy of the tree expands level by level as each scenario instance calls other scenarios or scenario modifiers.

Objects in the program tree such as scenario instances or fields within scenario instances can be accessed by path expressions. A path expression comprises steps connected by dots. Each step can be an identifier, a method call or an array element reference. For example, sut.car.drive() is a path expression referencing the drive() action in the car field of the global actor sut (the SUT). In some path expressions, the head (the first step in the path) is a special identifier such as outer or it.

Scenario invocations can have user-defined labels. Automatic labels (implicit labels) are computed for all other invocations. Using labels, the behavior or attributes of a specific scenario or scenario invocation can be controlled from outside the scenario. See Automatic label computation for how automatic labels are computed.

Note: It is recommended to represent scenario libraries as new global actors. For example, you should define simulator-specific scenarios for the my_sim simulator inside a global actor type my_sim. Then, create a field of type my_sim in the actor top. To facilitate readability, it is recommended to use the global actor type’s name as the field name. For example, if my_sim is a global actor type, it is recommended to declare my_sim as follows:

OSC2 code: represent scenario libraries as global actors
extend top:
    my_sim: my_sim

See complete example.

Scenario name resolution

Scenarios reside in the namespace of their actor, so there can be a car.turn() and a person.turn(). When scenario car1.slow_down() invokes a lower-level scenario turn(), these rules determine which actor's scenario turn() is called:

  • If you specify the actor instance for turn() explicitly, for example car2.turn(), the actor is car2.
  • Else if car1’s actor type (car) has a scenario turn(), then car1 is used.
  • Else this is an error.

Notes:

  • Invoking the generic form of the scenario (actor.scenario) is not allowed. A scenario invocation must be associated with an actor instance.
  • Scenario modifiers are searched by the same rules as scenarios.

Name resolution for other objects

Here are the scoping rules when referring in a path expression to an object other than a scenario, such as a field, method or event x inside a struct, actor, or scenario y.

If you refer to x via a path (car1.x) then that path is used.

Else if you refer to x using the implicit variable it.x, the path of the object referred to by it is used. (it is available only in certain contexts.)

Else if y has an object x, its path is used.

Else if y is a scenario of actor z, and z has an object x, that object’s path is used.

Else if x is in the global scope, x is used.

Else this is an error.

Example

OSC2 code: path resolution using the implicit variable
actor z:
    x: int

scenario z.y:
    keep(x == 2) # resolves to z.x

See complete example.

When resolving a single-step path where a field has the same name as an enum constant, if there is an immediate enum type context, the step is interpreted as an enum member of the context type. For example:

OSC2 code: resolving field and constant with same name
enum color: [red, green, blue]

extend top.main:
  red: color with:   # field has same name as an enum constant 'red'
    keep (it == blue)

  c: color with:
    keep (it == red) # 'c' is type color, so 'red' is resolved
                     # as the enum constant 'red', not the value of the field 'red'

  def foo(c: color) is empty

  do call foo(red)   # foo's parameter 'c' is type 'color', so 'red' is resolved
                     # as the enum constant 'red'

See complete example.

The immediate context is derived from:

  1. The other side of a boolean expression
  2. A scenario argument's formal type
  3. A method argument's formal type

If you want to refer explicitly to the field, you can specify a self, it, or actor-name prefix. For example:

OSC2 code: refer explicitly to a field
enum color: [red, green, blue]

extend top.main:
  red: color with:
    keep (it == blue)

  c: color with:
    keep (it == self.red) # right-hand side is resolved to the field 'red'

See complete example.

Note that this ambiguity appears only in single-step paths. The following code is not ambiguous: 

OSC2 code: unambiguous field reference
enum color: [red, green, blue]

struct color_container:
  the_color: color

extend top.main:
  red: color_container

  c: color with:
    keep (it == self.red.the_color) # the first step is resolved to the field 'red'

See complete example.

Automatic Label Computation

Scenario invocations and modifier applications that have no user-defined labels get automatically generated labels (called implicit labels) according to the following algorithm:

Implicit label access syntax is label( <label-path> ), where <label-path> is a concatenation of identifiers using a period (.) as separator.

  • If the scenario invocation has a path, the first identifier in the path is used,
  • Else the scenario name is used.
  • If the resulting label is not unique, the suffix ( <num> ) is added, where <num> is the next sequential number. The numbering starts from 2.

Below is a list of rules that apply when accessing implicit labels. Refer to Examples 1 and 2 below the list for the full context of the examples.

  • label() is limited to accessing invocations within the current scenario. To access objects declared in the body of a nested scenario, use the following syntax:

    OSC2 code: automatic label syntax
    <current-scenario-label>.<path-to-object-in-invoked-scenario>
    

    Examples 1 and 2 (below) have the same three levels of scenario hierarchy, so references to objects in the lowest level have the following syntax:

    OSC2 code: label syntax for Ex 1 and Ex 2
    <cut-in-and-stop-label>.<cut-in-label>.<path-to-object>
    
    OSC2 code: actual labels for Example 1 and Example 2
    cut_in_and_stop.cut_in.label(change_lane.car1)                         # Ex 1
    label(cut_in_and_stop).label(serial.cut_in).label(serial.parallel.sut) # Ex 2
    

  • Implicit labels use the first identifier in any invocation, so the implicit label for car1.drive() is car1 and the implicit label for sut.car.drive() is sut. In the following example, the implicit label label(change_lane.car1) is a path to car1.drive().

    OSC2 code: implicit label example
    cut_in_and_stop.cut_in.label(change_lane.car1)
    
  • Do not use label() to wrap user-defined labels unless those labels are part of the path to an object. In the following example, the user-defined label change_lane is part of the path to car1.drive(), so it is wrapped in label(). However, wrapping either cut_in_and_stop or cut_in in label() causes a compilation error.

    OSC2 code: label() and user-defined labels
    cut_in_and_stop.cut_in.label(change_lane.car1)
    

Example 1

This example shows a test with three levels of hierarchy. User-defined labels are provided for the invocations of user-defined scenarios and for the first phase of the cut_in() scenario.

OSC2 code: user-defined labels (example)
import "$FTX_BASIC/exe_platforms/sumo_ssp/config/sumo_config.osc"

extend test_config:
    set map = "$FTX_PACKAGES/maps/hooder.xodr"

extend top.main:

    in cut_in_and_stop.label(start_behind_sut.sut) with: speed([30..70]kph)
    in cut_in_and_stop.cut_in with: keep(it.duration in [5..7]second)
    in cut_in_and_stop.cut_in.label(change_lane.car1) with:
        speed([50..70]kph)
        lane(1)

    do cut_in_and_stop: sut.cut_in_and_stop() # cut_in_and_stop

scenario sut.cut_in_and_stop:
    car1: vehicle
    side: av_side

    do serial:                         # cut_in_and_stop
        start_behind_sut: parallel(overlap:equal): # cut_in_and_stop.start_behind_sut
           sut.car.drive()             # cut_in_and_stop.label(start_behind_sut.sut)
           car1.drive()                # cut_in_and_stop.label(start_behind_sut.car1)

        cut_in: cut_in(car1: car1, side: side) # cut_in_and_stop.cut_in

scenario sut.cut_in:
    car1: vehicle
    side: av_side

    do serial:                        # cut_in_and_stop.cut_in
        change_lane: parallel(overlap:equal): # cut_in_and_stop.cut_in.change_lane
            sut.car.drive()           # cut_in_and_stop.cut_in.label(change_lane.sut)
            car1.drive()              # cut_in_and_stop.cut_in.label(change_lane.car1)

Example 2

This shows the same test as in Example 1, but with no user-defined labels.

OSC2 code: automatic labels (example)
import "$FTX_BASIC/exe_platforms/sumo_ssp/config/sumo_config.osc"

extend test_config:
    set map = "$FTX_PACKAGES/maps/hooder.xodr"

extend top.main:

  in label(sut).label(serial.parallel.sut) with: speed([30..70]kph)
  in label(sut).label(serial.cut_in).label(serial) with:
    keep(it.duration in [5..7]second)
  in label(sut).label(serial.cut_in).label(serial.parallel.car1) with:
    speed([50..70]kph)
    lane(1)

  do sut.cut_in_and_stop()         # label(sut)

scenario sut.cut_in_and_stop:
  car1: vehicle
  side: av_side

  do serial:                   # label(sut).label(serial)
    parallel(overlap:equal):   # label(sut).label(serial.parallel)
      sut.car.drive()          # label(sut).label(serial.parallel.sut)
      car1.drive()             # label(sut).label(serial.parallel.car1)

    cut_in(car1: car1, side: side) # label(sut).label(serial.cut_in)

scenario sut.cut_in:
  car1: vehicle
  side: av_side

  # label(sut).label(serial.cut_in).label(serial)
  do serial:
    # label(sut).label(serial.cut_in).label(serial.parallel)
    parallel(overlap:equal):
      # label(sut).label(serial.cut_in).label(serial.parallel.sut)
      sut.car.drive()
      # label(sut).label(serial.cut_in).label(serial.parallel.car1)
      car1.drive()

Example 3

The following example shows a scenario with two car1.drive() invocations without explicitly defined labels. This example also shows how to reference scenario modifiers within a drive() invocation.

OSC2 code: referencing scenario modifiers
scenario sut.scen1:
    car1: vehicle
    do serial:                             # label(serial)
        car1.drive() with: keep_lane()     # label(serial.car1)
        car1.drive() with:                 # label(serial.car1(2))
            lane(1)                        # label(serial.car1(2).lane)
            speed(speed: [30..120]kph)     # label(serial.car1(2).speed)
    with:
        keep(label(serial.car1(2).speed).speed == 45kph)

See complete example.