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
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.
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.
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.
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.
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:
-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.
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.
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.
rise(<expression>)
<expression>- (Required) A Boolean expression.
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.
fall(<expression>)
<expression>- (Required) A Boolean expression.
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:
-
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.
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 | |
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:
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:
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:
[point1, point2]
The [n..n] notation is not allowed for lists; it is reserved for ranges.
convoy: list of vehicle
distances: list of length
See complete example.
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.
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:
do serial:
log_info("There are $(cars.size()) cars.")
See complete example.
You can concatenate multiple strings with the + character. For example:
"a string" + " with concatenation"
You can continue strings onto multiple lines with the + character and \ character:
"a string" + \
" with continuation"
Within a string, the backslash is an escape character, for example:
"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).
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)")
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:
watcher modifier passive_w
not_w operator
The not_w operator creates intervals when an input watcher doesn't have an active interval.
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:
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.
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.
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.
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.
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.
watcher <watcher name> is above_w(sample_type: <numeric type>, sample_expression: <expression>, threshold: <expression>[, tolerance: <expression>] [, sample_event: <qualified event>])
- 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.
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.
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.
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:
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:
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.
watcher <watcher name> is below_w(sample_type: <numeric type>, sample_expression: <expression>, threshold: <expression>[, tolerance: <expression>] [, sample_event: <qualified event>])
- 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.
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.
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.
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:
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:
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.
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:
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.
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:
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:
- The other side of a boolean expression
- A scenario argument's formal type
- 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:
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:
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 2cut_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 examplecut_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 labelscut_in_and_stop.cut_in.label(change_lane.car1)
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.
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)
This shows the same test as in Example 1, but with no user-defined labels.
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()
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.
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.