Composing Scenarios
Scenic provides facilities for defining multiple scenarios in a single program and composing them in various ways. This enables writing a library of scenarios which can be repeatedly used as building blocks to construct more complex scenarios.
Modular Scenarios
The scenario
statement defines a named, reusable scenario, optionally with tunable parameters: what we call a modular scenario.
For example, here is a scenario which creates a parked car on the shoulder of the ego
’s current lane (assuming there is one), using some APIs from the Driving Domain:
scenario ParkedCar(gap=0.25):
precondition: ego.laneGroup._shoulder != None
setup:
spot = new OrientedPoint on visible ego.laneGroup.curb
parkedCar = new Car left of spot by gap
The setup
block contains Scenic code which executes when the scenario is instantiated, and which can define classes, create objects, declare requirements, etc. as in any ordinary Scenic scenario.
Additionally, we can define preconditions and invariants, which operate in the same way as for dynamic behaviors.
Having now defined the ParkedCar
scenario, we can use it in a more complex scenario, potentially multiple times:
scenario Main():
setup:
ego = new Car
compose:
do ParkedCar(), ParkedCar(0.5)
Here our Main
scenario itself only creates the ego car; then its compose
block orchestrates how to run other modular scenarios.
In this case, we invoke two copies of the ParkedCar
scenario in parallel, specifying in one case that the gap between the parked car and the curb should be 0.5 m instead of the default 0.25.
So the scenario will involve three cars in total, and as usual Scenic will automatically ensure that they are all on the road and do not intersect.
Parallel and Sequential Composition
The scenario above is an example of parallel composition, where we use the do
statement to run two scenarios at the same time.
We can also use sequential composition, where one scenario begins after another ends.
This is done the same way as in behaviors: in fact, the compose
block of a scenario is executed in the same way as a monitor, and allows all the same control-flow constructs.
For example, we could write a compose
block as follows:
1while True:
2 do ParkedCar(gap=0.25) for 30 seconds
3 do ParkedCar(gap=0.5) for 30 seconds
Here, a new parked car is created every 30 seconds, [1] with the distance to the curb alternating between 0.25 and 0.5 m.
Note that without the for 30 seconds
qualifier, we would never get past line 2, since the ParkedCar
scenario does not define any termination conditions using terminate when
(or terminate
in a compose
block) and so runs forever by default.
If instead we want to create a new car only when the ego
has passed the current one, we can use a do-until
statement:
while True:
subScenario = ParkedCar(gap=0.25)
do subScenario until (distance past subScenario.parkedCar) > 10
Note how we can refer to the parkedCar
variable created in the ParkedCar
scenario as a property of the scenario.
Combined with the ability to pass objects as parameters of scenarios, this is convenient for reusing objects across scenarios.
Interrupts, Overriding, and Initial Scenarios
The try-interrupt
statement used in behaviors can also be used in compose
blocks to switch between scenarios.
For example, suppose we already have a scenario where the ego
is following a leadCar
, and want to elaborate it by adding a parked car which suddenly pulls in front of the lead car.
We could write a compose
block as follows:
1following = FollowingScenario()
2try:
3 do following
4interrupt when (distance to following.leadCar) < 10:
5 do ParkedCarPullingAheadOf(following.leadCar)
If the ParkedCarPullingAheadOf
scenario is defined to end shortly after the parked car finishes entering the lane, the interrupt handler will complete and Scenic will resume executing FollowingScenario
on line 3 (unless the ego
is still within 10 m of the lead car).
Suppose that we want the lead car to behave differently while the parked car scenario is running; for example, perhaps the behavior for the lead car defined in FollowingScenario
does not handle a parked car suddenly pulling in.
To enable changing the behavior
or other properties of an object in a sub-scenario, Scenic provides the override
statement, which we can use as follows:
scenario ParkedCarPullingAheadOf(target):
setup:
override target with behavior FollowLaneAvoidingCollisions
parkedCar = new Car left of ...
Here we override the behavior
property of target
for the duration of the scenario, reverting it back to its original value (and thereby continuing to execute the old behavior) when the scenario terminates.
The override object specifier, ...
statement takes a comma-separated list of specifiers like an instance creation, and can specify any properties of the object except for dynamic properties like position
or speed
which can only be indirectly controlled by taking actions.
In order to allow writing scenarios which can both stand on their own and be invoked during another scenario, Scenic provides a special conditional statement testing whether we are inside the initial scenario, i.e., the very first scenario to run. For instance:
scenario TwoLanePedestrianScenario():
setup:
if initial scenario: # create ego on random 2-lane road
roads = filter(lambda r: len(r.lanes) == 2, network.roads)
road = Uniform(*roads) # pick uniformly from list
ego = new Car on road
else: # use existing ego car; require it is on a 2-lane road
require len(ego.road.lanes) == 2
road = ego.road
new Pedestrian on visible road.sidewalkRegion, with behavior ...
Random Selection of Scenarios
For very general scenarios, like “driving through a city, encountering typical human traffic”, we may want a variety of different events and interactions to be possible. We saw in the Dynamic Scenarios tutorial how we can write behaviors for individual agents which choose randomly between possible actions; Scenic allows us to do the same with entire scenarios. Most simply, since scenarios are first-class objects, we can write functions which operate on them, perhaps choosing a scenario from a list of options based on some complex criterion:
chosenScenario = pickNextScenario(ego.position, ...)
do chosenScenario
However, some scenarios may only make sense in certain contexts; for example, a red light runner scenario can take place only at an intersection.
To facilitate modeling such situations, Scenic provides variants of the do
statement which randomly choose scenarios to run amongst only those whose preconditions are satisfied:
1do choose RedLightRunner, Jaywalker, ParkedCar(gap=0.5)
2do choose {RedLightRunner: 2, Jaywalker: 1, ParkedCar(gap=0.5): 1}
3do shuffle RedLightRunner, Jaywalker, ParkedCar
Here, line 1 checks the preconditions of the three given scenarios, then executes one (and only one) of the enabled scenarios. If for example the current road has no shoulder, then ParkedCar
will be disabled and we will have a 50/50 chance of executing either RedLightRunner
or Jaywalker
(assuming their preconditions are satisfied).
If none of the three scenarios are enabled, Scenic will reject the simulation.
Line 2 shows a non-uniform variant, where RedLightRunner
is twice as likely to be chosen as each of the other scenarios (so if only ParkedCar
is disabled, we will pick RedLightRunner
with probability 2/3; if none are disabled, 2/4).
Finally, line 3 is a shuffled variant, where all three scenarios will be executed, but in random order. [2]
Footnotes