Skip to content

Defining constraints

The phase of the test process that creates data structures and assigns values to fields is called generation. This process follows the specifications you provide in type declarations, in field declarations, and in keep statements. Those specifications are called constraints.

The degree to which generated values for a field are random depends on the constraints that are specified. A field’s values can be either:

  • Fully random — without explicit constraints, for example:

    OSC2 code: fully random example
    extend top.main:
    tolerance: int
    

    See complete example.

  • Fully directed — with constraints that specify a single value, for example:

    OSC2 code: fully directed example
        keep(my_speed == 50kph) # my_speed is set to 50 kph
    

    See complete example.

  • Constrained random — with constraints that specify a range of possible values, for example:

    OSC2 code: constrained random example
        keep(my_speed in [30..80]kph) # my_speed is restricted to a range
    

    See complete example.

This topic describes various types of constraints—from simple to more complex—and gives examples. For a precise definition of constraint syntax, see Constraints.

Simple Boolean constraints

Simple Boolean constraints are constraints applied to a single field. You can add keep() constraints to fields inside structs, scenarios, or actors, for example:

OSC2 code: simple Boolean constraint
    my_speed: speed with:
        keep(it in [30..80]kph)

See complete example.

You can use the in constraint operator to constrain a physical or numeric parameter to a range of values. In the example below, the valid value for the field dist is any value between 2m and 4m, inclusive. The valid values for the field i are -1, 0, and 1.

You can also use the in constraint operator to constrain an enumerated type to a list of values. In the example below, the valid values for the field ms are assertive and timid.

OSC2 code: simple Boolean constraint with 'in'
enum my_driving_style: [aggressive, assertive, normal, timid]

struct misc:
    dist: length with:
        keep(it in [2..4]m)
    i: int with:
        keep(it in [-1..1])

    ms: my_driving_style with:
        keep(it in [assertive, timid])

See complete example.

Compound Boolean constraints

Compound Boolean constraints define relationships between two or more fields. For example, if an object has three fields:

  • legal_speed: the speed allowed by law on that road
  • lawful_driver: a driver who follows the laws
  • current speed: the current speed of the vehicle driven by lawful driver

You can define the current speed in relation to the other fields, so that a lawful driver implies the current speed is less than or equal to the speed allowed by law:

Examples

OSC2 code: compound Boolean constraint example 1
    keep(lawful_driver => (current_speed <= legal_speed))

See complete example.

OSC2 code: compound Boolean constraint example 2
    # both constraint expressions must evaluate to true
    keep(x <= 3 and x > y)

    # at least one constraint expression must evaluate to true
    keep(x <= 3 or x > y)

    # if the first expression evaluates to true, the second one must
    # also evaluate to true
    keep((x <= 3) => (x > y))

See complete example.

List constraints

You can use the list method .size() and list indexing (list[index]) in list constraint expressions. In the following example, the constraints specify that the list size is between 2 and 10, inclusive. The third constraint in this example specifies that the first car in the list must be the object first_car.

OSC2 code: list constraint
actor my_car_convoy:
    first_car: vehicle
    cars: list of vehicle

    # the list will have between 2 and 10 items
    # the first item is first_car
    keep(soft cars.size() <= 10)
    keep(soft cars.size() >=  2)
    keep(cars[0] == first_car) # list indexing

See complete example.

Relative strength of constraints

You can define the strength of keep constraints:

A hard constraint must be obeyed when the item is generated. If a hard constraint conflicts with another hard constraint, a contradiction error is issued. For example, if the following constraint is applied and the generator cannot assign the value 25 to the field current_speed, an error is issued:

OSC2 code: hard constraint
    current_speed: speed with:
        keep(it == 25kph)

See complete example.

A soft constraint must be obeyed unless it contradicts a hard constraint, or a later-specified soft constraint. Soft constraints are ignored without issuing an error. For example, if the following constraint is applied and the generator assigns the value green to the field color because of a later constraint, no error is issued:

OSC2 code: soft constraint example 1
    color: car_color with:
        keep(soft it!= green)

See complete example.

Note that if a later constraint overrides a portion of a soft compound constraint, the entire compound constraint is ignored. For example, the following example results in a = 10 and b is unconstrained.

OSC2 code: soft with compound constraints
    keep(soft a == 1 and b == 0)
    keep(soft a == 10)

See complete example.

You can apply soft constraints to parameters when invoking a scenario, for example:

OSC2 code: using soft when passing parameters
scenario top.foo:
  a: int

extend top.main:
  do foo(a: soft 123)

See complete example.

Soft constraints are ignored during monitoring. For example, multi_match() (a composition operator that monitors a scenario execution) succeeds even if soft constraints are ignored. Also, dynamic processes, such as re-adjustments caused by unpredictable SUT behavior, may cause soft constraints to be ignored.

A default constraint must be obeyed unless it is overridden by another default constraint or by a simple (not compound) hard constraint that specifies an explicit value or range of values. The following example shows that:

  • A hard compound constraint causes a contradiction with a default constraint, and a soft compound constraint is ignored.
  • A hard simple constraint that attempts to override the default without specifying an explicit value or range of values causes a contradiction.
  • Hard simple constraints that specify an explicit value or range of values successfully override the default.
  • Default constraints analysis depends on their position in the equation variables. A field with a default constraint can only be overridden if it is on the left side.
OSC2 code: default constraints
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
scenario sut.scenario1:
    b: uint
    c: uint

    keep(default b == 10)
    keep(default c == 20)

    keep(b == 1 and c == 2)      # contradiction
    keep(soft b == 1 and c == 2) # b and c get default values

    keep(b > 20) # contradiction
    keep(b > 5)  # b gets 10

    keep(b in [12..18]) # b gets 16
    keep(b == 7) # b gets 7
    keep(b == c) # b and c get 20

    do log_info("### b: $(b); c: $(c)")
  • Line 8: A hard compound constraint that contradicts a default constraint results in a contradiction.
  • Line 9: A soft compound constraint is ignored.
  • Line 11: A hard simple constraint without an explicit value or range that contradicts a default constraint results in a contradiction.
  • Line 12: A hard simple constraint without an explicit value or range that does not contradict a default constraint is applied.
  • Line 14: A hard simple constraint with an explicit range is applied, even if it contradicts the default.
  • Line 15 - 16: A hard simple constraint with an explicit value is applied, even if it contradicts the default.

See complete example.

Note

You can apply default constraints only to fields within scenarios, not within actors or structs.

Soft constraints and default constraints

Default constraints seem like soft constraints. However, default constraints may cause contradictions, whereas soft constraints are ignored if they contradict hard constraints or a later-applied soft constraint.

In the example below, a default constraint is applied to x: keep(default it == 0). If you extend vehicle.foo() for a new test and add the constraint keep(y==0) without changing the value of x, a contradiction occurs. The value of x remains 0 because there is no direct overriding constraint on x.

OSC2 code: default constraint
scenario vehicle.foo:
    x: int with:
        keep(default it == 0)
    y: int with:
        keep(it!= x)

See complete example.

In the example below, the default constraint is replaced by a soft constraint. Here, adding the constraint keep(y==0) does not cause a contradiction because the value of x is changed to some non-zero value.

OSC2 code: soft constraint example 2
scenario vehicle.foo:
    x: int with:
        keep(soft it == 0)
    y: int with:
        keep(it!= x)

See complete example.

Remove default constraints

Many times, you might want to ignore the default value when some conditions are met. For example, if you want to override this constraint:

OSC2 code: default constraint
max_speed: speed with:
    keep(default it == 300kph)

With the following constraints:

OSC2 code: compound constraints
keep(odd == town => max_speed == 60kph)
keep(odd == highway => max_speed == 120kph)

The constraints cause a contradiction since the added constraints do not override the default.

This problem becomes even more complicated if you want to be able to use default values on multiple variables. In this case, the actual constraints in the implementation of relevant modifiers will rarely override the default. In this case, you can use remove_default().

remove_default()

Purpose

Remove a default constraint

Category

Struct, actor, or scenario member

Note

remove_default() is allowed anywhere that a constraint is allowed.

Syntax

remove_default( <path-exp> )

Syntax parameters

<path-exp>
Resolves to a field reference.

Description

If a default constraint causes a contradiction, you can remove it with this construct.

Example

OSC2 code: remove_default() constraint
scenario vehicle.foo:
    x: int with:
        keep(default it == 0)
    y: int with:
        keep(it!= x)
    do drive(duration: 5s)

extend vehicle.foo:
    remove_default(x)
    keep(y == 0)

See complete example.

Soft constraints and distribution methods

The following syntax lets you use distribution methods to constrain generated values:

OSC2 code: distribution method syntax
keep(soft <path-exp> == <distribution-method>())

<distribution-method> is one of the following:

  • weighted()
  • A predefined, general-purpose method such as random.normal() or random.uniform()
  • A user-defined external method

Note

All distribution methods require the use of soft.

weighted()

The weighted() method is a constraint-specific construct that returns a requested value or range of values for a generateable item based on a specified weight. Unlike other distribution methods, weighted() knows the valid ranges of the generateable item. You can use weighted() for any type of generatable item.

The syntax for weighted() is as follows:

OSC2 code: weighted distribution method syntax
keep(soft <gen-item> == weighted(<weight>: <request>, ...))
<gen_item>
Is a generateable item.
<weight>
Is the relative probability that the generateable item receives the requested value. The specified weights for a requested value do not need to add up to 100.
<request>

Is one of the following:

  • <exp> specifies the value to be returned.
  • [<value1>..<value2] – returns a value drawn uniformly from that range.

Example

OSC2 code: weighted distribution method example
extend top.set_values:
    side: av_side # assign left, right in a 20 : 80 ratio
    keep(soft side == weighted(20: left, 80: right))

    top_speed: speed # assign a value from one of three buckets in a 10:30:10 ratio
    keep(soft top_speed == weighted(10: [20..30]kph,
                                    30: [30..70]kph,
                                    10: [70..120]kph))

    dist: length # assign 10m and the return value of foo() in a 50 : 50 ratio
    keep(soft dist == weighted(50: 10m, 50: foo()))

    num_of_wheels: int # assign 4 or 10 wheels in a 50 : 50 ratio
    keep(soft num_of_wheels == weighted(50: 4, 50: 10))

See complete example.

Predefined random distribution methods

Foretellix libraries contain predefined general-purpose random distribution methods:

  • random.normal() returns a floating point number selected from a normal distribution.

    OSC2 code: random.normal distribution method syntax
    keep(soft <gen-item> == random.normal(<mean>,
                                      <standard_deviation>) [* 1<unit>])
    

  • random.truncated_normal() returns a floating point number selected from a normal distribution truncated between two values.

    OSC2 code: random.truncated_normal distribution method syntax
    keep(soft <gen-item> == random.truncated_normal(<from_value>,
                                          <to_value>,
                                          <mean>,
                                          <standard_deviation>) [* 1<unit>])
    

    Note

    The current implementation of random.truncated_random() generates random numbers in a loop until the value falls within [<from_value>..<to_value>]. If the chosen <mean> and <standard_deviation> parameters result in a distribution where values in the range [<from_value>..<to_value>] are unlikely, performance will be degraded.

  • random.uniform() returns a floating point number selected from a uniform distribution

    OSC2 code: random.uniform distribution method syntax
    keep(soft <gen-item> == random.uniform(<from_value>,
                                           <to_value>) [* 1<unit>])
    

<gen_item>
Is a generatable item.
<from-value>, <to-value>
Specify the beginning and end of the range of values to select from.
<mean>, <standard-deviation>
Specify the mean and standard deviation parameters of a normal distribution formula.
<unit>
Is required for physical types such as speed, length, angle and so on.
OSC2 code: random distribution method examples
extend top.main:
    top_speed: speed
    keep(soft top_speed == random.normal(55.5, 7) * 1kph)

    top_speed_truncated: speed
    keep(soft top_speed_truncated == random.truncated_normal(10, 120, 55.5, 7) * 1kph)


    dist: length
    keep(soft dist == random.uniform(20, 50.5) * 1m)

    end_speed: speed
    keep(soft end_speed == weighted(
        30: [20..30]kph,
        70: random.truncated_normal(10, 120, 55.5, 7) * 1kph))

See complete example.

Notes

  • The first example returns a number between 10kph to 120kph, with a mean of 55.5kph and a standard deviation of 7kph.
  • The second example returns a number using a uniform (flat) distribution in the requested range.
  • The third example shows that you can call these random functions inside weighted().

User-defined distribution methods

You can also define any other distribution function, for example:

OSC2 code: user-defined distribution method example
extend random:
    def distribute_int(i: int, j: int) -> int is empty

scenario sut.my_scenario:
    num_of_cars: int
    keep(soft num_of_cars == random.distribute_int(1,5))

See complete example.

Notes

  • User-defined distribution functions, unlike weighted(), do not know the valid range of values for the generateable item. Thus, if they return a value that is outside the range, the constraint is ignored, as with all soft constraints.
  • You can call a user-defined distribution function inside weighted().