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:

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



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:
Install VerifAI in a virtual environment of your choice.
Activate the virtual environment.
Change directory to your clone of the Scenic repository.
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:

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:
“is at position X” (an absolute position)
“is just left of position X” (a position based on orientation)
“is 3 m West of the taxi” (a relative position)
“is 3 m left of the taxi” (a local coordinate system)
“is one lane left of the taxi” (another local coordinate system)
“appears to be 10 m behind the taxi” (relative to the line of sight)
“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.

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 regionroad
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.

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



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.






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 setego
to one object, build a part of the scene around it, then reassignego
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.

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
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
expressing truth values |
|
representing distances, angles, etc. as floating-point numbers |
|
representing positions and offsets in space |
|
representing orientations in space |
|
associating an orientation (i.e. a heading) to each point in space |
|
representing sets of points in space |
Distributions
uniformly-distributed real number in the interval |
|
uniformly-distributed integer in the (fixed) interval |
|
normal distribution with the given mean and standard deviation |
|
normal distribution truncated to the given window |
|
uniform over a finite set of values |
|
discrete with given values and weights |
|
uniformly-distributed Point in a region |
Statements
Compound Statements
Syntax |
Meaning |
---|---|
Defines a Scenic class. |
|
Defines a dynamic behavior. |
|
Defines a monitor. |
|
Defines a modular scenario. |
|
Run code with interrupts inside a dynamic behavior or modular scenario. |
Simple Statements
Syntax |
Meaning |
---|---|
Select the world model. |
|
Import a Scenic or Python module. |
|
Define global parameters of the scenario. |
|
Define a hard requirement. |
|
Define a soft requirement. |
|
Define a dynamic hard requirement. |
|
Define a termination condition. |
|
Set the scenario to terminate after a given amount of time. |
|
Enable mutation of the given list of objects. |
|
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 the action(s) specified. |
|
Take no actions this time step. |
|
Immediately end the scenario. |
|
Run one or more sub-behaviors/sub-scenarios until they complete. |
|
Run sub-behaviors/scenarios until they complete or a condition is met. |
|
Run sub-behaviors/scenarios for (at most) a specified period of time. |
|
Run one choice of sub-behavior/scenario whose preconditions are satisfied. |
|
Run several sub-behaviors/scenarios in a random order, satisfying preconditions. |
|
Break out of the current |
|
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 |
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 |
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 |
initial velocity (later, instantaneous velocity) |
angularSpeed 1 |
0 |
angular speed (change in heading/time) |
behavior |
dynamic behavior, if any |
|
allowCollisions |
whether collisions are allowed |
|
requireVisible |
whether object must be visible from ego |
|
regionContainedIn |
Region the object must lie within |
|
cameraOffset |
(0, 0) |
position of camera for ‘can see’ |
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.

Illustration of the beyond
, behind
, and offset by
specifiers.
Each OrientedPoint
(e.g. P
) is shown as a bold arrow.
Specifier for |
Meaning |
---|---|
Positions the object at the given global coordinates |
|
Positions the object at the given coordinates in the local coordinate system of ego (which must already be defined) |
|
Positions the object at the given coordinates, in a local coordinate system centered at ego and oriented along the given direction |
|
Positions the object further to the left/right by the given scalar distance |
|
As above, except placing the object ahead of or behind the given position |
|
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 |
|
Positions the object uniformly at random in the visible region of the ego, or of the given Point/OrientedPoint if given |
|
Positions the object uniformly at random in the non-visible region of the ego, or of the given Point/OrientedPoint if given |
Specifier for |
Meaning |
---|---|
Positions the object uniformly at random in the given Region |
|
Positions the object to the left/right of the given OrientedPoint, depending on the object’s width |
|
As above, except positioning the object ahead of or behind the given OrientedPoint, thereby depending on length |
|
Position by following the given vector field for the given distance starting from ego or the given vector |
Specifier for |
Meaning |
---|---|
Orients the object along the given heading in global coordinates |
|
Orients the object along the given vector field at the object’s position |
|
Orients the object toward/away from the given position (thereby depending on the object’s position) |
|
Orients the object so that it has the given heading with respect to the line of sight from ego (or the given vector) |
Operators

Illustration of several operators.
Each OrientedPoint
(e.g. P
) is shown as a bold arrow.
Scalar Operators |
Meaning |
---|---|
The relative heading of the given heading with respect to ego (or the |
|
The apparent heading of the OrientedPoint, with respect to the line of sight from ego (or the given vector) |
|
The distance to the given position from ego (or the |
|
The heading to the given position from ego (or the |
Boolean Operators |
Meaning |
---|---|
Whether or not a position or Object is visible from a Point or OrientedPoint. |
|
Whether a position or Object lies in the region |
Heading Operators |
Meaning |
---|---|
The given heading, interpreted as being in degrees |
|
The heading specified by the vector field at the given position |
|
The first direction, interpreted as an offset relative to the second direction |
Vector Operators |
Meaning |
---|---|
The first vector, interpreted as an offset relative to the second vector (or vice versa) |
|
The second vector, interpreted in a local coordinate system centered at the first vector and oriented along the given direction |
Region Operators |
Meaning |
---|---|
The part of the given region visible from ego |
|
The part of the given region not visible from ego |
OrientedPoint Operators |
Meaning |
---|---|
The given vector, interpreted in the local coordinate system of the OrientedPoint |
|
Equivalent to |
|
The midpoint of the corresponding edge of the bounding box of the Object, oriented along its heading |
|
The corresponding corner of the Object’s bounding box, also oriented along its heading |
Built in Functions
Function |
Description |
---|---|
Various Python functions including |
|
Filter a possibly-random list (allowing limited randomized control flow). |
|
Sample a new value from a distribution. |
|
Convert a relative path to an absolute path, based on the current directory. |
|
Like |
|
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
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.
- 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.
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 (aLineString
orMultiLineString
).- Parameters
points – sequence of points making up the polyline (or
None
if using the polyline argument instead).polyline –
shapely
polyline or collection of polylines (orNone
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
- 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
- __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
- 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 (aPolygon
orMultiPolygon
).- Parameters
points – sequence of points making up the boundary of the polygon (or
None
if using the polygon argument instead).polygon –
shapely
polygon or collection of polygons (orNone
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 thepositionStdDev
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 radiusvisibleDistance
.
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 theheadingStdDev
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 radiusvisibleDistance
, oriented alongheading
and subtending an angle ofviewAngle
.
Object
A physical object. This class subclasses OrientedPoint, adding a variety of properties including:
width
andlength
to define the bounding box of the object;allowCollisions
,requireVisible
, andregionContainedIn
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 toFalse
.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 toTrue
.
- 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 valueTrue
.regionContainedIn (Region or
None
) – A Region the object is required to be contained in. IfNone
, 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’sposition
. 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
andself.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 thecameraOffset
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

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:
If a property is specified non-optionally by mutiple specifiers in S, an ambiguity error is raised.
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.
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.
The dependency graph of the specifiers S is constructed. If it is cyclic, an error is raised.
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

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:
Decide which user-defined requirements will be enforced for this sample (soft requirements have only some probability of being required).
Invoke the external sampler to sample any external parameters.
Sample values for all distributions defined in the scene (all expressions which have random values, represented internally as
Distribution
objects).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:
Execute all currently-running modular scenarios for one time step. Specifically, for each such scenario:
Check if any of its
require always
conditions are violated; if so, reject the simulation.Check which
require eventually
conditions are satisfied; remember these for later.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.If the scenario is not currently running a sub-scenario (with
do
), check its invariants; if any are violated, reject the simulation. 1If 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 usingdo
executeswait
). If the block executes arequire
statement with a false condition, reject the simulation. If it executesterminate
, or finishes executing, go to step (f) below to stop the scenario.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 usingdo
; 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.)
Save the values of all
record
statements, as well asrecord initial
statements if it is time step 0.Run each monitor for one time step (i.e. resume it until it executes
wait
). If it executes arequire
statement with a false condition, reject the simulation. If it executesterminate
, set the termination flag (and continue running any other monitors).If the termination flag is set, any of the
terminate simulation when
conditions are satisfied, or a time limit passed toSimulator.simulate
has been reached, go to step (10) to terminate the simulation.Execute the dynamic behavior of each agent to select its action(s) for the time step. Specifically, for each agent’s behavior:
If the behavior is not currently running a sub-behavior (with
do
), check its invariants; if any are violated, reject the simulation. 1Resume the behavior until it (or a subbehavior it is currently running using
do
) executestake
orwait
. If the behavior executes arequire
statement with a false condition, reject the simulation. If it executesterminate
, go to step (10) to terminate the simulation. Otherwise, save the (possibly empty) set of actions specified for the agent to take.
For each agent, execute the actions (if any) its behavior chose in the previous step.
Run the simulator for one time step.
Update every dynamic property of every object to its current value in the simulator.
Increment the simulation clock (the
currentTime
attribute ofSimulation
).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 anyrecord 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 yourPYTHONPATH
(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 toscenic.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 anint
orfloat
, it is; otherwise it is kept as a string.The equivalent of this option for the Python API is the
params
argument toscenic.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 (althoughrandom
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 (seenumpy.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 toscenic.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
andscenic.simulators.lgsvl.model
. If your scenario is written for an abstract domain, likescenic.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
andterminate
).
- --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 thePYTHONWARNINGS
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.
- -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 Sphinxsamp
role with Scenic syntax highlighting;a
sampref
role which makes a cross-reference likekeyword
but allows emphasizing variables likesamp
;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's core types and associated support code. |
|
General scenario domains used across simulators. |
|
Support for file formats not specific to particular simulators. |
|
World models and interfaces for particular simulators. |
|
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!
Supported Simulators:
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:
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.
Install Scenic in your Python virtual environment as instructed in Getting Started with Scenic.
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:
A model for the Mars rover example used in our PLDI paper. This model is extremely simple and might be a good baseline for developing your own model. See the examples in
examples/webots/mars
(including a dynamic version of the scenario from the paper) and the documentation ofscenic.simulators.webots.mars
for details.A general model for traffic scenarios, used in our VerifAI paper. Examples using this model can be found in the VerifAI repository; see also the documentation of
scenic.simulators.webots.road
.A more specific model for traffic scenarios at intersections, using guideways from the Intelligent Intersections Toolkit. See the examples in
examples/webots/guideways
and the documentation ofscenic.simulators.webots.guideways
for details.
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 forrequire 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: useRange(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 renamedlength
to better match its intended use. The nameheight
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 syntaxObject 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 withUniform
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](see also the full version with appendices)
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.[See also this white paper and associated blog post]
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.