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