Scenic Scenario Description Language

repository·main·Indexed 18 days ago

https://github.com/berkeleylearnverify/scenic

A domain-specific probabilistic programming language and compiler used for modeling cyber-physical system environments and generating scenarios. Scenic supports various simulators including CARLA, Webots, and GTA V, providing tools for scenario generation, sampling (including VerifAI Halton sampler), and environment modeling for autonomous vehicle and robotics testing.

Tokens
60K
Snippets
188
Records
281
Agent score
61%

What's inside Scenic

  1. Overview of NHTSA Behavior Prediction Scenarios

    main

    This library contains Scenic programs designed for the CARLA simulator, modeled after NHTSA's Pre-Crash Scenarios. The scenarios are categorized into three main domains:

    Intersection Scenarios

    Focuses on ego vehicle maneuvers (straight, left turn, right turn) at 3-way or 4-way intersections and the required responses (sudden stops, yielding) to avoid collisions with adversary vehicles making turns or going straight.

    Bypassing Scenarios

    Focuses on lane change maneuvers, including bypassing slow adversary vehicles, returning to original lanes, and handling scenarios where the adversary accelerates, preventing a lane return.

    Pedestrian Scenarios

    Focuses on ego vehicle interactions with pedestrians, such as sudden stops to avoid unexpected crossings or yielding to pedestrians in crosswalks during turns or straight movements.

  2. Overview of Scenic

    main
    Scenic is a domain-specific probabilistic programming language designed for modeling the environments of cyber-physical systems. It functions as both a compiler and a scenario generator. Developers use Scenic to create complex, probabilistic models of environments that can be used for testing and verifying cyber-physical systems.
  3. Primitive Data Types in Scenic

    main

    Scenic uses several primitive data types to represent different aspects of a simulation environment:

    • Booleans: Truth values.
    • Scalars: Floating-point numbers representing distances, angles, etc.
    • Vectors: Positions and offsets in space.
    • Headings: 2D orientations in the XY plane.
    • Orientations: 3D orientations in space.
    • Vector Fields: Associations of an orientation to each point in space.
    • Regions: Sets of points in space.
    • Shapes: Shapes (regions modulo similarity).
    • Sensors: Sensors mounted on objects.
  4. Manage Specifier Priorities

    main

    When multiple specifiers attempt to set the same property, Scenic resolves conflicts using a priority system. Priorities are represented by positive integers:

    • Priority 1: Highest priority (cannot be overridden by other specifiers).
    • Higher Integers (e.g., 2, 3): Lower priority (can be overridden by specifiers with smaller integer values).

    Example Priority Chain:

    1. new Object ahead of plane by 100 sets parentOrientation with priority 3 (aligned with plane).
    2. new Object ahead of plane by 100, on ground sets parentOrientation with priority 2 (overrides plane, aligns with ground).
    3. new Object ahead of plane by 100, on ground, with parentOrientation (0, 90 deg, 0) sets parentOrientation with priority 1 (overrides everything with an explicit value).
  5. Define dynamic agents and behaviors

    main

    In Scenic, objects that take actions over time are called dynamic agents. You define their dynamic behavior using the behavior property.

    A behavior is a function that runs over the course of a scenario, periodically issuing actions (instantaneous operations like setting throttle or steering). Scenic uses discrete time steps; at each step, the behavior function specifies zero or more actions.

    To implement a behavior, use a loop (often while True) and the take keyword to issue actions. The take statement causes the behavior to pause until the next simulation time step.

    model scenic.domains.driving.model
    new Car with behavior FollowLaneBehavior
    
    behavior FollowLaneBehavior():
        while True:
            throttle, steering = ...    # compute controls
            take SetThrottleAction(throttle), SetSteerAction(steering)
  6. How Scenic generates scenes via rejection sampling

    main

    Scenic generates scenes by sampling from the probability distribution defined in your program using rejection sampling.

    The Process

    1. Requirement Selection: The tool decides which user-defined requirements to enforce (noting that soft requirements are only required with a certain probability).
    2. External Parameter Sampling: The tool invokes an external sampler to determine values for any external parameters.
    3. Distribution Sampling: The tool samples values for all Distribution objects (expressions with random values) defined in the scene.
    4. Requirement Validation: The tool checks if the sampled values satisfy both built-in and user-defined requirements. If they do not, the sample is rejected, and the process repeats from step 2.

    Performance and Constraints

    • Pruning: To avoid the inefficiency of pure rejection sampling, Scenic uses compilation-time "pruning" techniques to exclude parts of the scene space that are guaranteed to violate requirements.
    • Infinite Loops: If requirements are impossible to satisfy, rejection sampling could theoretically run forever. However, the Scenario.generate function imposes a finite limit on the number of iterations by default to prevent this.
  7. Understand the Scenic compilation lifecycle

    main

    The process of converting a Scenic program into a Scenario object follows a five-phase pipeline. Understanding this lifecycle is essential if you are modifying the Scenic language or debugging how Scenic constructs are translated into executable Python.

    1. Scenic Parser: Parses the program using a PEG grammar (scenic.gram) to generate a Scenic Abstract Syntax Tree (AST). This AST is a superset of the standard Python AST.
    2. Scenic Compiler: Transforms the Scenic AST into a Python AST by replacing Scenic-specific nodes with Python equivalents.
    3. AST Compilation: Compiles the resulting Python AST into a Python code object.
    4. Python Execution: Executes the code object. This phase populates internal Scenic data structures (e.g., Distribution objects, closures for require statements). Crucially, top-level code is executed only once during this phase, even if you sample many scenes later.
    5. Scenario Construction: Bundles the internal representations into a Scenario object and applies optimization/pruning techniques.

    Note that sampling (via Scenario.generate) and simulation are separate from this compilation process.

  8. How the Scenic Parser and Compiler work

    main

    Scenic operates through a two-stage process to convert Scenic source code into executable Python:

    1. Parsing: The Scenic Parser takes source code and produces a Scenic AST (Abstract Syntax Tree). This AST is a superset of the Python AST and includes specific nodes for Scenic language constructs defined in scenic.syntax.ast.
    2. Compilation: The Scenic Compiler (a subclass of ast.NodeTransformer) traverses the Scenic AST and transforms each Scenic-specific node into a corresponding Python AST node. This resulting Python AST can then be executed by the standard Python interpreter.
  9. Understand Scenic Specifiers and Property Dependencies

    main

    Scenic uses a unique specifier syntax (e.g., :specifier:left of {X}``) instead of traditional object-oriented constructors. This allows for natural language-like descriptions of object properties and handles complex property dependencies automatically.

    Key Concepts:

    • Property Dependencies: Specifiers can depend on other properties. For example, placing an object left of curb depends on the object's width, which in turn might depend on its model. Scenic automatically calculates the correct evaluation order.
    • Implicit Dependencies: When using relative operators like {X} relative to {Y}, Scenic automatically uses the object's current position as the reference point if the expression depends on a position.
    • Modifying Specifiers: Some specifiers can modify an already-specified property. For example, on {region} can act as a projection: if a position is already set, on ground will project that position onto the ground region.
    new Car left of curb by 0.5, 
        with model CarModel.models['BUS']
    
    # Using relative orientation with implicit position dependency
    new Car left of curb by 0.5, 
        facing Range(-5, 5) deg relative to roadDirection
    
    # Using a modifying specifier to project position
    new Object ahead of plane by 100, on ground
  10. Understand the Point, OrientedPoint, and Object class hierarchy

    main

    Scenic uses a hierarchical class structure to represent spatial entities:

    1. Point: Represents a location in space. It provides the fundamental position property and visibleRegion.
    2. OrientedPoint: Subclasses Point. It adds an orientation property, defining a local coordinate system at that location.
    3. Object: Subclasses OrientedPoint. It represents a physical entity with dimensions and dynamic properties. It adds:
      • Dimensions: width, length, and height.
      • Geometry: shape.
      • Constraints: allowCollisions, requireVisible, and regionContainedIn.
      • Dynamics: behavior, speed, and velocity.

    Use Point for simple coordinates, OrientedPoint when direction matters, and Object for physical entities that occupy space and interact with the environment.

  11. Use OrientedPoint for local coordinate systems

    main

    An OrientedPoint can be used as a virtual anchor to establish a local coordinate system for placing other objects. This is useful when you want to place objects relative to a specific heading or orientation.

    When placing objects relative to an OrientedPoint, you can use specifiers like left of, right of, ahead of, or behind combined with a facing specifier to define their orientation relative to the anchor's heading.

    Example of creating a bottleneck using an OrientedPoint:

    bottleneck = new OrientedPoint at ego offset by Range(-1.5, 1.5) @ Range(0.5, 1.5), facing Range(-30, 30) deg
    
    # Define edges relative to the bottleneck's heading
    gap = 1.2 * ego.width
    halfGap = gap / 2
    
    leftEdge = new OrientedPoint left of bottleneck by halfGap, \
        facing Range(60, 120) deg relative to bottleneck.heading
    rightEdge = new OrientedPoint right of bottleneck by halfGap, \
        facing Range(-120, -60) deg relative to bottleneck.heading
    
    # Place pipes relative to those edges
    new Pipe ahead of leftEdge, with length Range(1, 2), on ground, facing leftEdge, with parentOrientation 0
    new Pipe ahead of rightEdge, with length Range(1, 2), on ground, facing rightEdge, with parentOrientation 0
    bottleneck = new OrientedPoint at ego offset by Range(-1.5, 1.5) @ Range(0.5, 1.5), facing Range(-30, 30) deg
    
    gap = 1.2 * ego.width
    halfGap = gap / 2
    
    leftEdge = new OrientedPoint left of bottleneck by halfGap, \
        facing Range(60, 120) deg relative to bottleneck.heading
    rightEdge = new OrientedPoint right of bottleneck by halfGap, \
        facing Range(-120, -60) deg relative to bottleneck.heading
    
    new Pipe ahead of leftEdge, with length Range(1, 2), on ground, facing leftEdge, with parentOrientation 0
    new Pipe ahead of rightEdge, with length Range(1, 2), on ground, facing rightEdge, with parentOrientation 0