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.

Compiling Scenarios and Generating Scenes

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, *, mode2D=False, **kwargs)[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.

  • mode2D (bool) – Whether to compile this scenario in 2D Compatibility Mode.

Returns:

A Scenario object representing the Scenic scenario.

Note for Scenic developers: this function accepts additional keyword arguments which are intended for internal use and debugging only. See _scenarioFromStream for details.

scenarioFromString(string, params={}, model=None, scenario=None, *, filename='<string>', mode2D=False, **kwargs)[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 = new Object with foo Range(0, 5)')
scene, numIterations = scenario.generate()
print(f'ego has foo = {scene.egoObject.foo}')
ego has foo = 2.083099362726706

Running Dynamic Simulations

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',
                                   mode2D=True)
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.

Storing Scenes/Simulations for Later Use

Scene and Simulation objects are heavyweight and not themselves suitable for bulk storage or transmission over a network [2]. However, Scenic provides serialization routines which can encode such objects into relatively short sequences of bytes. Compact encodings are achieved by storing only the sampled values of the primitive random variables in the scenario: all non-random information is obtained from the original Scenic file.

Having compiled a Scenic scenario into a Scenario object, any scenes you generate from the scenario can be encoded as bytes using the Scenario.sceneToBytes method. For example, to save a scene to a file one could use code like the following:

import scenic, tempfile, pathlib
scenario = scenic.scenarioFromFile('examples/gta/parkedCar.scenic', mode2D=True)
scene, _ = scenario.generate()
data = scenario.sceneToBytes(scene)
with open(pathlib.Path(tempfile.gettempdir()) / 'test.scene', 'wb') as f:
        f.write(data)
print(f'ego car position = {scene.egoObject.position}')

Then you could restore the scene in another process, obtaining the same position for the ego car:

import scenic, tempfile, pathlib
scenario = scenic.scenarioFromFile('examples/gta/parkedCar.scenic', mode2D=True)
with open(pathlib.Path(tempfile.gettempdir()) / 'test.scene', 'rb') as f:
        data = f.read()
scene = scenario.sceneFromBytes(data)
print(f'ego car position = {scene.egoObject.position}')

Notice how we need to compile the scenario a second time in order to decode the scene, if the original Scenario object is not available. If you need to send a large number of scenes from one computer to another, for example, it suffices to send the Scenic file for the underlying scenario, plus the encodings of each of the scenes.

You can encode and decode simulations run from a Scenario in a similar way, using the Scenario.simulationToBytes and Scenario.simulationFromBytes methods. One additional concern when replaying a serialized simulation is that if your simulator is not deterministic (or you change the simulator configuration), the original simulation and its replay can diverge, leading to unexpected behavior or exceptions. Scenic can attempt to detect such divergences by saving the exact history of the simulation and comparing it to the replay, but this greatly increases the size of the encoded simulation. See Simulator.simulate for the available options.

Note

The serialization format used for scenes and simulations is suitable for long-term storage (for instance if you want to save all the simulations you’ve run so that you can return to one later for further analysis), but it is not guaranteed to be compatible across major versions of Scenic.

See also

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

Footnotes