Welcome to Scenic’s documentation!

Scenic is a domain-specific probabilistic programming language for modeling the environments of cyber-physical systems like robots and autonomous cars. A Scenic program defines a distribution over scenes, configurations of physical objects and agents; sampling from this distribution yields concrete scenes which can be simulated to produce training or testing data. Scenic can also define (probabilistic) policies for dynamic agents, allowing modeling scenarios where agents take actions over time in response to the state of the world.

Scenic was designed and implemented by Daniel J. Fremont, Edward Kim, Tommaso Dreossi, Shromona Ghosh, Xiangyu Yue, Alberto L. Sangiovanni-Vincentelli, and Sanjit A. Seshia, with contributions from many others. For a description of the language and some of its applications, see our journal paper, which extends our PLDI 2019 paper on Scenic 1.x. Our publications page lists additional papers using Scenic.

Note

The syntax of Scenic 2.x is not completely backwards-compatible with 1.x, which was used in our papers prior to late 2020. See What’s New in Scenic for a list of syntax changes and new features. If your existing code no longer works, install the latest 1.x release from GitHub.

If you have any problems using Scenic, please submit an issue to our GitHub repository or contact Daniel at dfremont@ucsc.edu.

Table of Contents

Getting Started with Scenic

Installation

Scenic requires Python 3.7 or newer. You can install Scenic from PyPI by simply running:

$ python -m pip install scenic

Alternatively, if you want to run some of our example scenarios, modify Scenic, or make use of features that have not yet been released on PyPI, you can download or clone the Scenic repository. Activate the virtual environment in which you would like to install Scenic, go into the root folder of the Scenic repository, and then run:

$ python -m pip install -e .

Either installation method will install all of the dependencies which are required to run Scenic. If you will be developing Scenic, you will want to use a variant of this command: see Developing Scenic.

Note

If you are using Windows, or encounter any errors during installation or trying the examples below, please see our Notes on Installing Scenic for suggestions.

Trying Some Examples

The Scenic repository contains many example scenarios, found in the examples directory. They are organized by the simulator they are written for, e.g. GTA or Webots; there are also cross-platform scenarios written for Scenic’s abstract application domains, e.g. the driving domain. Each simulator has a specialized Scenic interface which requires additional setup (see Supported Simulators); however, for convenience Scenic provides an easy way to visualize scenarios without running a simulator. Simply run scenic, giving a path to a Scenic file:

$ scenic examples/gta/badlyParkedCar2.scenic

This will compile the Scenic program and sample from it, displaying a schematic of the resulting scene. Since this is the badly-parked car example from our GTA case study, you should get something like this:

_images/badlyParkedCar2.png

Here the circled rectangle is the ego car; its view cone extends to the right, where we see another car parked rather poorly at the side of the road (the white lines are curbs). If you close the window, Scenic will sample another scene from the same scenario and display it. This will repeat until you kill the generator (Control-c in Linux; right-clicking on the Dock icon and selecting Quit on OS X).

Scenarios for the other simulators can be viewed in the same way. Here are a few for different simulators:

$ scenic examples/driving/pedestrian.scenic
$ scenic examples/webots/mars/narrowGoal.scenic
$ scenic examples/webots/road/crossing.scenic
_images/pedestrian.png _images/narrowGoal.png _images/crossing.png

The scenic command has options for setting the random seed, running dynamic simulations, printing debugging information, etc.: see Command-Line Options.

Learning More

Depending on what you’d like to do with Scenic, different parts of the documentation may be helpful:

  • If you want to start learning how to write Scenic programs, see the Scenic Tutorial.

  • If you want to learn how to write dynamic scenarios in Scenic, see Dynamic Scenarios.

  • If you want to use Scenic with a simulator, see Supported Simulators (which also describes how to interface Scenic to a new simulator, if the one you want isn’t listed).

  • If you want to control Scenic from Python rather than using the command-line tool (for example if you want to collect data from the generated scenarios), see Using Scenic Programmatically.

  • If you want to add a feature to the language or otherwise need to understand Scenic’s inner workings, see our pages on Developing Scenic and Scenic Internals.

Notes on Installing Scenic

This page describes common issues with installing Scenic and suggestions for fixing them.

All Platforms

Missing Python Version

If during installation you get an error saying that your machine does not have a compatible version, this means that you do not have Python 3.7 or later on your PATH. Install a newer version of Python, either directly from the Python website or using pyenv (e.g. running pyenv install 3.10.4). Then use that version of Python when creating a virtual environment before installing Scenic.

Note

If you are using Poetry to manage your virtual environments, and you install the new version of Python somewhere that is not on your PATH (so that running python --version doesn’t give you the correct version), you’ll need to run poetry env use /full/path/to/python before running poetry install.

“setup.py” not found

This error indicates that you are using too old a version of pip: you need at least version 21.3. Run python -m pip install --upgrade pip to upgrade.

Dependency Conflicts

If you install Scenic using pip, you might see an error message like the following:

ERROR: pip’s dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.

This error means that in order to install Scenic, pip had to break the dependency constraints of some package you had previously installed (the error message will indicate which one). So while Scenic will work correctly, something else may now be broken. This won’t happen if you install Scenic into a fresh virtual environment.

Cannot Find Scenic

If when running the scenic command you get a “command not found” error, or when trying to import the scenic module you get a ModuleNotFoundError, then Scenic has not been installed where your shell or Python respectively can find it. The most likely problem is that you installed Scenic for one copy of Python but are now using a different one: for example, if you installed Scenic in a Python virtual environment (which we highly recommend), you may have forgotten to activate that environment, and so are using your system Python instead. See the virtual environment tutorial for instructions.

Note

If you used Poetry to install Scenic, the poetry install command may have created a virtual environment for you. You can then run poetry shell to create a terminal inside the environment and run Scenic as usual.

Scene Schematics Don’t Appear

If no window appears when you ask Scenic to generate and display a scene (as in the example commands in Getting Started with Scenic), this means that Matplotlib has no interactive backend installed. On Linux, try installing the python3-tk package (e.g. sudo apt-get install python3-tk).

Missing SDL

If you get an error about SDL being missing, you may need to install it. On Linux (or Windows with WSL), install the libsdl2-dev package (e.g. sudo apt-get install libsdl2-dev); on macOS, if you use Homebrew you can run brew install sdl2. For other platforms, see the SDL website.

Using a Local Scenic Version with VerifAI

If you are using Scenic as part of the VerifAI toolkit, the VerifAI installation process will automatically install Scenic from PyPI. However, if you need to use your own fork of Scenic or some features which have not yet been released on PyPI, you will need to install Scenic manually in VerifAI’s virtual environment. The easiest way to do this is as follows:

  1. Install VerifAI in a virtual environment of your choice.

  2. Activate the virtual environment.

  3. Change directory to your clone of the Scenic repository.

  4. Run pip install -e .

You can test that this process has worked correctly by going back to the VerifAI repo and running the Scenic part of its test suite with pytest tests/test_scenic.py.

Note

Installing Scenic in this way bypasses dependency resolution for VerifAI. If your local version of Scenic requires different versions of some of VerifAI’s dependencies, you may get errors from pip about dependency conflicts. Such errors do not actually prevent Scenic from being installed; however you may get unexpected behavior from VerifAI at runtime. If you are developing forks of Scenic and VerifAI, a more stable approach would be to modify VerifAI’s pyproject.toml to point to your fork of Scenic instead of the scenic package on PyPI.

Windows

Using WSL

For greatest ease of installation, we recommend using the Windows Subsystem for Linux (WSL, a.k.a. “Bash on Windows”) on Windows 10 and newer. If you’d like to use Poetry, it can be installed on WSL in the same way as on Linux: see the instructions here.

Some WSL users have reported encountering the error no display name and no $DISPLAY environmental variable, but have had success applying the techniques outlined here. Note that after applying this fix the command poetry shell may not work and one may need to use source $(poetry env info --path)/bin/activate instead.

It is possible to run Scenic natively on Windows; however, in the past there have been issues with some of Scenic’s dependencies either not providing wheels for Windows or requiring manual installation of additional libraries.

Problems building Shapely

In the past, the shapely package did not install properly on Windows. If you encounter this issue, try installing it manually following the instructions here.

Scenic Tutorial

This tutorial motivates and illustrates the main features of Scenic, focusing on aspects of the language that make it particularly well-suited for describing geometric scenarios. Throughout, we use examples from our case study using Scenic to generate traffic scenes in GTA V to test and train autonomous cars [F22], [F19].

We’ll focus here on the spatial aspects of scenarios; for adding temporal dynamics to a scenario, see our page on Dynamic Scenarios.

Classes, Objects, and Geometry

To start, suppose we want scenes of one car viewed from another on the road. We can write this very concisely in Scenic:

1from scenic.simulators.gta.model import Car
2ego = Car
3Car

Line 1 imports the GTA world model, a Scenic library defining everything specific to our GTA interface. This includes the definition of the class Car, as well as information about the road geometry that we’ll see later. We’ll suppress this import statement in subsequent examples.

Line 2 then creates a Car and assigns it to the special variable ego specifying the ego object. This is the reference point for the scenario: our simulator interfaces typically use it as the viewpoint for rendering images, and many of Scenic’s geometric operators use ego by default when a position is left implicit (we’ll see an example momentarily).

Finally, line 3 creates a second Car. Compiling this scenario with Scenic, sampling a scene from it, and importing the scene into GTA V yields an image like this:

Simple car scenario image.

A scene sampled from the simple car scenario, rendered in GTA V.

Note that both the ego car (where the camera is located) and the second car are both located on the road and facing along it, despite the fact that the code above does not specify the position or any other properties of the two cars. This is because in Scenic, any unspecified properties take on the default values inherited from the object’s class. Slightly simplified, the definition of the class Car begins:

1class Car:
2    position: Point on road
3    heading: roadDirection at self.position
4    width: self.model.width
5    height: self.model.height
6    model: CarModel.defaultModel()      # a distribution over several car models

Here road is a region, one of Scenic’s primitive types, defined in the gta model to specify which points in the workspace are on a road. Similarly, roadDirection is a vector field specifying the nominal traffic direction at such points. The operator F at X simply gets the direction of the field F at point X, so line 3 sets a Car’s default heading to be the road direction at its position. The default position, in turn, is a Point on road (we will explain this syntax shortly), which means a uniformly random point on the road. Thus, in our simple scenario above both cars will be placed on the road facing a reasonable direction, without our having to specify this explicitly.

We can of course override the class-provided defaults and define the position of an object more specifically. For example,

Car offset by Range(-10, 10) @ Range(20, 40)

creates a car that is 20–40 meters ahead of the camera (the ego), and up to 10 meters to the left or right, while still using the default heading (namely, being aligned with the road). Here Range(X, Y) creates a uniform distribution on the interval between X and Y, and X @ Y creates a vector from xy coordinates as in Smalltalk [GR83]. If you prefer, you can give a list or tuple of xy coordinates instead, e.g.:

Car offset by (Range(-10, 10), Range(20, 40))

One exception to the above rules for object creation is that if the name of a class is followed immediately by punctuation, then an object is not created. This allows us to refer to a Scenic class without creating an instance of that class in the environment, which is useful for statements like isinstance(obj, Car), [Taxi, Truck], Car.staticMethod, etc.

Local Coordinate Systems

Scenic provides a number of constructs for working with local coordinate systems, which are often helpful when building a scene incrementally out of component parts. Above, we saw how offset by could be used to position an object in the coordinate system of the ego, for instance placing a car a certain distance away from the camera 1.

It is equally easy in Scenic to use local coordinate systems around other objects or even arbitrary points. For example, suppose we want to make the scenario above more realistic by not requiring the car to be exactly aligned with the road, but to be within say 5°. We could write

Car offset by Range(-10, 10) @ Range(20, 40),
    facing Range(-5, 5) deg

but this is not quite what we want, since this sets the orientation of the car in global coordinates. Thus the car will end up facing within 5° of North, rather than within 5° of the road direction. Instead, we can use Scenic’s general operator X relative to Y, which can interpret vectors and headings as being in a variety of local coordinate systems:

Car offset by Range(-10, 10) @ Range(20, 40),
    facing Range(-5, 5) deg relative to roadDirection

If instead we want the heading to be relative to that of the ego car, so that the two cars are (roughly) aligned, we can simply write Range(-5, 5) deg relative to ego.

Notice that since roadDirection is a vector field, it defines a different local coordinate system at each point in space: at different points on the map, roads point different directions! Thus an expression like 15 deg relative to field does not define a unique heading. The example above works because Scenic knows that the expression Range(-5, 5) deg relative to roadDirection depends on a reference position, and automatically uses the position of the Car being defined. This is a feature of Scenic’s system of specifiers, which we explain next.

Readable, Flexible Specifiers

The syntax offset by X and facing Y for specifying positions and orientations may seem unusual compared to typical constructors in object-oriented languages. There are two reasons why Scenic uses this kind of syntax: first, readability. The second is more subtle and based on the fact that in natural language there are many ways to specify positions and other properties, some of which interact with each other. Consider the following ways one might describe the location of an object:

  1. “is at position X” (an absolute position)

  2. “is just left of position X” (a position based on orientation)

  3. “is 3 m West of the taxi” (a relative position)

  4. “is 3 m left of the taxi” (a local coordinate system)

  5. “is one lane left of the taxi” (another local coordinate system)

  6. “appears to be 10 m behind the taxi” (relative to the line of sight)

  7. “is 10 m along the road from the taxi” (following a potentially-curving vector field)

These are all fundamentally different from each other: for example, (4) and (5) differ if the taxi is not parallel to the lane.

Furthermore, these specifications combine other properties of the object in different ways: to place the object “just left of” a position, we must first know the object’s heading; whereas if we wanted to face the object “towards” a location, we must instead know its position. There can be chains of such dependencies: for example, the description “the car is 0.5 m left of the curb” means that the right edge of the car is 0.5 m away from the curb, not its center, which is what the car’s position property stores. So the car’s position depends on its width, which in turn depends on its model. In a typical object-oriented language, these dependencies might be handled by first computing values for position and all other properties, then passing them to a constructor. For “a car is 0.5 m left of the curb” we might write something like:

# hypothetical Python-like language
model = Car.defaultModelDistribution.sample()
pos = curb.offsetLeft(0.5 + model.width / 2)
car = Car(pos, model=model)

Notice how model must be used twice, because model determines both the model of the car and (indirectly) its position. This is inelegant, and breaks encapsulation because the default model distribution is used outside of the Car constructor. The latter problem could be fixed by having a specialized constructor or factory function:

# hypothetical Python-like language
car = CarLeftOfBy(curb, 0.5)

However, such functions would proliferate since we would need to handle all possible combinations of ways to specify different properties (e.g. do we want to require a specific model? Are we overriding the width provided by the model for this specific car?). Instead of having a multitude of such monolithic constructors, Scenic factors the definition of objects into potentially-interacting but syntactically-independent parts:

Car left of spot by 0.5,
    with model CarModel.models['BUS']

Here left of X by D and with model M are specifiers which do not have an order, but which together specify the properties of the car. Scenic works out the dependencies between properties (here, position is provided by left of, which depends on width, whose default value depends on model) and evaluates them in the correct order. To use the default model distribution we would simply omit line 2; keeping it affects the position of the car appropriately without having to specify BUS more than once.

Specifying Multiple Properties Together

Recall that we defined the default position for a Car to be a Point on road: this is an example of another specifier, on region, which specifies position to be a uniformly random point in the given region. This specifier illustrates another feature of Scenic, namely that specifiers can specify multiple properties simultaneously. Consider the following scenario, which creates a parked car given a region curb (also defined in the scenic.simulators.gta.model library):

spot = OrientedPoint on visible curb
Car left of spot by 0.25

The function visible region returns the part of the region that is visible from the ego object. The specifier on visible curb will then set position to be a uniformly random visible point on the curb. We create spot as an OrientedPoint, which is a built-in class that defines a local coordinate system by having both a position and a heading. The on region specifier can also specify heading if the region has a preferred orientation (a vector field) associated with it: in our example, curb is oriented by roadDirection. So spot is, in fact, a uniformly random visible point on the curb, oriented along the road. That orientation then causes the Car to be placed 0.25 m left of spot in spot’s local coordinate system, i.e. 0.25 m away from the curb, as desired.

In fact, Scenic makes it easy to elaborate this scenario without needing to alter the code above. Most simply, we could specify a particular model or non-default distribution over models by just adding with model M to the definition of the Car. More interestingly, we could produce a scenario for badly-parked cars by adding two lines:

spot = OrientedPoint on visible curb
badAngle = Uniform(1, -1) * Range(10, 20) deg
Car left of spot by 0.25,
    facing badAngle relative to roadDirection

This will yield cars parked 10-20° off from the direction of the curb, as seen in the image below. This example illustrates how specifiers greatly enhance Scenic’s flexibility and modularity.

Badly-parked car image.

A scene sampled from the badly-parked car scenario, rendered in GTA V.

Declarative Hard and Soft Constraints

Notice that in the scenarios above we never explicitly ensured that two cars will not intersect each other. Despite this, Scenic will never generate such scenes. This is because Scenic enforces several default requirements:

  • All objects must be contained in the workspace, or a particular specified region (its container). For example, we can define the Car class so that all of its instances must be contained in the region road by default.

  • Objects must not intersect each other (unless explicitly allowed).

  • Objects must be visible from the ego object (so that they affect the rendered image; this requirement can also be disabled, for example for dynamic scenarios).

Scenic also allows the user to define custom requirements checking arbitrary conditions built from various geometric predicates. For example, the following scenario produces a car headed roughly towards the camera, while still facing the nominal road direction:

ego = Car on road
car2 = Car offset by Range(-10, 10) @ Range(20, 40), with viewAngle 30 deg
require car2 can see ego

Here we have used the X can see Y predicate, which in this case is checking that the ego car is inside the 30° view cone of the second car.

Requirements, called observations in other probabilistic programming languages, are very convenient for defining scenarios because they make it easy to restrict attention to particular cases of interest. Note how difficult it would be to write the scenario above without the require statement: when defining the ego car, we would have to somehow specify those positions where it is possible to put a roughly-oncoming car 20–40 meters ahead (for example, this is not possible on a one-way road). Instead, we can simply place ego uniformly over all roads and let Scenic work out how to condition the distribution so that the requirement is satisfied 2. As this example illustrates, the ability to declaratively impose constraints gives Scenic greater versatility than purely-generative formalisms. Requirements also improve encapsulation by allowing us to restrict an existing scenario without altering it. For example:

import genericTaxiScenario    # import another Scenic scenario
fifthAvenue = ...             # extract a Region from a map here
require genericTaxiScenario.taxi on fifthAvenue

The constraints in our examples above are hard requirements which must always be satisfied. Scenic also allows imposing soft requirements that need only be true with some minimum probability:

require[0.5] car2 can see ego   # condition only needs to hold with prob. >= 0.5

Such requirements can be useful, for example, in ensuring adequate representation of a particular condition when generating a training set: for instance, we could require that at least 90% of generated images have a car driving on the right side of the road.

Mutations

A common testing paradigm is to randomly generate variations of existing tests. Scenic supports this paradigm by providing syntax for performing mutations in a compositional manner, adding variety to a scenario without changing its code. For example, given a complex scenario involving a taxi, we can add one additional line:

from bigScenario import taxi
mutate taxi

The mutate statement will add Gaussian noise to the position and heading properties of taxi, while still enforcing all built-in and custom requirements. The standard deviation of the noise can be scaled by writing, for example, mutate taxi by 2 (which adds twice as much noise), and in fact can be controlled separately for position and heading (see scenic.core.object_types.Mutator).

A Worked Example

We conclude with a larger example of a Scenic program which also illustrates the language’s utility across domains and simulators. Specifically, we consider the problem of testing a motion planning algorithm for a Mars rover able to climb over rocks. Such robots can have very complex dynamics, with the feasibility of a motion plan depending on exact details of the robot’s hardware and the geometry of the terrain. We can use Scenic to write a scenario generating challenging cases for a planner to solve in simulation.

We will write a scenario representing a rubble field of rocks and piples with a bottleneck between the rover and its goal that forces the path planner to consider climbing over a rock. First, we import a small Scenic library for the Webots robotics simulator (scenic.simulators.webots.mars.model) which defines the (empty) workspace and several types of objects: the Rover itself, the Goal (represented by a flag), and debris classes Rock, BigRock, and Pipe. Rock and BigRock have fixed sizes, and the rover can climb over them; Pipe cannot be climbed over, and can represent a pipe of arbitrary length, controlled by the length property (which corresponds to Scenic’s y axis).

1from scenic.simulators.webots.mars.model import *

Then we create the rover at a fixed position and the goal at a random position on the other side of the workspace:

2ego = Rover at 0 @ -2
3goal = Goal at Range(-2, 2) @ Range(2, 2.5)

Next we pick a position for the bottleneck, requiring it to lie roughly on the way from the robot to its goal, and place a rock there.

4bottleneck = OrientedPoint offset by Range(-1.5, 1.5) @ Range(0.5, 1.5),
5                           facing Range(-30, 30) deg
6require abs((angle to goal) - (angle to bottleneck)) <= 10 deg
7BigRock at bottleneck

Note how we define bottleneck as an OrientedPoint, with a range of possible orientations: this is to set up a local coordinate system for positioning the pipes making up the bottleneck. Specifically, we position two pipes of varying lengths on either side of the bottleneck, with their ends far enough apart for the robot to be able to pass between:

 8halfGapWidth = (1.2 * ego.width) / 2
 9leftEnd = OrientedPoint left of bottleneck by halfGapWidth,
10                        facing Range(60, 120) deg relative to bottleneck
11rightEnd = OrientedPoint right of bottleneck by halfGapWidth,
12                         facing Range(-120, -60) deg relative to bottleneck
13Pipe ahead of leftEnd, with length Range(1, 2)
14Pipe ahead of rightEnd, with length Range(1, 2)

Finally, to make the scenario slightly more interesting, we add several additional obstacles, positioned either on the far side of the bottleneck or anywhere at random (recalling that Scenic automatically ensures that no objects will overlap).

15BigRock beyond bottleneck by Range(-0.5, 0.5) @ Range(0.5, 1)
16BigRock beyond bottleneck by Range(-0.5, 0.5) @ Range(0.5, 1)
17Pipe
18Rock
19Rock
20Rock

This completes the scenario, which can also be found in the Scenic repository under examples/webots/mars/narrowGoal.scenic. Several scenes generated from the scenario and visualized in Webots are shown below.

Mars rover scenario image.

A scene sampled from the Mars rover scenario, rendered in Webots.

_images/mars3.jpg _images/mars4.jpg _images/mars5.jpg

Further Reading

This tutorial illustrated the syntax of Scenic through several simple examples. Much more complex scenarios are possible, such as the platoon and bumper-to-bumper traffic GTA V scenarios shown below. For many further examples using a variety of simulators, see the examples folder, as well as the links in the Supported Simulators page.

_images/platoon2.jpg _images/platoon3.jpg _images/platoon4.jpg _images/btb1.jpg _images/btb3.jpg _images/btb4.jpg

Our page on Dynamic Scenarios describes how to define scenarios with dynamic agents that move or take other actions over time.

For a comprehensive overview of Scenic’s syntax, including details on all specifiers, operators, distributions, statements, and built-in classes, see the Language Reference. Our Syntax Guide summarizes all of these language constructs in convenient tables with links to the detailed documentation.

Footnotes

1

In fact, ego is a variable and can be reassigned, so we can set ego to one object, build a part of the scene around it, then reassign ego and build another part of the scene.

2

On the other hand, Scenic may have to work hard to satisfy difficult constraints. Ultimately Scenic falls back on rejection sampling, which in the worst case will run forever if the constraints are inconsistent (although we impose a limit on the number of iterations: see Scenario.generate).

References

F22

Fremont et al., Scenic: A Language for Scenario Specification and Data Generation, Machine Learning, 2022. [Online]

F19

Fremont et al., Scenic: A Language for Scenario Specification and Scene Generation, PLDI 2019.

GR83

Goldberg and Robson, Smalltalk-80: The Language and its Implementation, Addison-Wesley, 1983. [PDF]

Dynamic Scenarios

The Scenic Tutorial described how Scenic can model scenarios like “a badly-parked car” by defining spatial relationships between objects. Here, we’ll cover how to model temporal aspects of scenarios: for a scenario like “a badly-parked car, which pulls into the road as the ego car approaches”, we need to specify not only the initial position of the car but how it behaves over time.

Agents, Actions, and Behaviors

In Scenic, we call objects which take actions over time dynamic agents, or simply agents. These are ordinary Scenic objects, so we can still use all of Scenic’s syntax for describing their initial positions, orientations, etc. In addition, we specify their dynamic behavior using a built-in property called behavior. Here’s an example using one of the built-in behaviors from the Driving Domain:

model scenic.domains.driving.model
Car with behavior FollowLaneBehavior

A behavior defines a sequence of actions for the agent to take, which need not be fixed but can be probabilistic and depend on the state of the agent or other objects. In Scenic, an action is an instantaneous operation executed by an agent, like setting the steering angle of a car or turning on its headlights. Most actions are specific to particular application domains, and so different sets of actions are provided by different simulator interfaces. For example, the Driving Domain defines a SetThrottleAction for cars.

To define a behavior, we write a function which runs over the course of the scenario, periodically issuing actions. Scenic uses a discrete notion of time, so at each time step the function specifies zero or more actions for the agent to take. For example, here is a very simplified version of the FollowLaneBehavior above:

behavior FollowLaneBehavior():
    while True:
        throttle, steering = ...    # compute controls
        take SetThrottleAction(throttle), SetSteerAction(steering)

We intend this behavior to run for the entire scenario, so we use an infinite loop. In each step of the loop, we compute appropriate throttle and steering controls, then use the take statement to take the corresponding actions. When that statement is executed, Scenic pauses the behavior until the next time step of the simulation, when the function resumes and the loop repeats.

When there are multiple agents, all of their behaviors run in parallel; each time step, Scenic sends their selected actions to the simulator to be executed and advances the simulation by one step. It then reads back the state of the simulation, updating the positions and other dynamic properties of the objects.

Diagram showing interaction between Scenic and a simulator.

Behaviors can access the current state of the world to decide what actions to take:

behavior WaitUntilClose(threshold=15):
    while (distance from self to ego) > threshold:
        wait
    do FollowLaneBehavior()

Here, we repeatedly query the distance from the agent running the behavior (self) to the ego car; as long as it is above a threshold, we wait, which means take no actions. Once the threshold is met, we start driving by invoking the FollowLaneBehavior we saw above using the do statement. Since FollowLaneBehavior runs forever, we will never return to the WaitUntilClose behavior.

The example above also shows how behaviors may take arguments, like any Scenic function. Here, threshold is an argument to the behavior which has default value 15 but can be customized, so we could write for example:

ego = Car
car2 = Car visible, with behavior WaitUntilClose
car3 = Car visible, with behavior WaitUntilClose(20)

Both car2 and car3 will use the WaitUntilClose behavior, but independent copies of it with thresholds of 15 and 20 respectively.

Unlike ordinary Scenic code, control flow constructs such as if and while are allowed to depend on random variables inside a behavior. Any distributions defined inside a behavior are sampled at simulation time, not during scene sampling. Consider the following behavior:

1behavior Foo:
2    threshold = Range(4, 7)
3    while True:
4        if self.distanceToClosest(Pedestrian) < threshold:
5            strength = TruncatedNormal(0.8, 0.02, 0.5, 1)
6            take SetBrakeAction(strength), SetThrottleAction(0)
7        else:
8            take SetThrottleAction(0.5), SetBrakeAction(0)

Here, the value of threshold is sampled only once, at the beginning of the scenario when the behavior starts running. The value strength, on the other hand, is sampled every time control reaches line 5, so that every time step when the car is braking we use a slightly different braking strength (0.8 on average, but with Gaussian noise added with standard deviation 0.02, truncating the possible values to between 0.5 and 1).

Interrupts

It is frequently useful to take an existing behavior and add a complication to it; for example, suppose we want a car that follows a lane, stopping whenever it encounters an obstacle. Scenic provides a concept of interrupts which allows us to reuse the basic FollowLaneBehavior without having to modify it:

behavior FollowAvoidingObstacles():
    try:
        do FollowLaneBehavior()
    interrupt when self.distanceToClosest(Object) < 5:
        take SetBrakeAction(1)

This try-interrupt statement has similar syntax to the Python try statement (and in fact allows except clauses just as in Python), and begins in the same way: at first, the code block after the try: (the body) is executed. At the start of every time step during its execution, the condition from each interrupt clause is checked; if any are true, execution of the body is suspended and we instead begin to execute the corresponding interrupt handler. In the example above, there is only one interrupt, which fires when we come within 5 meters of any object. When that happens, FollowLaneBehavior is paused and we instead apply full braking for one time step. In the next step, we will resume FollowLaneBehavior wherever it left off, unless we are still within 5 meters of an object, in which case the interrupt will fire again.

If there are multiple interrupt clauses, successive clauses take precedence over those which precede them. Furthermore, such higher-priority interrupts can fire even during the execution of an earlier interrupt handler. This makes it easy to model a hierarchy of behaviors with different priorities; for example, we could implement a car which drives along a lane, passing slow cars and avoiding collisions, along the following lines:

behavior Drive():
    try:
        do FollowLaneBehavior()
    interrupt when self.distanceToNextObstacle() < 20:
        do PassingBehavior()
    interrupt when self.timeToCollision() < 5:
        do CollisionAvoidance()

Here, the car begins by lane following, switching to passing if there is a car or other obstacle too close ahead. During either of those two sub-behaviors, if the time to collision gets too low, we switch to collision avoidance. Once the CollisionAvoidance behavior completes, we will resume whichever behavior was interrupted earlier. If we were in the middle of PassingBehavior, it will run to completion (possibly being interrupted again) before we finally resume FollowLaneBehavior.

As this example illustrates, when an interrupt handler completes, by default we resume execution of the interrupted code. If this is undesired, the abort statement can be used to cause the entire try-interrupt statement to exit. For example, to run a behavior until a condition is met without resuming it afterward, we can write:

behavior ApproachAndTurnLeft():
    try:
        do FollowLaneBehavior()
    interrupt when (distance from self to intersection) < 10:
        abort    # cancel lane following
    do WaitForTrafficLightBehavior()
    do TurnLeftBehavior()

This is a common enough use case of interrupts that Scenic provides a shorthand notation:

behavior ApproachAndTurnLeft():
    do FollowLaneBehavior() until (distance from self to intersection) < 10
    do WaitForTrafficLightBehavior()
    do TurnLeftBehavior()

Scenic also provides a shorthand for interrupting a behavior after a certain period of time:

behavior DriveForAWhile():
    do FollowLaneBehavior() for 30 seconds

The alternative form do behavior for n steps uses time steps instead of real simulation time.

Finally, note that when try-interrupt statements are nested, interrupts of the outer statement take precedence. This makes it easy to build up complex behaviors in a modular way. For example, the behavior Drive we wrote above is relatively complicated, using interrupts to switch between several different sub-behaviors. We would like to be able to put it in a library and reuse it in many different scenarios without modification. Interrupts make this straightforward; for example, if for a particular scenario we want a car that drives normally but suddenly brakes for 5 seconds when it reaches a certain area, we can write:

behavior DriveWithSuddenBrake():
    haveBraked = False
    try:
        do Drive()
    interrupt when self in targetRegion and not haveBraked:
        do StopBehavior() for 5 seconds
        haveBraked = True

With this behavior, Drive operates as it did before, interrupts firing as appropriate to switch between lane following, passing, and collision avoidance. But during any of these sub-behaviors, if the car enters the targetRegion it will immediately brake for 5 seconds, then pick up where it left off.

Stateful Behaviors

As the last example shows, behaviors can use local variables to maintain state, which is useful when implementing behaviors which depend on actions taken in the past. To elaborate on that example, suppose we want a car which usually follows the Drive behavior, but every 15-30 seconds stops for 5 seconds. We can implement this behavior as follows:

behavior DriveWithRandomStops():
    delay = Range(15, 30) seconds
    last_stop = 0
    try:
        do Drive()
    interrupt when simulation.currentTime - last_stop > delay:
        do StopBehavior() for 5 seconds
        delay = Range(15, 30) seconds
        last_stop = simulation.currentTime

Here delay is the randomly-chosen amount of time to run Drive for, and last_stop keeps track of the time when we last started to run it. When the time elapsed since last_stop exceeds delay, we interrupt Drive and stop for 5 seconds. Afterwards, we pick a new delay before the next stop, and save the current time in last_stop, effectively resetting our timer to zero.

Note

It is possible to change global state from within a behavior by using the Python global statement, for instance to communicate between behaviors. If using this ability, keep in mind that the order in which behaviors of different agents is executed within a single time step could affect your results. The default order is the order in which the agents were defined, but it can be adjusted by overriding the Simulation.scheduleForAgents method.

Requirements and Monitors

Just as you can declare spatial constraints on scenes using the require statement, you can also impose constraints on dynamic scenarios. For example, if we don’t want to generate any simulations where car1 and car2 are simultaneously visible from the ego car, we could write:

require always not ((ego can see car1) and (ego can see car2))

The require always condition statement enforces that the given condition must hold at every time step of the scenario; if it is ever violated during a simulation, we reject that simulation and sample a new one. Similarly, we can require that a condition hold at some time during the scenario using the require eventually statement:

require eventually ego in intersection

You can also use the ordinary require statement inside a behavior to require that a given condition hold at a certain point during the execution of the behavior. For example, here is a simple elaboration of the WaitUntilClose behavior we saw above:

behavior WaitUntilClose(threshold=15):
    while (distance from self to ego) > threshold:
        require self.distanceToClosest(Pedestrian) > threshold
        wait
    do FollowLaneBehavior()

The requirement ensures that no pedestrian comes close to self until the ego does; after that, we place no further restrictions.

To enforce more complex temporal properties like this one without modifying behaviors, you can define a monitor. Like behaviors, monitors are functions which run in parallel with the scenario, but they are not associated with any agent and any actions they take are ignored (so you might as well only use the wait statement). Here is a monitor for the property “car1 and car2 enter the intersection before car3”:

1monitor Car3EntersLast:
2    seen1, seen2 = False, False
3    while not (seen1 and seen2):
4        require car3 not in intersection
5        if car1 in intersection:
6            seen1 = True
7        if car2 in intersection:
8            seen2 = True
9        wait

We use the variables seen1 and seen2 to remember whether we have seen car1 and car2 respectively enter the intersection. The loop will iterate as long as at least one of the cars has not yet entered the intersection, so if car3 enters before either car1 or car2, the requirement on line 4 will fail and we will reject the simulation. Note the necessity of the wait statement on line 9: if we omitted it, the loop could run forever without any time actually passing in the simulation.

Preconditions and Invariants

Even general behaviors designed to be used in multiple scenarios may not operate correctly from all possible starting states: for example, FollowLaneBehavior assumes that the agent is actually in a lane rather than, say, on a sidewalk. To model such assumptions, Scenic provides a notion of guards for behaviors. Most simply, we can specify one or more preconditions:

behavior MergeInto(newLane):
    precondition: self.lane is not newLane and self.road is newLane.road
    ...

Here, the precondition requires that whenever the MergeInto behavior is executed by an agent, the agent must not already be in the destination lane but should be on the same road. We can add any number of such preconditions; like ordinary requirements, violating any precondition causes the simulation to be rejected.

Since behaviors can be interrupted, it is possible for a behavior to resume execution in a state it doesn’t expect: imagine a car which is lane following, but then swerves onto the shoulder to avoid an accident; naïvely resuming lane following, we find we are no longer in a lane. To catch such situations, Scenic allows us to define invariants which are checked at every time step during the execution of a behavior, not just when it begins running. These are written similarly to preconditions:

behavior FollowLaneBehavior():
    invariant: self in road
    ...

While the default behavior for guard violations is to reject the simulation, in some cases it may be possible to recover from a violation by taking some additional actions. To enable this kind of design, Scenic signals guard violations by raising a GuardViolation exception which can be caught like any other exception; the simulation is only rejected if the exception propagates out to the top level. So to model the lane-following-with-collision-avoidance behavior suggested above, we could write code like this:

behavior Drive():
    while True:
        try:
            do FollowLaneBehavior()
        interrupt when self.distanceToClosest(Object) < 5:
            do CollisionAvoidance()
        except InvariantViolation:   # FollowLaneBehavior has failed
            do GetBackOntoRoad()

When any object comes within 5 meters, we suspend lane following and switch to collision avoidance. When the CollisionAvoidance behavior completes, FollowLaneBehavior will be resumed; if its invariant fails because we are no longer on the road, we catch the resulting InvariantViolation exception and run a GetBackOntoRoad behavior to restore the invariant. The whole try statement then completes, so the outermost loop iterates and we begin lane following once again.

Terminating the Scenario

By default, scenarios run forever, unless the --time option is used to impose a time limit. However, scenarios can also define termination criteria using the terminate when statement; for example, we could decide to end a scenario as soon as the ego car travels at least a certain distance:

start = Point on road
ego = Car at start
terminate when (distance to start) >= 50

Additionally, the terminate statement can be used inside behaviors and monitors: if it is ever executed, the scenario ends. For example, we can use a monitor to terminate the scenario once the ego spends 30 time steps in an intersection:

monitor StopAfterTimeInIntersection:
    totalTime = 0
    while totalTime < 30:
        if ego in intersection:
            totalTime += 1
        wait
    terminate

Note

In order to make sure that requirements are not violated, termination criteria are only checked after all requirements. So if in the same time step a monitor uses the terminate statement but another behavior uses require with a false condition, the simulation will be rejected rather than terminated.

Trying Some Examples

You can see all of the above syntax in action by running some of our examples of dynamic scenarios. We have examples written for the CARLA and LGSVL driving simulators, and those in examples/driving in particular are designed to use Scenic’s abstract driving domain and so work in either of these simulators, as well as Scenic’s built-in Newtonian physics simulator. The Newtonian simulator is convenient for testing and simple experiments; you can find details on how to install the more realistic simulators in our Supported Simulators page (they should work on both Linux and Windows, but not macOS, at the moment).

Let’s try running examples/driving/badlyParkedCarPullingIn.scenic, which implements the “a badly-parked car, which pulls into the road as the ego car approaches” scenario we mentioned above. To start out, you can run it like any other Scenic scenario to get the usual schematic diagram of the generated scenes:

$ scenic examples/driving/badlyParkedCarPullingIn.scenic

To run dynamic simulations, add the --simulate option (-S for short). Since this scenario is not written for a particular simulator, you’ll need to specify which one you want by using the --model option (-m for short) to select the corresponding Scenic world model: for example, to use the Newtonian simulator we could add --model scenic.simulators.newtonian.driving_model. It’s also a good idea to put a time bound on the simulations, which we can do using the --time option.

$ scenic examples/driving/badlyParkedCarPullingIn.scenic \
    --simulate \
    --model scenic.simulators.newtonian.driving_model \
    --time 200

Running the scenario in CARLA is exactly the same, except we use the --model scenic.simulators.carla.model option instead (make sure to start CARLA running first). For LGSVL, the one difference is that this scenario specifies a map which LGSVL doesn’t have built in; fortunately, it’s easy to switch to a different map. For scenarios using the driving domain, the map file is specified by defining a global parameter map, and for the LGSVL interface we use another parameter lgsvl_map to specify the name of the map in LGSVL (the CARLA interface likewise uses a parameter carla_map). These parameters can be set at the command line using the --param option (-p for short); for example, let’s pick the “BorregasAve” LGSVL map, an OpenDRIVE file for which is included in the Scenic repository. We can then run a simulation by starting LGSVL in “API Only” mode and invoking Scenic as follows:

$ scenic examples/driving/badlyParkedCarPullingIn.scenic \
    --simulate \
    --model scenic.simulators.lgsvl.model \
    --time 200 \
    --param map tests/formats/opendrive/maps/LGSVL/borregasave.xodr \
    --param lgsvl_map BorregasAve

Try playing around with different example scenarios and different choices of maps (making sure that you keep the map and lgsvl_map/carla_map parameters consistent). For both CARLA and LGSVL, you don’t have to restart the simulator between scenarios: just kill Scenic 1 and restart it with different arguments.

Further Reading

This tutorial illustrated most of Scenic’s core syntax for dynamic scenarios. As with the rest of Scenic’s syntax, these constructs are summarized in our Syntax Guide, with links to detailed documentation in the Language Reference. You may also be interested in some other sections of the documentation:

Composing Scenarios

Building more complex scenarios out of simpler ones in a modular way.

Supported Simulators

Details on which simulator interfaces support dynamic scenarios.

Execution of Dynamic Scenarios

The gory details of exactly how behaviors run, monitors are checked, etc. (probably not worth reading unless you’re having a subtle timing issue).

Footnotes

1

Or use the --count option to have Scenic automatically terminate after a desired number of simulations.

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 = OrientedPoint on visible ego.laneGroup.curb
        parkedCar = 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 = 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 = 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 = 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
        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

1

In a real implementation, we would probably want to require that the parked car is not initially visible from the ego, to avoid the sudden appearance of cars out of nowhere.

2

Respecting preconditions, so in particular the simulation will be rejected if at some point none of the remaining scenarios to execute are enabled.

Syntax Guide

This page summarizes the syntax of Scenic (excluding syntax inherited from Python). For more details, click the links for individual language constructs to go to the corresponding section of the Language Reference.

Primitive Data Types

Booleans

expressing truth values

Scalars

representing distances, angles, etc. as floating-point numbers

Vectors

representing positions and offsets in space

Headings

representing orientations in space

Vector Fields

associating an orientation (i.e. a heading) to each point in space

Regions

representing sets of points in space

Distributions

Range(low, high)

uniformly-distributed real number in the interval

DiscreteRange(low, high)

uniformly-distributed integer in the (fixed) interval

Normal(mean, stdDev)

normal distribution with the given mean and standard deviation

TruncatedNormal(mean, stdDev, low, high)

normal distribution truncated to the given window

Uniform(value, ...)

uniform over a finite set of values

Discrete({value: weight, ...})

discrete with given values and weights

Point (in | on) region

uniformly-distributed Point in a region

Statements

Compound Statements

Syntax

Meaning

class name[(superclass)]:

Defines a Scenic class.

behavior name(arguments):

Defines a dynamic behavior.

monitor name:

Defines a monitor.

scenario name(arguments):

Defines a modular scenario.

try: ... interrupt when boolean:

Run code with interrupts inside a dynamic behavior or modular scenario.

Simple Statements

Syntax

Meaning

model name

Select the world model.

import module

Import a Scenic or Python module.

param name = value, ...

Define global parameters of the scenario.

require boolean

Define a hard requirement.

require[number] boolean

Define a soft requirement.

require (always | eventually) boolean

Define a dynamic hard requirement.

terminate when boolean

Define a termination condition.

terminate after scalar (seconds | steps)

Set the scenario to terminate after a given amount of time.

mutate identifier, ... [by number]

Enable mutation of the given list of objects.

record [initial | final] value as name

Save a value at every time step or only at the start/end of the simulation.

Dynamic Statements

These statements can only be used inside a dynamic behavior, monitor, or compose block of a modular scenario.

Syntax

Meaning

take action, ...

Take the action(s) specified.

wait

Take no actions this time step.

terminate

Immediately end the scenario.

do behavior/scenario, ...

Run one or more sub-behaviors/sub-scenarios until they complete.

do behavior/scenario, ... until boolean

Run sub-behaviors/scenarios until they complete or a condition is met.

do behavior/scenario, ... for scalar (seconds | steps)

Run sub-behaviors/scenarios for (at most) a specified period of time.

do choose behavior/scenario, ...

Run one choice of sub-behavior/scenario whose preconditions are satisfied.

do shuffle behavior/scenario, ...

Run several sub-behaviors/scenarios in a random order, satisfying preconditions.

abort

Break out of the current try-interrupt statement.

override object specifier, ...

Override properties of an object for the duration of the current scenario.

Objects

The syntax class specifier, ... creates an instance of a Scenic class.

The Scenic class Point provides the basic position properties in the first table below; its subclass OrientedPoint adds the orientation properties in the second table. Finally, the class Object, which represents physical objects and is the default superclass of user-defined Scenic classes, adds the properties in the third table. See the Objects and Classes Reference for details.

Property

Default

Meaning

position 1

(0, 0)

position in global coordinates

viewDistance

50

distance for the ‘can see’ operator

mutationScale

0

overall scale of mutations

positionStdDev

1

mutation standard deviation for position

Properties added by OrientedPoint:

Property

Default

Meaning

heading 1

0

heading in global coordinates

viewAngle

360 degrees

angle for the ‘can see’ operator

headingStdDev

5 degrees

mutation standard deviation for heading

Properties added by Object:

Property

Default

Meaning

width

1

width of bounding box (X axis)

length

1

length of bounding box (Y axis)

speed 1

0

initial speed (later, instantaneous speed)

velocity 1

from speed

initial velocity (later, instantaneous velocity)

angularSpeed 1

0

angular speed (change in heading/time)

behavior

None

dynamic behavior, if any

allowCollisions

False

whether collisions are allowed

requireVisible

True

whether object must be visible from ego

regionContainedIn

workspace

Region the object must lie within

cameraOffset

(0, 0)

position of camera for ‘can see’

1(1,2,3,4,5)

These are dynamic properties, updated automatically every time step during dynamic simulations.

Specifiers

The with property value specifier can specify any property, including new properties not built into Scenic. Additional specifiers for the position and heading properties are listed below.

Diagram illustrating several specifiers.

Illustration of the beyond, behind, and offset by specifiers. Each OrientedPoint (e.g. P) is shown as a bold arrow.

Specifier for position

Meaning

at vector

Positions the object at the given global coordinates

offset by vector

Positions the object at the given coordinates in the local coordinate system of ego (which must already be defined)

offset along direction by vector

Positions the object at the given coordinates, in a local coordinate system centered at ego and oriented along the given direction

(left | right) of vector [by scalar]

Positions the object further to the left/right by the given scalar distance

(ahead of | behind) vector [by scalar]

As above, except placing the object ahead of or behind the given position

beyond vector by vector [from vector]

Positions the object at coordinates given by the second vector, centered at the first vector and oriented along the line of sight from the third vector/ego

visible [from (Point | OrientedPoint)]

Positions the object uniformly at random in the visible region of the ego, or of the given Point/OrientedPoint if given

not visible [from (Point | OrientedPoint)]

Positions the object uniformly at random in the non-visible region of the ego, or of the given Point/OrientedPoint if given

Specifier for position and optionally heading

Meaning

(in | on) region

Positions the object uniformly at random in the given Region

(left | right) of (OrientedPoint | Object) [by scalar]

Positions the object to the left/right of the given OrientedPoint, depending on the object’s width

(ahead of | behind) (OrientedPoint | Object) [by scalar]

As above, except positioning the object ahead of or behind the given OrientedPoint, thereby depending on length

following vectorField [from vector] for scalar

Position by following the given vector field for the given distance starting from ego or the given vector

Specifier for heading

Meaning

facing heading

Orients the object along the given heading in global coordinates

facing vectorField

Orients the object along the given vector field at the object’s position

facing (toward | away from) vector

Orients the object toward/away from the given position (thereby depending on the object’s position)

apparently facing heading [from vector]

Orients the object so that it has the given heading with respect to the line of sight from ego (or the given vector)

Operators

Diagram illustrating several operators.

Illustration of several operators. Each OrientedPoint (e.g. P) is shown as a bold arrow.

Scalar Operators

Meaning

relative heading of heading [from heading]

The relative heading of the given heading with respect to ego (or the from heading)

apparent heading of OrientedPoint [from vector]

The apparent heading of the OrientedPoint, with respect to the line of sight from ego (or the given vector)

distance [from vector] to vector

The distance to the given position from ego (or the from vector)

angle [from vector] to vector

The heading to the given position from ego (or the from vector)

Boolean Operators

Meaning

(Point | OrientedPoint) can see (vector | Object)

Whether or not a position or Object is visible from a Point or OrientedPoint.

(vector | Object) in region

Whether a position or Object lies in the region

Heading Operators

Meaning

scalar deg

The given heading, interpreted as being in degrees

vectorField at vector

The heading specified by the vector field at the given position

direction relative to direction

The first direction, interpreted as an offset relative to the second direction

Vector Operators

Meaning

vector (relative to | offset by) vector

The first vector, interpreted as an offset relative to the second vector (or vice versa)

vector offset along direction by vector

The second vector, interpreted in a local coordinate system centered at the first vector and oriented along the given direction

Region Operators

Meaning

visible region

The part of the given region visible from ego

not visible region

The part of the given region not visible from ego

OrientedPoint Operators

Meaning

vector relative to OrientedPoint

The given vector, interpreted in the local coordinate system of the OrientedPoint

OrientedPoint offset by vector

Equivalent to vector relative to OrientedPoint above

(front | back | left | right) of Object

The midpoint of the corresponding edge of the bounding box of the Object, oriented along its heading

(front | back) (left | right) of Object

The corresponding corner of the Object’s bounding box, also oriented along its heading

Built in Functions

Function

Description

Misc Python functions

Various Python functions including min, max, sin, cos, etc.

filter

Filter a possibly-random list (allowing limited randomized control flow).

resample

Sample a new value from a distribution.

localPath

Convert a relative path to an absolute path, based on the current directory.

verbosePrint

Like print, but silent at low-enough verbosity levels.

simulation

Get the the current simulation object.

Language Reference

Language Constructs

These pages describe the syntax of Scenic in detail. For a one-page summary of Scenic’s syntax, see the Syntax Guide.

Data Types Reference

This page describes the primitive data types built into Scenic. In addition to these types, Scenic provides a class hierarchy for points, oriented points, and objects: see the Objects and Classes Reference.

Boolean

Booleans represent truth values, and can be True or False.

Note

These are equivalent to the Python bool type.

Scalar

Scalars represent distances, angles, etc. as floating-point numbers, which can be sampled from various distributions.

Note

These are equivalent to the Python float type; however, any context which accepts a scalar will also allow an int or a NumPy numeric type such as numpy.single (to be precise, any instance of numbers.Real is legal).

Vector

Vectors represent positions and offsets in space. They are constructed from coordinates with the syntax X @ Y (inspired by Smalltalk); using a length-2 list or tuple ([X, Y] or (X, Y)) is also allowed. By convention, coordinates are in meters, although the semantics of Scenic does not depend on this. More significantly, the vector syntax is specialized for 2-dimensional space. The 2D assumption dramatically simplifies much of Scenic’s syntax (particularly that dealing with orientations, as we will see below), while still being adequate for a variety of applications. Some world models, such as that for the Driving Domain, define an elevation property which defines a 3D location in combination with Scenic’s 2D position property. A future extension of Scenic will natively support 3D space.

For convenience, instances of Point can be used in any context where a vector is expected: so for example if P is a Point, then P offset by (1, 2) is equivalent to P.position offset by (1, 2).

Heading

Headings represent orientations in space. Conveniently, in 2D these can be expressed using a single angle (rather than Euler angles or a quaternion). Scenic represents headings in radians, measured anticlockwise from North, so that a heading of 0 is due North and a heading of π/2 is due West. We use the convention that the heading of a local coordinate system is the heading of its Y-axis, so that, for example, the vector -2 @ 3 means 2 meters left and 3 ahead.

For convenience, instances of OrientedPoint can be used in any context where a heading is expected: so for example if OP is an OrientedPoint, then relative heading of OP is equivalent to relative heading of OP.heading. Since OrientedPoint is a subclass of Point, expressions involving two oriented points like OP1 relative to OP2 can be ambiguous: the polymorphic operator relative to accepts both vectors and headings, and either version could be meant here. Scenic rejects such expressions as being ambiguous: more explicit syntax like OP1.position relative to OP2 must be used instead.

Vector Field

Vector fields associate an orientation (i.e. a heading) to each point in space. For example, a vector field could represent the shortest paths to a destination, or the nominal traffic direction on a road (e.g. scenic.domains.driving.model.roadDirection).

Region

Regions represent sets of points in space. Scenic provides a variety of ways to define regions: rectangles, circular sectors, line segments, polygons, occupancy grids, and explicit lists of points, among others.

Regions can have an associated vector field giving points in the region preferred orientations. For example, a region representing a lane of traffic could have a preferred orientation aligned with the lane, so that we can easily talk about distances along the lane, even if it curves. Another possible use of preferred orientations is to give the surface of an object normal vectors, so that other objects placed on the surface face outward by default.

The main operations available for use with all regions are the (vector | Object) in region operator to test containment within a region, the visible region operator to get the part of a region which is visible from the ego, and the (in | on) region specifier to choose a position uniformly at random inside a region.

If you need to perform more complex operations on regions, or are writing a world model and need to define your own regions, you will have to work with the Region class (which regions are instances of) and its subclasses for particular types of regions. These are summarized below: if you are working on Scenic’s internals, see the scenic.core.regions module for full details.

Abstract Regions
class Region(name, *dependencies, orientation=None)[source]

Abstract class for regions.

intersect(other)[source]

Get a Region representing the intersection of this one with another.

If both regions have a preferred orientation, the one of self is inherited by the intersection.

Return type

Region

intersects(other)[source]

Check if this Region intersects another.

Return type

bool

union(other)[source]

Get a Region representing the union of this one with another.

Not supported by all region types.

Return type

Region

Simple Shapes

Unlike the more complex regions, these simple geometric shapes are allowed to depend on random values: for example, the visible region of an Object is a SectorRegion based at the object’s position, which might not be fixed.

class CircularRegion(center, radius, resolution=32, name=None)[source]

A circular region with a possibly-random center and radius.

Parameters
  • center (Vector) – center of the disc.

  • radius (float) – radius of the disc.

  • resolution (int; optional) – number of vertices to use when approximating this region as a polygon.

  • name (str; optional) – name for debugging.

class SectorRegion(center, radius, heading, angle, resolution=32, name=None)[source]

A sector of a CircularRegion.

This region consists of a sector of a disc, i.e. the part of a disc subtended by a given arc.

Parameters
  • center (Vector) – center of the corresponding disc.

  • radius (float) – radius of the disc.

  • heading (float) – heading of the centerline of the sector.

  • angle (float) – angle subtended by the sector.

  • resolution (int; optional) – number of vertices to use when approximating this region as a polygon.

  • name (str; optional) – name for debugging.

class RectangularRegion(position, heading, width, length, name=None)[source]

A rectangular region with a possibly-random position, heading, and size.

Parameters
  • position (Vector) – center of the rectangle.

  • heading (float) – the heading of the length axis of the rectangle.

  • width (float) – width of the rectangle.

  • length (float) – length of the rectangle.

  • name (str; optional) – name for debugging.

Polylines and Polygons

These subclasses represent fixed 1D and 2D regions defined by line segments and polygons.

class PolylineRegion(points=None, polyline=None, orientation=True, name=None)[source]

Region given by one or more polylines (chain of line segments).

The region may be specified by giving either a sequence of points or shapely polylines (a LineString or MultiLineString).

Parameters
  • points – sequence of points making up the polyline (or None if using the polyline argument instead).

  • polylineshapely polyline or collection of polylines (or None if using the points argument instead).

  • orientation (optional) – preferred orientation to use, or True to use an orientation aligned with the direction of the polyline (the default).

  • name (str; optional) – name for debugging.

property start

Get an OrientedPoint at the start of the polyline.

The OP’s heading will be aligned with the orientation of the region, if there is one (the default orientation pointing along the polyline).

property end

Get an OrientedPoint at the end of the polyline.

The OP’s heading will be aligned with the orientation of the region, if there is one (the default orientation pointing along the polyline).

signedDistanceTo(point)[source]

Compute the signed distance of the PolylineRegion to a point.

The distance is positive if the point is left of the nearest segment, and negative otherwise.

Return type

float

pointAlongBy(distance, normalized=False)[source]

Find the point a given distance along the polyline from its start.

If normalized is true, then distance should be between 0 and 1, and is interpreted as a fraction of the length of the polyline. So for example pointAlongBy(0.5, normalized=True) returns the polyline’s midpoint.

Return type

Vector

__getitem__(i)[source]

Get the ith point along this polyline.

If the region consists of multiple polylines, this order is linear along each polyline but arbitrary across different polylines.

Return type

Vector

__len__()[source]

Get the number of vertices of the polyline.

Return type

int

class PolygonalRegion(points=None, polygon=None, orientation=None, name=None)[source]

Region given by one or more polygons (possibly with holes).

The region may be specified by giving either a sequence of points defining the boundary of the polygon, or a collection of shapely polygons (a Polygon or MultiPolygon).

Parameters
  • points – sequence of points making up the boundary of the polygon (or None if using the polygon argument instead).

  • polygonshapely polygon or collection of polygons (or None if using the points argument instead).

  • orientation (Vector Field; optional) – preferred orientation to use.

  • name (str; optional) – name for debugging.

property boundary: PolylineRegion

Get the boundary of this region as a PolylineRegion.

Point Sets and Grids
class PointSetRegion(name, points, kdTree=None, orientation=None, tolerance=1e-06)[source]

Region consisting of a set of discrete points.

No Object can be contained in a PointSetRegion, since the latter is discrete. (This may not be true for subclasses, e.g. GridRegion.)

Parameters
  • name (str) – name for debugging

  • points (arraylike) – set of points comprising the region

  • kdTree (scipy.spatial.KDTree, optional) – k-D tree for the points (one will be computed if none is provided)

  • orientation (Vector Field; optional) – preferred orientation for the region

  • tolerance (float; optional) – distance tolerance for checking whether a point lies in the region

class GridRegion(name, grid, Ax, Ay, Bx, By, orientation=None)[source]

Bases: PointSetRegion

A Region given by an obstacle grid.

A point is considered to be in a GridRegion if the nearest grid point is not an obstacle.

Parameters
  • name (str) – name for debugging

  • grid – 2D list, tuple, or NumPy array of 0s and 1s, where 1 indicates an obstacle and 0 indicates free space

  • Ax (float) – spacing between grid points along X axis

  • Ay (float) – spacing between grid points along Y axis

  • Bx (float) – X coordinate of leftmost grid column

  • By (float) – Y coordinate of lowest grid row

  • orientation (Vector Field; optional) – orientation of region

Distributions Reference

Scenic provides functions for sampling from various types of probability distributions, and it is also possible to define custom types of distributions.

If you want to sample multiple times from the same distribution (for example if the distribution is passed as an argument to a helper function), you can use the resample function.

Built-in Distributions

Range(low, high)

Uniformly-distributed real number in the interval.

DiscreteRange(low, high)

Uniformly-distributed integer in the (fixed) interval.

Normal(mean, stdDev)

Normal distribution with the given mean and standard deviation.

TruncatedNormal(mean, stdDev, low, high)

Normal distribution as above, but truncated to the given window.

Uniform(value, …)

Uniform over a finite set of values. The Uniform distribution can also be used to uniformly select over a list of unknown length. This can be done using the unpacking operator (which supports distributions over lists) as follows: Uniform(*list).

Discrete({value: weight, … })

Discrete distribution over a finite set of values, with weights (which need not add up to 1). Each value is sampled with probability proportional to its weight.

Uniform Distribution over a Region

Scenic can also sample points uniformly at random from a Region, using the (in | on) region specifier. Most subclasses of Region support random sampling. A few regions, such as the everywhere region representing all space, cannot be sampled from since a uniform distribution over them does not exist.

Defining Custom Distributions

If necessary, custom distributions may be implemented by subclassing the Distribution class. New subclasses must implement the sampleGiven method, which computes a random sample from the distribution given values for its dependencies (if any). See Range (the implementation of the uniform distribution over a range of real numbers) for a simple example of how to define a subclass. Additional functionality can be enabled by implementing the optional clone, bucket, and supportInterval methods; see their documentation for details.

Statements Reference

Compound Statements

Class Definition
class <name>[(<superclass>)]:
    [<property>: <value>]*

Defines a Scenic class. If a superclass is not explicitly specified, Object is used (see Objects and Classes Reference). The body of the class defines a set of properties its objects have, together with default values for each property. Properties are inherited from superclasses, and their default values may be overridden in a subclass. Default values may also use the special syntax self.property to refer to one of the other properties of the same object, which is then a dependency of the default value. The order in which to evaluate properties satisfying all dependencies is computed (and cyclic dependencies detected) during Specifier Resolution.

Scenic classes may also define attributes and methods in the same way as Python classes.

Behavior Definition
behavior <name>(<arguments>):
    [precondition: <boolean>]*
    [invariant: <boolean>]*
    <statement>+

Defines a dynamic behavior, which can be assigned to a Scenic object by setting its behavior property using the with behavior behavior specifier; this makes the object an agent. See our tutorial on Dynamic Scenarios for examples of how to write behaviors.

Behavior definitions have the same form as function definitions, with an argument list and a body consisting of one or more statements; the body may additionally begin with definitions of preconditions and invariants. Preconditions are checked when a behavior is started, and invariants are checked at every time step of the simulation while the behavior is executing (including time step zero, like preconditions, but not including time spent inside sub-behaviors: this allows sub-behaviors to break and restore invariants before they return).

The body of a behavior executes in parallel with the simulation: in each time step, it must either take specified action(s) or wait and perform no actions. After each take or wait statement, the behavior’s execution is suspended, the simulation advances one step, and the behavior is then resumed. It is thus an error for a behavior to enter an infinite loop which contains no take or wait statements (or do statements invoking a sub-behavior; see below): the behavior will never yield control to the simulator and the simulation will stall.

Behaviors end naturally when their body finishes executing (or if they return): if this happens, the agent performing the behavior will take no actions for the rest of the scenario. Behaviors may also terminate the current scenario, ending it immediately.

Behaviors may invoke sub-behaviors, optionally for a limited time or until a desired condition is met, using do statements. It is also possible to (temporarily) interrupt the execution of a sub-behavior under certain conditions and resume it later, using try-interrupt statements.

Monitor Definition
monitor <name>:
    <statement>+

Defines a Scenic monitor, which runs in parallel with the simulation like a dynamic behavior. Monitors are not associated with an Object and cannot take actions, but can wait to wait for the next time step (or terminate the simulation). The main purpose of monitors is to evaluate complex temporal properties that do not fit into the require always and require eventually statements: they can maintain state and use require to enforce requirements depending on that state. For examples of monitors, see our tutorial on Dynamic Scenarios.

Modular Scenario Definition
scenario <name>(<arguments>):
    [precondition: <boolean>]*
    [invariant: <boolean>]*
    [setup:
        <statement>+]
    [compose:
        <statement>+]
scenario <name>(<arguments>):
    <statement>+

Defines a Scenic modular scenario. Scenario definitions, like behavior definitions, may include preconditions and invariants. The body of a scenario consists of two optional parts: a setup block and a compose block. The setup block contains code that runs once when the scenario begins to execute, and is a list of statements like a top-level Scenic program (so it may create objects, define requirements, etc.). The compose block orchestrates the execution of sub-scenarios during a dynamic scenario, and may use do and any of the other statements allowed inside behaviors (except take, which only makes sense for an individual agent). If a modular scenario does not use preconditions, invariants, or sub-scenarios (i.e., it only needs a setup block) it may be written in the second form above, where the entire body of the scenario comprises the setup block.

See also

Our tutorial on Composing Scenarios gives many examples of how to use modular scenarios.

Try-Interrupt Statement
try:
    <statement>+
[interrupt when <boolean>:
    <statement>+]*
[except <exception> [as <name>]:
    <statement>+]*

A try-interrupt statement can be placed inside a behavior (or compose block of a modular scenario) to run a series of statements, including invoking sub-behaviors with do, while being able to interrupt at any point if given conditions are met. When a try-interrupt statement is encountered, the statements in the try block are executed. If at any time step one of the interrupt conditions is met, the corresponding interrupt block (its handler) is entered and run. Once the interrupt handler is complete, control is returned to the statement that was being executed under the try block.

If there are multiple interrupt clauses, successive clauses take precedence over those which precede them; furthermore, during execution of an interrupt handler, successive interrupt clauses continue to be checked and can interrupt the handler. Likewise, if try-interrupt statements are nested, the outermost statement takes precedence and can interrupt the inner statement at any time. When one handler interrupts another and then completes, the original handler is resumed (and it may even be interrupted again before control finally returns to the try block).

The try-interrupt statement may conclude with any number of except blocks, which function identically to their Python counterparts.

Simple Statements

The following statements can occur throughout a Scenic program unless otherwise stated.

model name

Select a world model to use for this scenario. The statement model X is equivalent to from X import * except that X can be replaced using the --model command-line option or the model keyword argument to the top-level APIs. When writing simulator-agnostic scenarios, using the model statement is preferred to a simple import since a more specific world model for a particular simulator can then be selected at compile time.

import module

Import a Scenic or Python module. This statement behaves as in Python, but when importing a Scenic module it also imports any objects created and requirements imposed in that module. Scenic also supports the form from module import identifier, ... , which as in Python imports the module plus one or more identifiers from its namespace.

Note

Scenic modules can only be imported at the top level, or in a top-level try-except block that does not create any objects (so that you can catch ModuleNotFoundError for example). Python modules can be imported dynamically inside functions as usual.

param name = value, …

Defines one or more global parameters of the scenario. These have no semantics in Scenic, simply having their values included as part of the generated Scene, but provide a general-purpose way to encode arbitrary global information.

If multiple param statements define parameters with the same name, the last statement takes precedence, except that Scenic world models imported using the model statement do not override existing values for global parameters. This allows models to define default values for parameters which can be overridden by particular scenarios. Global parameters can also be overridden at the command line using the --param option, or from the top-level API using the params argument to scenic.scenarioFromFile.

To access global parameters within the scenario itself, you can read the corresponding attribute of the globalParameters object. For example, if you declare param weather = 'SUNNY', you could then access this parameter later in the program via globalParameters.weather. If the parameter was not overridden, this would evaluate to 'SUNNY'; if Scenic was run with the command-line option --param weather SNOW, it would evaluate to 'SNOW' instead.

Some simulators provide global parameters whose names are not valid identifiers in Scenic. To support giving values to such parameters without renaming them, Scenic allows the names of global parameters to be quoted strings, as in this example taken from an X-Plane scenario:

param simulation_length = 30
param 'sim/weather/cloud_type[0]' = DiscreteRange(0, 5)
param 'sim/weather/rain_percent' = 0
require boolean

Defines a hard requirement, requiring that the given condition hold in all instantiations of the scenario. This is equivalent to an “observe” statement in other probabilistic programming languages.

require[number] boolean

Defines a soft requirement; like require above but enforced only with the given probability, thereby requiring that the given condition hold with at least that probability (which must be a literal number, not an expression). For example, require[0.75] ego in parking_lot would require that the ego be in the parking lot at least 75% percent of the time.

require (always | eventually) boolean

Require a condition hold at each time step (always) or at some point during the simulation (eventually).

terminate when boolean

Terminates the scenario when the provided condition becomes true. If this statement is used in a modular scenario which was invoked from another scenario, only the current scenario will end, not the entire simulation.

terminate simulation when boolean

The same as terminate when, except terminates the entire simulation even when used inside a sub-scenario (so there is no difference between the two statements when used at the top level).

terminate after scalar (seconds | steps)

Like terminate when above, but terminates the scenario after the given amount of time. The time limit can be an expression, but must be a non-random value.

mutate identifier, … [by scalar]

Enables mutation of the given list of objects (any Point, OrientedPoint, or Object), with an optional scale factor (default 1). If no objects are specified, mutation applies to every Object already created.

The default mutation system adds Gaussian noise to the position and heading properties, with standard deviations equal to the scale factor times the positionStdDev and headingStdDev properties.

Note

User-defined classes may specify custom mutators to allow mutation to apply to properties other than position and heading. This is done by providing a value for the mutator property, which should be an instance of Mutator. Mutators inherited from superclasses (such as the default position and heading mutators from Point and OrientedPoint) will still be applied unless the new mutator disables them; see Mutator for details.

record [initial | final] value [as name]

Record the value of an expression during each simulation. The value can be recorded at the start of the simulation (initial), at the end of the simulation (final), or at every time step (if neither initial nor final is specified). The recorded values are available in the records dictionary of SimulationResult: its keys are the given names of the records (or synthesized names if not provided), and the corresponding values are either the value of the recorded expression or a tuple giving its value at each time step as appropriate. For debugging, the records can also be printed out using the --show-records command-line option.

Dynamic Statements

The following statements are valid only in dynamic behaviors, monitors, and compose blocks.

take action, …

Takes the action(s) specified and pass control to the simulator until the next time step. Unlike wait, this statement may not be used in monitors or modular scenarios, since these do not take actions.

wait

Take no actions this time step.

terminate

Immediately end the scenario. As for terminate when, if this statement is used in a modular scenario which was invoked from another scenario, only the current scenario will end, not the entire simulation.

do behavior/scenario, …

Run one or more sub-behaviors or sub-scenarios in parallel. This statement does not return until all invoked sub-behaviors/scenarios have completed.

do behavior/scenario, … until boolean

As above, except the sub-behaviors/scenarios will terminate when the condition is met.

do behavior/scenario for scalar (seconds | steps)

Run sub-behaviors/scenarios for a set number of simulation seconds/time steps. This statement can return before that time if all the given sub-behaviors/scenarios complete.

do choose behavior/scenario, …

Randomly pick one of the given behaviors/scenarios whose preconditions are satisfied, and run it. If no choices are available, the simulation is rejected.

This statement also allows the more general form do choose { behaviorOrScenario: weight, ... }, giving weights for each choice (which need not add up to 1). Among all choices whose preconditions are satisfied, this picks a choice with probability proportional to its weight.

do shuffle behavior/scenario, …

Like do choose above, except that when the chosen sub-behavior/scenario completes, a different one whose preconditions are satisfied is chosen to run next, and this repeats until all the sub-behaviors/scenarios have run once. If at any point there is no available choice to run (i.e. we have a deadlock), the simulation is rejected.

This statement also allows the more general form do shuffle { behaviorOrScenario: weight, ... }, giving weights for each choice (which need not add up to 1). Each time a new sub-behavior/scenario needs to be selected, this statement finds all choices whose preconditions are satisfied and picks one with probability proportional to its weight.

abort

Used in an interrupt handler to terminate the current try-interrupt statement.

override object specifier, …

Override one or more properties of an object, e.g. its behavior, for the duration of the current scenario. The properties will revert to their previous values when the current scenario terminates. It is illegal to override dynamic properties, since they are set by the simulator each time step and cannot be mutated manually.

Objects and Classes Reference

This page describes the classes built into Scenic, representing points, oriented points, and physical objects, and how they are instantiated to create objects.

Note

The documentation given here describes only the public properties and methods provided by the built-in classes. If you are working on Scenic’s internals, you can find more complete documentation in the scenic.core.object_types module.

Instance Creation

<class> [<specifier> [, <specifier>]*]

Instantiates a Scenic object from a Scenic class. The properties of the object are determined by the given set of zero or more specifiers. For details on the available specifiers and how they interact, see the Specifiers Reference.

Instantiating an instance of Object has a side effect: the object is added to the scenario being defined.

Names of Scenic classes followed immediately by punctuation are not considered instance creations. This allows us to refer to a Scenic class without creating an instance of that class in the environment, which is useful for expressions like isinstance(obj, Car), [Taxi, Truck], Car.staticMethod, etc.

Built-in Classes

Point

Locations in space. This class provides the fundamental property position and several associated properties.

class Point(<specifiers>)[source]

The Scenic base class Point.

The default mutator for Point adds Gaussian noise to position with a standard deviation given by the positionStdDev property.

Properties
  • position (Vector; dynamic) – Position of the point. Default value is the origin.

  • visibleDistance (float) – Distance for can see operator. Default value 50.

  • width (float) – Default value zero (only provided for compatibility with operators that expect an Object).

  • length (float) – Default value zero.

  • mutationScale (float) – Overall scale of mutations, as set by the mutate statement. Default value zero (mutations disabled).

  • positionStdDev (float) – Standard deviation of Gaussian noise to add to this object’s position when mutation is enabled with scale 1. Default value 1.

property visibleRegion

The visible region of this object.

The visible region of a Point is a disc centered at its position with radius visibleDistance.

OrientedPoint

A location along with an orientation, defining a local coordinate system. This class subclasses Point, adding the fundamental property heading and several associated properties.

class OrientedPoint(<specifiers>)[source]

The Scenic class OrientedPoint.

The default mutator for OrientedPoint adds Gaussian noise to heading with a standard deviation given by the headingStdDev property, then applies the mutator for Point.

Properties
  • heading (float; dynamic) – Heading of the OrientedPoint. Default value 0 (North).

  • viewAngle (float) – View cone angle for can see operator. Default value 2π.

  • headingStdDev (float) – Standard deviation of Gaussian noise to add to this object’s heading when mutation is enabled with scale 1. Default value 5°.

property visibleRegion

The visible region of this object.

The visible region of an OrientedPoint is a sector of the disc centered at its position with radius visibleDistance, oriented along heading and subtending an angle of viewAngle.

Object

A physical object. This class subclasses OrientedPoint, adding a variety of properties including:

  • width and length to define the bounding box of the object;

  • allowCollisions, requireVisible, and regionContainedIn to control the built-in requirements that apply to the object;

  • behavior, specifying the object’s dynamic behavior if any;

  • speed, velocity, and other properties capturing the dynamic state of the object during simulations.

The built-in requirements applying to each object are:

  • The object must be completely contained within its container, the region specified as its regionContainedIn property (by default the entire workspace).

  • The object must be visible from the ego object, unless its requireVisible property is set to False.

  • The object must not intersect another object (i.e., their bounding boxes must not overlap), unless either of the two objects has their allowCollisions property set to True.

class Object(<specifiers>)[source]

The Scenic class Object.

This is the default base class for Scenic classes.

Properties
  • width (float) – Width of the object, i.e. extent along its X axis. Default value 1.

  • length (float) – Length of the object, i.e. extent along its Y axis. Default value 1.

  • allowCollisions (bool) – Whether the object is allowed to intersect other objects. Default value False.

  • requireVisible (bool) – Whether the object is required to be visible from the ego object. Default value True.

  • regionContainedIn (Region or None) – A Region the object is required to be contained in. If None, the object need only be contained in the scenario’s workspace.

  • cameraOffset (Vector) – Position of the camera for the can see operator, relative to the object’s position. Default (0, 0).

  • speed (float; dynamic) – Speed in dynamic simulations. Default value 0.

  • velocity (Vector; dynamic) – Velocity in dynamic simulations. Default value is the velocity determined by self.speed and self.heading.

  • angularSpeed (float; dynamic) – Angular speed in dynamic simulations. Default value 0.

  • behavior – Behavior for dynamic agents, if any (see Dynamic Scenarios). Default value None.

startDynamicSimulation()[source]

Hook called at the beginning of each dynamic simulation.

Does nothing by default; provided for objects to do simulator-specific initialization as needed.

property visibleRegion

The visible region of this object.

The visible region of an Object is a circular sector as for OrientedPoint, except that the base of the sector may be offset from position by the cameraOffset property (to allow modeling cameras which are not located at the center of the object).

Specifiers Reference

Specifiers are used to define the properties of an object when a Scenic class is instantiated. This page describes all the specifiers built into Scenic, and the procedure used to resolve a set of specifiers into an assignment of values to properties.

Each specifier assigns values one or more properties of an object, as a function of the arguments of the specifier and possibly other properties of the object assigned by other specifiers. For example, the left of X by Y specifier assigns the position property of the object being defined so that the object is a distance Y to the left of X: this requires knowing the width of the object first, so we say the left of specifier specifies the position property and depends on the width property. In fact, the left of specifier also optionally specifies the heading property (to be the same as X), meaning that it assigns a value to heading if no other specifier does so: if we write Object left of X by Y, facing Z, then the new object’s heading property will be determined by facing, not left of. The Specifier Resolution process works out which specifier determines each property of an object, as well as an appropriate order in which to evaluate the specifiers so that dependencies have already been computed when needed.

General Specifiers

with property value

Assigns the given property to the given value. This is currently the only specifier available for properties other than position and heading.

Position Specifiers

Diagram illustrating several specifiers.

Illustration of the beyond, behind, and offset by specifiers. Each OrientedPoint (e.g. P) is shown as a bold arrow.

at vector

Positions the object at the given global coordinates.

offset by vector

Positions the object at the given coordinates in the local coordinate system of ego (which must already be defined).

offset along direction by vector

Positions the object at the given coordinates, in a local coordinate system centered at ego and oriented along the given direction (which, if a vector field, is evaluated at ego to obtain a heading).

(left | right) of vector [by scalar]

Depends on heading and width. Without the optional by scalar, positions the object immediately to the left/right of the given position; i.e., so that the midpoint of the object’s right/left edge is at that position. If by scalar is used, the object is placed further to the left/right by the given distance.

(ahead of | behind) vector [by scalar]

As above, except placing the object ahead of or behind the given position (so that the midpoint of the object’s back/front edge is at that position); thereby depending on heading and length.

beyond vector by vector [from vector]

Positions the object at coordinates given by the second vector, in a local coordinate system centered at the first vector and oriented along the line of sight from the third vector (i.e. a heading of 0 in the local coordinate system faces directly away from the first vector). If no third vector is provided, it is assumed to be the ego. For example, beyond taxi by (0, 3) means 3 meters directly behind the taxi as viewed by the camera.

visible [from (Point | OrientedPoint)]

Positions the object uniformly at random in the visible region of the ego, or of the given Point/OrientedPoint if given. More precisely, this specifier sets the position of the object being created (i.e. its center) to be a uniformly-random point in the visible region. (This specifier is therefore slightly stricter than a requirement that the ego can see the object: the specifier makes the center visible, while the can see condition will be satisfied if the center is not visible but some other part of the object is visible.)

not visible [from (Point* | OrientedPoint)]

Like visible [from (Point | OrientedPoint)] except it positions the object uniformly at random in the non-visible region of the ego. Depends on regionContainedIn, in order to restrict the non-visible region to the container of the object being created, which is hopefully a bounded region (if the non-visible region is unbounded, it cannot be uniformly sampled from and an error will be raised).

(in | on) region

Positions the object uniformly at random in the given Region. If the Region has a preferred orientation (a vector field), also optionally specifies heading to be equal to that orientation at the object’s position.

(left | right) of (OrientedPoint | Object) [by scalar]

Positions the object to the left/right of the given OrientedPoint, depending on the object’s width. Also optionally specifies heading to be the same as that of the OrientedPoint. If the OrientedPoint is in fact an Object, the object being constructed is positioned to the left/right of its left/right edge (i.e. the width of both objects is taken into account).

(ahead of | behind) (OrientedPoint | Object) [by scalar]

As above, except positioning the object ahead of or behind the given OrientedPoint, thereby depending on length.

following vectorField [from vector ] for scalar

Positions the object at a point obtained by following the given vector field for the given distance starting from ego (or the position optionally provided with from vector). Optionally specifies heading to be the heading of the vector field at the resulting point.

Note

This specifier uses a forward Euler approximation of the continuous vector field. The choice of step size can be customized for individual fields: see the documentation of Vector Field. If necessary, you can also call the underlying method VectorField.followFrom directly.

Heading Specifiers

facing heading

Orients the object along the given heading in global coordinates.

facing vectorField

Orients the object along the given vector field at the object’s position.

facing (toward | away from) vector

Orients the object so that it faces toward/away from the given position (thereby depending on the object’s position).

apparently facing heading [from vector]

Orients the object so that it has the given heading with respect to the line of sight from ego (or the from vector). For example, apparently facing 90 deg orients the object so that the camera views its left side head-on.

Specifier Resolution

Specifier resolution is the process of determining, given the set of specifiers used to define an object, which properties each specifier should determine and what order to evaluate the specifiers in. As each specifier can specify multiple properties, both non-optionally and optionally, and can depend on the results of other specifiers, this process is somewhat non-trivial. Assuming there are no cyclic dependencies or conflicts, the process will conclude with each property being determined by its unique non-optional specifier if one exists; otherwise its unique optional specifier if one exists; or finally by its default value if no specifiers apply at all (with default values from subclasses overriding those in superclasses).

The full procedure, given a set of specifiers S used to define an instance of class C, works as follows:

  1. If a property is specified non-optionally by mutiple specifiers in S, an ambiguity error is raised.

  2. The set of properties P for the new object is found by combining the properties specified by all members of S with the properties inherited from the class C.

  3. Default value specifiers from C (or if not overridden, from its superclasses) are added to S as needed so that each property in P is paired with a unique specifier in S specifying it, using the following precedence order: non-optional specifier, optional specifier, then default value.

  4. The dependency graph of the specifiers S is constructed. If it is cyclic, an error is raised.

  5. The graph is topologically sorted and the specifiers are evaluated in this order to determine the values of all properties P of the new object.

Operators Reference

Diagram illustrating several operators.

Illustration of several operators. Each OrientedPoint (e.g. P) is shown as a bold arrow.

Scalar Operators

relative heading of heading [from heading]

The relative heading of the given heading with respect to ego (or the heading provided with the optional from heading)

apparent heading of OrientedPoint [from vector]

The apparent heading of the OrientedPoint, with respect to the line of sight from ego (or the position provided with the optional from vector)

distance [from vector] to vector

The distance to the given position from ego (or the position provided with the optional from vector)

angle [from vector ] to vector

The heading to the given position from ego (or the position provided with the optional from vector ). For example, if angle to taxi is zero, then taxi is due North of ego

Boolean Operators

(Point | OrientedPoint) can see (vector | Object)

Whether or not a position or Object is visible from a Point or OrientedPoint. Visible regions are defined as follows: a Point can see out to a certain distance, and an OrientedPoint restricts this to the circular sector along its heading with a certain angle. A position is then visible if it lies in the visible region, and an Object is visible if its bounding box intersects the visible region.

Note

Technically, Scenic only checks that a corner of the object is visible, which could result in the side of a large object being visible but Scenic not counting it as so. Scenic’s visibility model also does not take into account occlusion, although this would be straightforward to add.

(vector | Object) in region

Whether a position or Object lies in the Region; for the latter, the object’s bounding box must be completely contained in the region.

Heading Operators

scalar deg

The given heading, interpreted as being in degrees. For example 90 deg evaluates to π/2

vectorField at vector

The heading specified by the vector field at the given position

direction relative to direction

The first direction, interpreted as an offset relative to the second direction. For example, -5 deg relative to 90 deg is simply 85 degrees. If either direction is a vector field, then this operator yields an expression depending on the position property of the object being specified.

Vector Operators

vector (relative to | offset by) vector

The first vector, interpreted as an offset relative to the second vector (or vice versa). For example, (5, 5) relative to (100, 200) is (105, 205). Note that this polymorphic operator has a specialized version for instances of OrientedPoint, defined below: so for example (-3, 0) relative to taxi will not use the version of this operator for vectors (even though the Object taxi can be coerced to a vector).

vector offset along direction by vector

The second vector, interpreted in a local coordinate system centered at the first vector and oriented along the given direction (which, if a vector field, is evaluated at the first vector to obtain a heading)

Region Operators

visible region

The part of the given region which is visible from the ego object (i.e. the intersection of the given region with the visible region of the ego).

not visible region

The part of the given region which is not visible from the ego object (as above, based on the ego’s visible region).

OrientedPoint Operators

vector relative to OrientedPoint

The given vector, interpreted in the local coordinate system of the OrientedPoint. So for example (1, 2) relative to ego is 1 meter to the right and 2 meters ahead of ego.

OrientedPoint offset by vector

Equivalent to vector relative to OrientedPoint above

(front | back | left | right) of Object

The midpoint of the corresponding edge of the bounding box of the Object, oriented along its heading/

(front | back) (left | right) of Object

The corresponding corner of the Object’s bounding box, also oriented along its heading.

Built-in Functions Reference

These functions are built into Scenic and may be used without needing to import any modules.

Miscellaneous Python Functions

The following functions work in the same way as their Python counterparts except that they accept random values:

Note

If in the definition of a scene you would like to pass random values into some other function from the Python standard library (or any other Python package), you will need to wrap the function with the distributionFunction decorator. This is not necessary when calling external functions inside requirements or dynamic behaviors.

filter

The filter function works as in Python except it is now able to operate over random lists. This feature can be used to work around Scenic’s lack of support for randomized control flow in certain cases. In particular, Scenic does not allow iterating over a random list, but it is still possible to select a random element satisfying a desired criterion using filter:

mylist = Uniform([-1, 1, 2], [-3, 4])    # pick one of these lists 50/50
filtered = filter(lambda e: e > 0, y)    # extract only the positive elements
x = Uniform(*filtered)                   # pick one of them at random

In the last line, we use Python’s unpacking operator * to use the elements of the chosen list which pass the filter as arguments to Uniform; thus x is sampled as a uniformly-random choice among such elements. 1

For an example of this idiom in a realistic scenario, see examples/driving/OAS_scenarios/oas_scenario_28.scenic.

resample

The resample function takes a distribution and samples a new value from it, conditioned on the values of its parameters, if any. This is useful in cases where you have a complicated distribution that you want multiple samples from.

For example, in the program

x = Uniform(0, 5)
y = Range(x, x+1)
z = resample(y)

with probability 1/2 both y and z are independent uniform samples from the interval \((0, 1)\), and with probability 1/2 they are independent uniform samples from \((5, 6)\). It is never the case that \(y \in (0, 1)\) and \(z \in (5, 6)\) or vice versa, which would require inconsistent assignments to x.

Note

This function can only be applied to the basic built-in distributions (see the Distributions Reference). Resampling a more complex expression like x + y where x and y are distributions would be ambiguous (what if x and y are used elsewhere?) and so is not allowed.

localPath

The localPath function takes a relative path with respect to the directory containing the .scenic file where it is used, and converts it to an absolute path.

verbosePrint

The verbosePrint function operates like print except that it you can specify at what verbosity level (see --verbosity) it should actually print. If no level is specified, it prints at all levels except verbosity 0.

Scenic libraries intended for general use should use this function instead of print so that all non-error messages from Scenic can be silenced by setting verbosity 0.

simulation

The simulation function, available for use in dynamic behaviors and scenarios, returns the currently-running Simulation. This allows access to global information about the simulation, e.g. simulation().currentTime to find the current time step; however, it is provided primarily so that scenarios written for a specific simulator may use simulator-specific functionality (by calling custom methods provided by that simulator’s subclass of Simulation).

1

If there are no such elements, i.e., the filtered list is empty, then Scenic will reject the scenario and try sampling again.

Semantics and Scenario Generation

The pages above describe the semantics of each of Scenic’s constructs individually; the following pages cover the semantics of entire Scenic programs, and how scenes and simulations are generated from them.

Scene Generation

The “output” of a Scenic program has two parts: a scene describing a configuration of physical objects, and a policy defining how those objects behave over time. The latter is relevant only for running dynamic simulations from a Scenic program, and is discussed in our page on Execution of Dynamic Scenarios. In this page, we describe how scenes are generated from a Scenic program.

In Scenic, a scene consists of the following data:

  • a set of objects present in the scene (one of which is designated the ego object);

  • concrete values for all of the properties of these objects, such as position, heading, etc.;

  • concrete values for each global parameter.

A Scenic program defines a probability distribution over such scenes in the usual way for imperative probabilistic programming languages with constraints (often called observations). Running the program ignoring any require statements and making random choices whenever a distribution is evaluated yields a distribution over possible executions of the program and therefore over generated scenes. Then any executions which violate a require condition are discarded, normalizing the probabilities of the remaining executions.

The Scenic tool samples from this distribution using rejection sampling: repeatedly sampling scenes until one is found which satisfies the requirements. This approach has the advantage of allowing arbitrarily-complex requirements and sampling from the exact distribution we want. However, if the requirements have a low probability of being satisfied, it may take many iterations to find a valid scene: in the worst case, if the requirements cannot be satisfied, rejection sampling will run forever (although the Scenario.generate function imposes a finite limit on the number of iterations by default). To reduce the number of iterations required in some common cases, Scenic applies several “pruning” techniques to exclude parts of the scene space which violate the requirements ahead of time (this is done during compilation; see our paper for details). The scene generation procedure then works as follows:

  1. Decide which user-defined requirements will be enforced for this sample (soft requirements have only some probability of being required).

  2. Invoke the external sampler to sample any external parameters.

  3. Sample values for all distributions defined in the scene (all expressions which have random values, represented internally as Distribution objects).

  4. Check if the sampled values satisfy the built-in and user-defined requirements: if not, reject the sample and repeat from step (2).

Execution of Dynamic Scenarios

As described in our tutorial on Dynamic Scenarios, Scenic scenarios can specify the behavior of agents over time, defining a policy which chooses actions for each agent at each time step. Having sampled an initial scene from a Scenic program (see Scene Generation), we can run a dynamic simulation by setting up the scene in a simulator and running the policy in parallel to control the agents. The API for running dynamic simulations is described in Using Scenic Programmatically (mainly the Simulator.simulate method); this page details how Scenic executes such simulations.

The policy for each agent is given by its dynamic behavior, which is a coroutine that usually executes like an ordinary function, but is suspended when it takes an action (using take or wait) and resumed after the simulation has advanced by one time step. As a result, behaviors effectively run in parallel with the simulation. Behaviors are also suspended when they invoke a sub-behavior using do, and are not resumed until the sub-behavior terminates.

When a behavior is first invoked, its preconditions are checked, and if any are not satisfied, the simulation is rejected, requiring a new simulation to be sampled. 1 The behavior’s invariants are handled similarly, except that they are also checked whenever the behavior is resumed (i.e. after taking an action and after a sub-behavior terminates).

Monitors and compose blocks of modular scenarios execute in the same way as behaviors, with the latter also including additional checks to see if any of their terminate when conditions have been met or their require always conditions violated.

In detail, a single time step of a dynamic simulation is executed according to the following procedure:

  1. Execute all currently-running modular scenarios for one time step. Specifically, for each such scenario:

    1. Check if any of its require always conditions are violated; if so, reject the simulation.

    2. Check which require eventually conditions are satisfied; remember these for later.

    3. Check if the scenario’s time limit (if terminate after has been used) has been reached; if so, go to step (f) below to stop the scenario.

    4. If the scenario is not currently running a sub-scenario (with do), check its invariants; if any are violated, reject the simulation. 1

    5. If the scenario has a compose block, run it for one time step (i.e. resume it until it or a subscenario it is currently running using do executes wait). If the block executes a require statement with a false condition, reject the simulation. If it executes terminate, or finishes executing, go to step (f) below to stop the scenario.

    6. If the scenario is stopping for one of the reasons above, first check if any of the require eventually conditions were never satisfied: if so, reject the simulation. Otherwise, the scenario returns to its parent scenario if it was invoked using do; if it was the top-level scenario, we set a flag indicating the top-level scenario has terminated. (We do not terminate immediately since we still need to check monitors in the next step.)

  2. Save the values of all record statements, as well as record initial statements if it is time step 0.

  3. Run each monitor for one time step (i.e. resume it until it executes wait). If it executes a require statement with a false condition, reject the simulation. If it executes terminate, set the termination flag (and continue running any other monitors).

  4. If the termination flag is set, any of the terminate simulation when conditions are satisfied, or a time limit passed to Simulator.simulate has been reached, go to step (10) to terminate the simulation.

  5. Execute the dynamic behavior of each agent to select its action(s) for the time step. Specifically, for each agent’s behavior:

    1. If the behavior is not currently running a sub-behavior (with do), check its invariants; if any are violated, reject the simulation. 1

    2. Resume the behavior until it (or a subbehavior it is currently running using do) executes take or wait. If the behavior executes a require statement with a false condition, reject the simulation. If it executes terminate, go to step (10) to terminate the simulation. Otherwise, save the (possibly empty) set of actions specified for the agent to take.

  6. For each agent, execute the actions (if any) its behavior chose in the previous step.

  7. Run the simulator for one time step.

  8. Update every dynamic property of every object to its current value in the simulator.

  9. Increment the simulation clock (the currentTime attribute of Simulation).

  10. If the simulation is stopping for one of the reasons above, first check if any of the require eventually conditions of any remaining scenarios were never satisfied: if so, reject the simulation. Otherwise, save the values of any record final statements.

Footnotes

1(1,2,3)

By default, violations of preconditions and invariants cause the simulation to be rejected; however, Simulator.simulate has an option to treat them as fatal errors instead.

Command-Line Options

The scenic command supports a variety of options. Run scenic -h for a full list with short descriptions; we elaborate on some of the most important options below.

Options may be given before and after the path to the Scenic file to run, so the syntax of the command is:

$ scenic [options] FILE [options]

General Scenario Control

-m <model>, --model <model>

Specify the world model to use for the scenario, overriding any model statement in the scenario. The argument must be the fully qualified name of a Scenic module found on your PYTHONPATH (it does not necessarily need to be built into Scenic). This allows scenarios written using a generic model, like that provided by the Driving Domain, to be executed in a particular simulator (see the dynamic scenarios tutorial for examples).

The equivalent of this option for the Python API is the model argument to scenic.scenarioFromFile.

-p <param> <value>, --param <param> <value>

Specify the value of a global parameter. This assignment overrides any param statements in the scenario. If the given value can be interpreted as an int or float, it is; otherwise it is kept as a string.

The equivalent of this option for the Python API is the params argument to scenic.scenarioFromFile (which, however, does not attempt to convert strings to numbers).

-s <seed>, --seed <seed>

Specify the random seed used by Scenic, to make sampling deterministic.

This option sets the seed for the Python random number generator random, so external Python code called from within Scenic can also be made deterministic (although random should not be used in place of Scenic’s own sampling constructs in Scenic code). Note though that NumPy provides other RNGs whose seeds are not set by this option (see numpy.random).

--scenario <scenario>

If the given Scenic file defines multiple scenarios, select which one to run. The named modular scenario must not require any arguments.

The equivalent of this option for the Python API is the scenario argument to scenic.scenarioFromFile.

Dynamic Simulations

-S, --simulate

Run dynamic simulations from scenes instead of plotting scene diagrams. This option will only work for scenarios which specify a simulator, which is done automatically by the world models for the simulator interfaces that support dynamic scenarios, e.g. scenic.simulators.carla.model and scenic.simulators.lgsvl.model. If your scenario is written for an abstract domain, like scenic.domains.driving, you will need to use the --model option to specify the specific model for the simulator you want to use.

--time <steps>

Maximum number of time steps to run each simulation (the default is infinity). Simulations may end earlier if termination criteria defined in the scenario are met (see terminate when and terminate).

--count <number>

Number of successful simulations to run (i.e., not counting rejected simulations). The default is to run forever.

Debugging

--version

Show which version of Scenic is being used.

-v <verbosity>, --verbosity <verbosity>

Set the verbosity level, from 0 to 3 (default 1):

0

Nothing is printed except error messages and warnings (to stderr). Warnings can be suppressed using the PYTHONWARNINGS environment variable.

1

The main steps of compilation and scene generation are indicated, with timing statistics.

2

Additionally, details on which modules are being compiled and the reasons for any scene/simulation rejections are printed.

3

Additionally, the actions taken by each agent at each time step of a dynamic simulation are printed.

This option can be configured from the Python API using scenic.setDebuggingOptions.

--show-params

Show values of global parameters for each generated scene.

--show-records

Show recorded values (see record) for each dynamic simulation.

-b, --full-backtrace

Include Scenic’s internals in backtraces printed for uncaught exceptions. This information will probably only be useful if you are developing Scenic.

This option can be enabled from the Python API using scenic.setDebuggingOptions.

--pdb

If an error occurs, enter the Python interactive debugger pdb. Implies the -b option.

This option can be enabled from the Python API using scenic.setDebuggingOptions.

--pdb-on-reject

If a scene/simulation is rejected (so that another must be sampled), enter pdb. Implies the -b option.

This option can be enabled from the Python API using scenic.setDebuggingOptions.

Using Scenic Programmatically

While Scenic is most easily invoked as a command-line tool, it also provides a Python API for compiling Scenic programs, sampling scenes from them, and running dynamic simulations.

The top-level interface to Scenic is provided by two functions in the scenic module which compile a Scenic program:

scenarioFromFile(path, params={}, model=None, scenario=None, cacheImports=False)[source]

Compile a Scenic file into a Scenario.

Parameters
  • path (str) – Path to a Scenic file.

  • params (dict) – Global parameters to override, as a dictionary mapping parameter names to their desired values.

  • model (str) – Scenic module to use as world model.

  • scenario (str) – If there are multiple modular scenarios in the file, which one to compile; if not specified, a scenario called ‘Main’ is used if it exists.

  • cacheImports (bool) – Whether to cache any imported Scenic modules. The default behavior is to not do this, so that subsequent attempts to import such modules will cause them to be recompiled. If it is safe to cache Scenic modules across multiple compilations, set this argument to True. Then importing a Scenic module will have the same behavior as importing a Python module. See purgeModulesUnsafeToCache for a more detailed discussion of the internals behind this.

Returns

A Scenario object representing the Scenic scenario.

scenarioFromString(string, params={}, model=None, scenario=None, filename='<string>', cacheImports=False)[source]

Compile a string of Scenic code into a Scenario.

The optional filename is used for error messages. Other arguments are as in scenarioFromFile.

The resulting Scenario object represents the abstract scenario defined by the Scenic program. To sample concrete scenes from this object, you can call the Scenario.generate method, which returns a Scene. If you are only using static scenarios, you can extract the sampled values for all the global parameters and objects in the scene from the Scene object. For example:

import random, scenic
random.seed(12345)
scenario = scenic.scenarioFromString('ego = Object with foo Range(0, 5)')
scene, numIterations = scenario.generate()
print(f'ego has foo = {scene.egoObject.foo}')
ego has foo = 2.083099362726706

To run dynamic scenarios, you must instantiate an instance of the Simulator class for the particular simulator you want to use. Each simulator interface that supports dynamic simulations defines a subclass of Simulator; for example, NewtonianSimulator for the simple Newtonian simulator built into Scenic. These subclasses provide simulator-specific functionality, and have different requirements for their use: see the specific documentation of each interface under scenic.simulators for details.

Once you have an instance of Simulator, you can ask it to run a simulation from a Scene by calling the Simulator.simulate method. If Scenic is able to run a simulation that satisfies all the requirements in the Scenic program (potentially after multiple attempts – Scenic uses rejection sampling), this method will return a Simulation object. Results of the simulation can then be obtained by inspecting its result attribute, which is an instance of SimulationResult (simulator-specific subclasses of Simulation may also provide additional information). For example:

import scenic
from scenic.simulators.newtonian import NewtonianSimulator
scenario = scenic.scenarioFromFile('examples/driving/badlyParkedCarPullingIn.scenic',
                                   model='scenic.simulators.newtonian.driving_model')
scene, _ = scenario.generate()
simulator = NewtonianSimulator()
simulation = simulator.simulate(scene, maxSteps=10)
if simulation:  # `simulate` can return None if simulation fails
        result = simulation.result
        for i, state in enumerate(result.trajectory):
                egoPos, parkedCarPos = state
                print(f'Time step {i}: ego at {egoPos}; parked car at {parkedCarPos}')

If you want to monitor data from simulations to see if the system you are testing violates its specfications, you may want to use VerifAI instead of implementing your own code along the lines above. VerifAI supports running tests from Scenic programs, specifying system specifications using temporal logic or arbitrary Python monitor functions, actively searching the space of parameters in a Scenic program to find concrete scenarios where the system violates its specs 1, and more. See the VerifAI documentation for details.

See also

If you get exceptions or unexpected behavior when using the API, Scenic provides various debugging features: see Debugging.

1

VerifAI’s active samplers can be used directly from Scenic when VerifAI is installed. See scenic.core.external_params.

Developing Scenic

This page covers information useful if you will be developing Scenic, either changing the language itself or adding new built-in libraries or simulator interfaces.

Getting Started

Start by cloning our repository on GitHub and setting up your virtual environment. Then to install Scenic and its development dependencies in your virtual environment run:

$ python -m pip install -e ".[dev]"

This will perform an “editable” install, so that any changes you make to Scenic’s code will take effect immediately when running Scenic in your virtual environment.

Note

If you use Poetry, you can instead run the command poetry install -E dev to create the virtual environment and install Scenic in it, then poetry shell to activate the environment.

To find documentation (and code) for specific parts of Scenic’s implementation, see our page on Scenic Internals.

Running the Test Suite

Scenic has an extensive test suite exercising most of the features of the language. We use the pytest Python testing tool. To run the entire test suite, run the command pytest inside the virtual environment from the root directory of the repository.

Some of the tests are quite slow, e.g. those which test the parsing and construction of road networks. We add a --fast option to pytest which skips such tests, while still covering all of the core features of the language. So it is convenient to often run pytest --fast as a quick check, remembering to run the full pytest before making any final commits. You can also run specific parts of the test suite with a command like pytest tests/syntax/test_specifiers.py, or use pytest’s -k option to filter by test name, e.g. pytest -k specifiers.

Note that many of Scenic’s tests are probabilistic, so in order to reproduce a test failure you may need to set the random seed. We use the pytest-randomly plugin to help with this: at the beginning of each run of pytest, it prints out a line like:

Using --randomly-seed=344295085

Adding this as an option, i.e. running pytest --randomly-seed=344295085, will reproduce the same sequence of tests with the same Python/Scenic random seed. As a shortcut, you can use --randomly-seed=last to use the seed from the previous testing run.

If you’re running the test suite on a headless server or just want to stop windows from popping up during testing, use the --no-graphics option to skip graphical tests.

Debugging

You can use Python’s built-in debugger pdb to debug the parsing, compilation, sampling, and simulation of Scenic programs. The Scenic command-line option -b will cause the backtraces printed from uncaught exceptions to include Scenic’s internals; you can also use the --pdb option to automatically enter the debugger on such exceptions. If you’re trying to figure out why a scenario is taking many iterations of rejection sampling, first use the --verbosity option to print out the reason for each rejection. If the problem doesn’t become clear, you can use the --pdb-on-reject option to automatically enter the debugger when a scene or simulation is rejected.

If you’re using the Python API instead of invoking Scenic from the command line, these debugging features can be enabled using the following function from the scenic module:

setDebuggingOptions(*, verbosity=0, fullBacktrace=False, debugExceptions=False, debugRejections=False)[source]

Configure Scenic’s debugging options.

Parameters
  • verbosity (int) – Verbosity level. Zero by default, although the command-line interface uses 1 by default. See the --verbosity option for the allowed values.

  • fullBacktrace (bool) – Whether to include Scenic’s innards in backtraces (like the -b command-line option).

  • debugExceptions (bool) – Whether to use pdb for post-mortem debugging of uncaught exceptions (like the --pdb option).

  • debugRejections (bool) – Whether to enter pdb when a scene or simulation is rejected (like the --pdb-on-reject option).

It is possible to put breakpoints into a Scenic program using the Python built-in function breakpoint. Note however that since code in a Scenic program is not always executed the way you might expect (e.g. top-level code is only run once, whereas code in requirements can run every time we generate a sample: see How Scenic is Compiled), some care is needed when interpreting what you see in the debugger. The same consideration applies when adding print statements to a Scenic program. For example, a top-level print(x) will not print out the actual value of x every time a sample is generated: instead, you will get a single print at compile time, showing the Distribution object which represents the distribution of x (and which is bound to x in the Python namespace used internally for the Scenic module).

Building the Documentation

Scenic’s documentation is built using Sphinx. The freestanding documentation pages (like this one) are found under the docs folder, written in the reStructuredText format. The detailed documentation of Scenic’s internal classes, functions, etc. is largely auto-generated from their docstrings, which are written in a variant of Google’s style understood by the Napoleon Sphinx extension (see the docstring of Scenario.generate for a simple example: click the [source] link to the right of the function signature to see the code).

If you modify the documentation, you should build a copy of it locally to make sure everything looks good before you push your changes to GitHub (where they will be picked up automatically by ReadTheDocs). To compile the documentation, enter the docs folder and run make html. The output will be placed in the docs/_build/html folder, so the root page will be at docs/_build/html/index.html. If your changes do not appear, it’s possible that Sphinx has not detected them; you can run make clean to delete all the files from the last compilation and start from a clean slate.

Scenic extends Sphinx in a number of ways to improve the presentation of Scenic code and add various useful features: see docs/conf.py for full details. Some of the most commonly-used features are:

  • a scenic role which extends the standard Sphinx samp role with Scenic syntax highlighting;

  • a sampref role which makes a cross-reference like keyword but allows emphasizing variables like samp;

  • the term role for glossary terms is extended so that the cross-reference will work even if the link is plural but the glossary entry is singular or vice versa.

Scenic Internals

This section of the documentation describes the implementation of Scenic. Much of this information will probably only be useful for people who need to make some change to the language (e.g. adding a new type of distribution). However, the detailed documentation on Scenic’s abstract application domains (in scenic.domains) and simulator interfaces (in scenic.simulators) may be of interest to people using those features.

Scenic Modules

Detailed documentation on Scenic’s components is organized by the submodules of the main scenic module:

scenic.core

Scenic's core types and associated support code.

scenic.domains

General scenario domains used across simulators.

scenic.formats

Support for file formats not specific to particular simulators.

scenic.simulators

World models and interfaces for particular simulators.

scenic.syntax

The Scenic compiler and associated support code.

The scenic module itself provides the top-level API for using Scenic: see Using Scenic Programmatically.

How Scenic is Compiled

Note

This section describes the current compiler, which goes through various convolutions in order to make use of the Python parser as much as possible. This has the advantage of allowing almost all Python constructs to be used in Scenic, but the disadvantage of sometimes producing cryptic error messages. We are in the process of writing a native parser for Scenic which will replace Phases 1-4 below.

The process of compiling a Scenic program into a Scenario object can be split into several phases. Understanding what each phase does is useful if you plan to modify the Scenic language.

Phase 1: Import Handling

In this phase the program is segmented into blocks ending with import statements, which will then be compiled separately. This is done so that if a Scenic module defining new Scenic classes is imported, subsequent parsing can properly detect instantiations of those classes. For more details on the various rules for import statements in Scenic, see import.

Phase 2: Token Translation

In this phase the Scenic program is tokenized using the Python tokenizer. Various Scenic constructs yield sequences of tokens which are meaningless to the Python parser, which we will use to parse the code in the next phase. To remedy this, all Scenic-specific token sequences are translated into new sequences that will pass through the Python parser if they are syntactically-valid Scenic.

Phase 3: Python Parser

In this phase we parse the token stream resulting from Phase 2 using the Python parser. If the tokens do not parse properly, they come from a malformed Scenic program, and an error will be raised here. Unfortunately, since the tokens being parsed are not the original Scenic tokens and likely not semantically-valid Python either, the error messages can be confusing.

Phase 4: Python AST Modification

In this phase we walk the AST generated in Phase 3 to transform it into a Python AST with equivalent semantics to the original Scenic program. This undoes the translation performed in Phase 2 to get syntactically-valid Python, restoring the intended semantics of the program.

Phase 5: AST Compilation

Compile the Python AST down to a Python code object.

Phase 6: Python Execution

In this phase the Python code object compiled in Phase 5 is executed. When run, the definitions of objects, global parameters, requirements, behaviors, etc. produce Python data structures used internally by Scenic to keep track of the distributions, functions, coroutines, etc. used in their definitions. For example, a random value will evaluate to a Distribution object storing information about which distribution it is drawn from; actually sampling from that distribution will not occur until after the compilation process (when calling Scenario.generate). A require statement will likewise produce a closure which can be used at sampling time to check whether its condition is satisfied or not.

Note that since this phase only happens once, at compile time and not sampling time, top-level code in a Scenic program 1 is only executed once even when sampling many scenes from it. This is done deliberately, in order to generate a static representation of the semantics of the Scenic program which can be used for sampling without needing to re-run the entire program.

Phase 7: Scenario Construction

In this phase the various pieces of the internal representation of the program resulting from Phase 6 are bundled into a Scenario object and returned to the user. This phase is also where the program is analyzed and pruning techniques applied to optimize the scenario for later sampling.

Sampling and Executing Scenarios

Sampling scenes and executing dynamic simulations from them are not part of the compilation process 2. For documentation on how those are done, see Scenario.generate and scenic.core.simulators respectively.

1

As compared to code inside a require statement or a dynamic behavior, which will execute every time a scene is sampled or a simulation is run respectively.

2

Although there are some syntax errors which are currently not detected until those stages.

Scenic Libraries

One of the strengths of Scenic is its ability to reuse functions, classes, and behaviors across many scenarios, simplifying the process of writing complex scenarios. This page describes the libraries built into Scenic to facilitate scenario writing by end users.

Simulator Interfaces

Many of the simulator interfaces provide utility functions which are useful when writing scenarios for particular simulators. See the documentation for each simulator on the Supported Simulators page, as well as the corresponding module under scenic.simulators.

Abstract Domains

To enable cross-platform scenarios which are not specific to one simulator, Scenic defines abstract domains which provide APIs for particular application domains like driving scenarios. An abstract domain defines a protocol which can be implemented by various simulator interfaces so that scenarios written for that domain can be executed in those simulators. For example, a scenario written for our driving domain can be run in both LGSVL and CARLA.

A domain provides a Scenic world model which defines Scenic classes for the various types of objects that occur in its scenarios. The model also provides a simulator-agnostic way to access the geometry of the simulated world, by defining regions, vector fields, and other objects as appropriate (for example, the driving domain provides a Network class abstracting a road network). For domains which support dynamic scenarios, the model will also define a set of simulator-agnostic actions for dynamic agents to use.

Driving Domain

The driving domain, scenic.domains.driving, is designed to support scenarios taking place on or near roads. It defines generic classes for cars and pedestrians, and provides a representation of a road network that can be loaded from standard map formats (e.g. OpenDRIVE). The domain supports dynamic scenarios, providing actions for agents which can drive and walk as well as implementations of common behaviors like lane following and collision avoidance. See the documentation of the scenic.domains.driving module for further details.

Supported Simulators

Scenic is designed to be easily interfaced to any simulator (see Interfacing to New Simulators). On this page we list interfaces that we and others have developed; if you have a new interface, let us know and we’ll list it here!

Built-in Newtonian Simulator

To enable debugging of dynamic scenarios without having to install an external simulator, Scenic includes a simple Newtonian physics simulator. The simulator supports scenarios written using the cross-platform Driving Domain, and can render top-down views showing the positions of objects relative to the road network. See the documentation of the scenic.simulators.newtonian module for details.

CARLA

Our interface to the CARLA simulator enables using Scenic to describe autonomous driving scenarios. The interface supports dynamic scenarios written using the CARLA world model (scenic.simulators.carla.model) as well as scenarios using the cross-platform Driving Domain. To use the interface, please follow these instructions:

  1. Install the latest version of CARLA (we’ve tested versions 0.9.9 through 0.9.13) from the CARLA Release Page. Note that CARLA currently only supports Linux and Windows.

  2. Install Scenic in your Python virtual environment as instructed in Getting Started with Scenic.

  3. Within the same virtual environment, install CARLA’s Python API. For CARLA 0.9.12 and onward, you can simply run pip install carla (unless you built CARLA from source; see detailed instructions here).

Note

For older versions of CARLA, you’ll need to install its Python API from the .egg by executing the following command:

$ easy_install /PATH_TO_CARLA_FOLDER/PythonAPI/carla/dist/carla-0.9.9-py3.7-linux-x86_64.egg

The exact name of the .egg file may vary depending on the version of CARLA you installed; make sure to use the file for Python 3, not 2. You may get an error message saying Could not find suitable distribution, which you can ignore. Instead, check that the carla package was correctly installed by running pip show carla.

To start CARLA, run the command ./CarlaUE4.sh in your CARLA folder. Once CARLA is running, you can run dynamic Scenic scenarios following the instructions in the dynamics tutorial.

Grand Theft Auto V

The interface to Grand Theft Auto V, used in our PLDI paper, allows Scenic to position cars within the game as well as to control the time of day and weather conditions. Many examples using the interface (including all scenarios from the paper) can be found in examples/gta. See the paper and scenic.simulators.gta for documentation.

Importing scenes into GTA V and capturing rendered images requires a GTA V plugin, which you can find here.

LGSVL

We have developed an interface to the LGSVL simulator for autonomous driving, used in our ITSC 2020 paper. The interface supports dynamic scenarios written using the LGSVL world model (scenic.simulators.lgsvl.model) as well as scenarios using the cross-platform Driving Domain.

To use the interface, first install the simulator from the LGSVL Simulator website. Then, within the Python virtual environment where you installed Scenic, install LGSVL’s Python API package from source.

An example of how to run a dynamic Scenic scenario in LGSVL is given in Dynamic Scenarios.

Webots

We have several interfaces to the Webots robotics simulator, for different use cases. Our main interface provides a generic world model that can be used with any Webots world and supports dynamic scenarios. See the examples/webots folder for example Scenic scenarios and Webots worlds using this interface, and scenic.simulators.webots for documentation.

Scenic also includes several more specialized world models for use with Webots:

Note

The last two models above, and the example .wbt files for them, were written for the R2018 version of Webots. Relatively minor changes would be required to make them work with the newer open source versions of Webots. We may get around to porting them eventually; we’d also gladly accept a pull request!

X-Plane

Our interface to the X-Plane flight simulator enables using Scenic to describe aircraft taxiing scenarios. This interface is part of the VerifAI toolkit; documentation and examples can be found in the VerifAI repository.

Interfacing to New Simulators

To interface Scenic to a new simulator, there are two steps: using the Scenic API to compile scenarios, generate scenes, and orchestrate dynamic simulations, and writing a Scenic library defining the virtual world provided by the simulator.

Using the Scenic API

Scenic’s Python API is covered in more detail in our Using Scenic Programmatically page; we summarize the main steps here.

Compiling a Scenic scenario is easy: just call the scenic.scenarioFromFile function with the path to a Scenic file (there’s also a variant scenic.scenarioFromString which works on strings). This returns a Scenario object representing the scenario; to sample a scene from it, call its generate method. Scenes are represented by Scene objects, from which you can extract the objects and their properties as well as the values of the global parameters (see the Scene documentation for details).

Supporting dynamic scenarios requires additionally implementing a subclass of Simulator which communicates periodically with your simulator to implement the actions taken by dynamic agents and read back the state of the simulation. See the scenic.simulators.carla.simulator and scenic.simulators.lgsvl.simulator modules for examples.

Defining a World Model

To make writing scenarios for your simulator easier, you should write a Scenic library specifying all the relevant information about the simulated world. This world model could include:

  • Scenic classes (subclasses of Object) corresponding to types of objects in the simulator;

  • instances of Region corresponding to locations of interest (e.g. one for each road);

  • a workspace specifying legal locations for objects (and optionally providing methods for schematically rendering scenes);

  • a set of actions which can be taken by dynamic agents during simulations;

  • any other information or utility functions that might be useful in scenarios.

Then any Scenic programs for your simulator can import this world model and make use of the information within.

Each of the simulators natively supported by Scenic has a corresponding :file:model.scenic file containing its world model. See the Supported Simulators page for links to the module under scenic.simulators for each simulator, where the world model can be found. The scenic.simulators.webots.mars model is particularly simple and would be a good place to start. For a more complex example, see the scenic.simulators.lgsvl model, which specializes the simulator-agnostic model provided by the Driving Domain (in scenic.domains.driving.model).

What’s New in Scenic

This page describes what new features have been added in each version of Scenic, as well as any syntax changes which break backwards compatibility. Scenic uses semantic versioning, so a program written for Scenic 2.1 should also work in Scenic 2.5, but not necessarily in Scenic 3.0. You can run scenic --version to see which version of Scenic you are using.

Scenic 2.x

The Scenic 2.x series is a major new version of Scenic which adds native support for dynamic scenarios, scenario composition, and more.

Scenic 2.1.0

Major new features:

  • Modular scenarios and ways to compose them together, introduced as a prototype in 2.0.0, are now finalized, with many fixes and improvements. See Composing Scenarios for an overview of the new syntax.

  • The record statement for recording values at every step of dynamic simulations (or only at the start/end).

  • A built-in Newtonian physics simulator for debugging dynamic scenarios without having to install an external simulator (see scenic.simulators.newtonian).

  • The interface to the Webots simulator has been greatly generalized, and now supports dynamic scenarios (see scenic.simulators.webots).

Minor new features:

  • You can now write require expr as name to give a name to a requirement; similarly for require always, termination conditions, etc.

  • Compatibility with Python 3.7 is restored. Scenic now supports all versions of Python from 3.7 to 3.11.

Scenic 2.0.0

Backwards-incompatible syntax changes:

  • The interval notation (low, high) for uniform distributions has been removed: use Range(low, high) instead. As a result of this change, the usual Python syntax for tuples is now legal in Scenic.

  • The height property of Object, measuring its extent along the Y axis, has been renamed length to better match its intended use. The name height will be used again in a future version of Scenic with native support for 3D geometry.

Major new features:

  • Scenic now supports writing and executing dynamic scenarios, where agents take actions over time according to behaviors specified in Scenic. See Dynamic Scenarios for an overview of the new syntax.

  • An abstract Driving Domain allowing traffic scenarios to be written in a platform-agnostic way and executed in multiple simulators (in particular, both CARLA and LGSVL). This library includes functionality to parse road networks from standard formats (currently OpenDRIVE) and expose information about them for use in Scenic scenarios.

  • A much generalized and improved interface to CARLA. (Many thanks to the CARLA team for contributing this.)

  • An interface to the LGSVL driving simulator. (Many thanks to the LG team for helping develop this interface.)

Minor new features:

  • Operators and specifiers which take vectors as arguments will now accept tuples and lists of length 2; for example, you can write Object at (1, 2). The old syntax Object at 1@2 is still supported.

  • The model statement allows a scenario to specify which world model it uses, while being possible to override from the command line with the --model option.

  • Global parameters can be overridden from the command line using the --param option (e.g. to specify a different map to use for a scenario).

  • The unpacking operator * can now be used with Uniform to select a random element of a random list/tuple (e.g. lane = Uniform(*network.lanes); sec = Uniform(*lane.sections)).

  • The Python built-in function filter is now supported, and can be used along with unpacking as above to select a random element of a random list satisfying a given condition (see filter for an example).

(Many other minor features didn’t make it into this list.)

Publications Using Scenic

Main Papers

The main paper on Scenic 2.x is:

Scenic: A Language for Scenario Specification and Data Generation.
Fremont, Kim, Dreossi, Ghosh, Yue, Sangiovanni-Vincentelli, and Seshia.
Machine Learning, 2022. [available here]

Our journal paper extends the earlier conference paper on Scenic 1.0:

Scenic: A Language for Scenario Specification and Scene Generation.
Fremont, Dreossi, Ghosh, Yue, Sangiovanni-Vincentelli, and Seshia.
PLDI 2019. [full version]

An expanded version of this paper appears as Chapters 5 and 8 of this thesis:

Algorithmic Improvisation. [thesis]
Daniel J. Fremont.
Ph.D. dissertation, 2019 (University of California, Berkeley; Group in Logic and the Methodology of Science).

Scenic is also integrated into the VerifAI toolkit, which is described in another paper:

VerifAI: A Toolkit for the Formal Design and Analysis of Artificial Intelligence-Based Systems.
Dreossi*, Fremont*, Ghosh*, Kim, Ravanbakhsh, Vazquez-Chanlatte, and Seshia.

* Equal contribution.

Case Studies

We have also used Scenic in several industrial case studies:

Formal Analysis and Redesign of a Neural Network-Based Aircraft Taxiing System with VerifAI.
Fremont, Chiu, Margineantu, Osipychev, and Seshia.
Formal Scenario-Based Testing of Autonomous Vehicles: From Simulation to the Real World.
Fremont, Kim, Pant, Seshia, Acharya, Bruso, Wells, Lemke, Lu, and Mehta.

Other Papers Building on Scenic

A Programmatic and Semantic Approach to Explaining and Debugging Neural Network Based Object Detectors.
Kim, Gopinath, Pasareanu, and Seshia.

Credits

If you use Scenic, we request that you cite our 2022 journal paper and/or our original PLDI 2019 paper.

Scenic is primarily maintained by Daniel J. Fremont.

The Scenic project was started at UC Berkeley in Sanjit Seshia’s research group.

The language was initially developed by Daniel J. Fremont, Tommaso Dreossi, Shromona Ghosh, Xiangyu Yue, Alberto L. Sangiovanni-Vincentelli, and Sanjit A. Seshia.

Edward Kim assisted in developing the library for dynamic driving scenarios and putting together this documentation.

The Scenic tool and example scenarios have benefitted from additional code contributions from:

  • Johnathan Chiu

  • Greg Crow

  • Francis Indaheng

  • Ellen Kalvan

  • Martin Jansa (LG Electronics, Inc.)

  • Kevin Li

  • Guillermo López

  • Shalin Mehta

  • Joel Moriana

  • Gaurav Rao

  • Matthew Rhea

  • Ameesh Shah

  • Jay Shenoy

  • Eric Vin

  • Kesav Viswanadha

  • Wilson Wu

Finally, many other people provided helpful advice and discussions, including:

  • Ankush Desai

  • Alastair Donaldson

  • Andrew Gordon

  • Steve Lemke

  • Jonathan Ragan-Kelley

  • Sriram Rajamani

  • German Ros

  • Marcell Vazquez-Chanlatte

Indices and Tables

License

Scenic is distributed under the 3-Clause BSD License.