First Task:
This image is captured one second into a longer video. The full video has been given the following caption: "[CAPTION]" Refine this caption for the video based on the image. As the caption describes the full video, and as the image is a frame after 1 seconds, the state of the scene might have evolved slightly from the description in the caption. Identify any significant objects in the image, the locations of those objects relative to one another and any interactions between the objects.

Second Task:
For this task you are performing the role of a computer scientist. Your task is to design a `VideoSimulation` class in python which faithfully simulates the rest of the video after this frame. The `VideoSimulation` class fits the starting state of the scene to the frame of the video provided earlier, and is subsequently iterated through to produce the simulation. Any required content which is not visible in that frame must be inferred using heuristics.

Your role is also to determine the appropriate abstraction of this scene, and you should aim to use the minimal abstraction, e.g. 2D or 3D abstraction, which is capable of faithfully representing the dynamics. How the final rendering of the simulation appears (e.g. as a segmentation mask, depth map or image) is up to you, though the output must be a video with frames of shape `[H,W]` or `[H,W,3]` with type `np.uint8`. The aesthetics of the video are not important, although you should not attempt to rescale the scene, but the dynamics must be accurate.

A key principle for this simulation is to distinguish between the core physical system and any external activating agents. Often, a video will feature an agent (e.g., a stick, a robotic grabber, a person's hand) whose only role is to initiate the main action. Your simulation must not model the activating agent itself. Instead, your goal is to model the effect of its interaction with the core system. This typically means identifying the first object that is acted upon and applying an initial action to it to trigger the process.

The `VideoSimulation` must inherit from the `Simulator` base class. The specification of this `Simulator` class is provided below. The `VideoSimulation` class must override three methods of this base class: `fit`, which fits the simulator parameters to the image; `update_simulation`, which steps the simulation forward by a specified timestep; and `render_frame`, which renders a single frame of the simulation. Additionally, you may override the `reset` method to reset the simulation to its initial state, and the `__init__` method. You may also add any necessary methods. This function is called before the start of each new simulation after the initial fitting to the image.

This `Simulator` class is an iterator, and at each call of `__next__`, calls `render_frame`, `update_simulation`, and returns the rendered frame of simulation. This is already implemented. When parameters are designed for use in the simulation, they must be written to the `default_controls` dictionary of the `VideoSimulation` class. This dictionary is used to control the simulation and is then accessible via the `controls` property. This `controls` property is a dictionary which contains `default_controls` as well as any user overrides. The user may override any parameter in `default_controls` by specifying it in the `override_controls` dictionary between fitting and running the simulation. Therefore, `reset` must be written to ensure that the simulation is correctly reset to the initial state as specified by the `controls` property.

```python
from typing import Callable

class Simulator:
    def __init__(self, frame_size=(1024, 576), api: API=None, fps=30):
        # Define state variables
        self.t = 0

        # Provided parameters
        self.frame_size = frame_size
        self.api=api
        self.fps = fps

        self.default_controls = {}
        self.override_controls = {}

    @property
    def controls(self):
        """
        Returns a dictionary of controls which can be used to control the simulator.
        This includes both the `default_controls` (which are derived while fitting the simulator) and override controls (which are used by the user to modify the simulation).
        Default controls are parameters which must be fit to the image or based on heuristics, while override controls are parameters which can be set by the user to override the simulator's state.
        """
        return self.default_controls | self.override_controls

    def render_frame(self):
        """
        Render the next frame of the simulation.
        """
        raise NotImplementedError()

    def update_simulation(self, dt: float):
        """
        Update the simulation by one timestep dt.

        Arguments:
            dt (float)
                Time step to update the simulation by.
        """
        raise NotImplementedError()

    def fit(self, image: np.ndarray):
        """
        Fit the parameters of the scene to the provided image.

        Arguments:
            image (np.ndarray[np.uint8])
                image showing physical process to be simulated. Video has size [H, W, 3] corresponding to the frame height, width, and three color channels.
        """
        raise NotImplementedError()

    def reset(self):
        """
        Reset the simulation to its initial state.
        """
        self.t = 0

    def __next__(self):
        """
        Return the next frame of the simulator generation and updates simulator by one timestep.

        Returns:
            frame (np.ndarray[np.uint8])
                Simulation frame of shape (height, width) or (height, width, 3).
        """
        frame = self.render_frame()             # Renders the scene at time 't'
        self.update_simulation(dt=1/self.fps)   # Advances the simulation to time 't + dt'

        target_height = self.frame_size[1]
        target_width = self.frame_size[0]
        height = frame.shape[0]
        width = frame.shape[1]
        assert (height, width) == (target_height, target_width), f"Expected frame size {target_height}x{target_width}, got {height}x{width}"
        if len(frame.shape) == 3:
            assert frame.shape[2] == 3, "Expected frame to have 3 color channels as it has 3 dimensions"
        else:
            assert len(frame.shape) == 2, f"Expected frame to have 2 dimensions (height, width) or 3 dimensions (height, width, channels). Got {len(frame.shape)} dimensions instead."
        assert frame.dtype == np.uint8

        return frame

    def run_simulation(self, n_frames):
        frames = []
        for _ in range(n_frames):
            frame = next(self)
            frames.append(frame)
        return frames

    def __iter__(self):
        return self

    def __del__(self):
        pass
```

As well as standard python libraries, the libraries `numpy`, `scipy`, `scikit-image`, `trimesh`, `pybullet` which steps at a default 240 fps, `pygame`, and `pymunk` are available. Recall that `pybullet` is not the same as `bullet`, and does not have several functions. If you do make the choice to use pybullet, you should take into account that:

- `p.applyExternalImpulse`, `p.getEulerFromMatrix`, `p.getQuaternionFromMatrix`, `p.GEOM_CONVEX_HULL` are not available.
- `p.addUserDebugPoints` is not available in headless `p.DIRECT` mode. 
- Pybullet has no alpha blending. Shapes must either be opaque or fully transparent.

A set of special functions are available to you via an API. These helper functions are available should you need them to produce your simulator. Then can be accessed via the `self.api: API` property of the `Simulator` class. The specifications for these special functions are as follows:

```python
class API:
    def segment(self, image: np.ndarray, objects: List[str]) -> dict[str, np.ndarray]:
        """
        An open-vocabulary segmentation method. All objects should be passed in a single call to the API.

        Arguments:
            image (np.ndarray)
                Image of shape `[h, w, 3]`. Dtype of `np.uint8`
            objects (List[str])
                List of the names of objects to be segmented.

        Returns:
            segmentation (dict[str, np.ndarray])
                A dictionary whose keys are the objects passed in the objects parameter.
                Each value of the dictionary is an `np.ndarray` of dtype `bool` corresponding to a distinct object instance found in the image.
                Each value has a shape of `[N, h, w]`, representing the binary segmentation mask for any `N` instances of that single object.

        **Best Practice Example:**
        # For best results, group all objects into one call. E.g. to segment bananas and apples in an image:

        segmentations = api.segment(image, ["banana", "apple"])
        banana_masks = segmentations["banana"]  # Shape [N_bananas, h, w]
        apple_masks = segmentations["apple"]    # Shape [N_apples, h, w]
        if np.any(banana_masks):
            first_banana_mask = banana_masks[0]  # Binary mask of the first banana
        """

    def pts3d(self, image: np.ndarray) -> np.ndarray:
        """
        An estimator of the 3d location of each pixel in the coordinate system of the camera for the image.

        Arguments:
            image (np.ndarray)
                Image of shape `[h, w, 3]`. Dtype of `np.uint8`

        Returns:
            pts3d (np.ndarray)
                Shape `[h, w, 3]`. Dtype of `np.float32`.
                3D Point map of frame. Points are represented so that the mean distance of points from the camera is 1.0 units.
                The points are in the camera coordinate system with the OpenGL convention right-handed coordinate system. +X points to the right of the image, +Y points to the top of the image, -Z points forward.

        """

    def clean_point_cloud(self, data: np.ndarray) -> np.ndarray:
        """
        Clean a point cloud by removing outliers.
        Arguments:
            data (np.ndarray)
                Point cloud of shape `[k, 3]`. Dtype of `np.float32`
        Returns:
            cleaned_data (np.ndarray)
                Cleaned point cloud of shape `[k_, 3]` where k_<k. Dtype of `np.float32`
                Outliers are defined as points that are more than 2 standard deviations away from the mean in any dimension.
        """


    def intrinsics(self, image: np.ndarray) -> np.ndarray:
        """
        Estimate intrinsic parameters of camera for image.

        Arguments:
            image (np.ndarray)
                Image of shape `[h, w, 3]`. Dtype of `np.uint8`

        Returns:
            intrinsics (np.ndarray)
                Matrix of camera intrinsics. [[fx, 0, cx],[0, fy, cy], [0, 0, 1]]. All parameters given in pixels.

        """

    def predict_ground_plane(self, points: np.ndarray) -> tuple:
        """
        Predicts the ground plane from a point cloud.

        Arguments:
            points (np.ndarray)
                Point map of shape (N, 3) representing the point cloud.

        Returns:
            tuple of:
                - best_plane_model (tuple): A tuple (normal, point) representing the best-fit plane, with the normal pointing up from the surface.
                                        `normal` is a numpy array of shape (3,).
                                        `point` is a numpy array on the plane of shape (3,).
                - best_inliers (np.ndarray): A boolean array of shape (N,) indicating the inlier points.

        **Best Practice Example:**
        # For best results, first segment the ground surface (e.g., a "table" or "floor")
        # and pass its corresponding 3D points to this function.

        table_mask = api.segment(image, ["table"])["table"][0]
        table_points = api.pts3d(image)[table_mask]
        ground_plane, inliers = api.predict_ground_plane(table_points)
        """

    def ground_plane_to_w2c(self, ground_plane: tuple) -> np.ndarray:
        """
        Converts a ground plane model to a world-to-camera transformation matrix.
        The calculation places the plane point at the origin and aligns the plane with the global x-y plane with the positive z axis pointing in the direction of the plane normal.

        Arguments:
            ground_plane (tuple)
                A tuple (normal, point) representing the ground plane.
                    `normal` is a numpy array of shape (3,) representing the plane normal.
                    `point` is a numpy array of shape (3,) representing a point on the plane.

        Returns:
            w2c (np.ndarray)
                An transformation matrix of shape (4,4) that transforms points from world coordinates to camera coordinates.
                In world coordinates, the plane lies along the x-y axis and the positive z axis points in the direction of the plane normal.
        """

    def view_proj_matrices(self, w2c: np.ndarray, intrinsics: np.ndarray, frame_size: tuple) -> tuple:
        """
        Computes the view and projection matrices for rendering.

        Arguments:
            w2c (np.ndarray)
                World-to-camera transformation matrix of shape (4,4).
            intrinsics (np.ndarray)
                Camera intrinsic matrix of shape (3,3).
            frame_size (tuple)
                The size of the frame as (width, height).
                Height and width are both integers.

        Returns:
            tuple of:
                View matrix (np.ndarray)
                    Matrix for rendering with OpenGL conventions. Shape (16,)
                Projection matrix (np.ndarray)
                    A projection matrix for rendering with OpenGL conventions. Shape (16,)
        """

    def fit_3d_shape(self, point_cloud: np.ndarray, shape_class: str, axis: np.ndarray = None) -> dict:
        """
        Fit a 3D shape to a provded point cloud.

        Arguments:
            point_cloud (np.ndarray)
                An Nx3 array of points in 3D space.
            shape_class (str)
                The type of shape to fit ('cuboid', 'sphere', 'plane', 'cylinder').
            axis (np.ndarray, optional)
                For 'cylinder' shape_class, a 3D vector indicating the cylinder's axis direction must be provided.
                For other shape classes, this parameter is ignored.

        Returns:
            Fitted parameters (dict)
                The structure of the dictionary depends on the shape class:
                    - 'cuboid': {'center': [x, y, z], 'size': [s_x, s_y, s_z], 'rotation': [x, y, z, w]}
                    - 'sphere': {'center': [x, y, z], 'radius': r}
                    - 'plane': {'normal': [nx, ny, nz], 'd': d}
                    - 'cylinder': {'center': [x, y, z], 'radius': r, 'height': h, 'axis': [ax, ay, az]}
                All returned sequences of numbers, e.g. [x, y, z] are numpy arrays.
                All sizes are full lengths, not half-lengths.
                All rotations are quaternions in (x, y, z, w) format.
                Where sizes are given, they are arranged from largest to smallest so that s_x >= s_y >= s_z.
        """

    def generate_surface_mesh(self, pointmap: np.ndarray, mask: np.ndarray, downsample_verts: int = 1000) -> tuple:
        """
        Generates a triangular mesh from a grid of 3D points based on a mask.

        This function triangulates a structured point cloud, connecting adjacent valid points to form a surface mesh.

        Note: If the pointmap is derived from an image pointmap, the resulting mesh represents only the visible 'front-facing' surface. It will be a 2.5D shell without thickness or a closed volume. Consequently, such open meshes are not necessarily physically balanced.

        Arguments:
            pointmap (np.ndarray)
                Array of shape [h, w, 3] representing a grid of 3D coordinates.
            mask (np.ndarray)
                Array of shape [h,w] of dtype bool. `True` indicates
                a point to include in the mesh.
            downsample_verts (int)
                If the generated mesh has more than this number of vertices, it will be downsampled to this number.

        Returns:
            tuple
                vertices (np.ndarray)
                    Array of shape [3 * k] of dtype float32 representing the vertex coordinates of the mesh.
                indices (np.ndarray)
                    Array of shape [3 * m] of dtype int32 representing the triangular faces of the mesh as indices into the vertices array.
        """

    def create_new_particles(self, source_position: np.ndarray, flow_rate: int, particle_radius: float, particle_mass: float, particle_color: List[float], initial_velocity: np.ndarray, max_particles: int, num_existing_particles: int, noise_std: float = 0.0, physics_client_id: int = 0, particle_col_shape: Optional[int] = None, particle_vis_shape: Optional[int] = None) -> List[int]:
        """
        Creates new particles for a particle-based PyBullet simulation.
        Note that a robust selection method should be used to determine the source position, for example if it is known that the fluid will flow into a recepticle or onto an obejct, the source position should be above that recepticle or object. Do not try to derive the source position by segmenting a fluid flow in an image, as this will not be robust.
        Note also that pre-existing reservoirs of fluid should not be modelled with particles due to computational constraints. These should be modelled with meshes instead.

        Arguments:
            source_position: 3D position (np.ndarray).
            flow_rate: Number of particles to create in this call. Must be no greater than 15.
            particle_radius: Radius of each particle.
            particle_mass: Mass of each particle. Can be negative for buoyant particles.
            particle_color: RGBA color for visualization.
            initial_velocity: Initial velocity vector for each particle.
            max_particles: Maximum allowed particles in the simulation.
            num_existing_particles: The current count of particles in the simulation.
            noise_std: Standard deviation for Gaussian noise added to source position.
            physics_client_id: PyBullet client ID.
            particle_col_shape: Optional pre-created PyBullet collision shape ID.
            particle_vis_shape: Optional pre-created PyBullet visual shape ID.

        Returns:
            A list of newly created particle body IDs.
        """

    def calculate_buoyancy(self, object_mass: float, submerged_volume: float = 0.0, fluid_density: float = 1000.0) -> np.ndarray:
        """
        Calculates the buoyant force acting on an object when submerged in a fluid.
        Buoyancy force limited to a maximum of twice the object's weight to prevent instability in simulations.
        Coordinate frame assumes z+ is up.
        
        Arguments:
            object_mass (float): Mass of the object in kilograms.
            submerged_volume (float): Volume of the object that is submerged in the fluid in cubic meters.
            fluid_density (float): Density of the fluid in kg/m^3. Default is 1000 kg/m^3 (density of water).
            
        Returns:
            np.ndarray: A 3D vector representing the combined hydrodynamic force acting on the object.
        """

    def add_pybullet_convex_hull(self, vertices, position=[0, 0, 0], orientation=[0, 0, 0, 1], mass=0.0, visual_color=[0.5, 0.5, 0.5, 1.0], visual_specular=[0.4, 0.4, 0], physics_client_id=0) -> int:
        """
        Create a PyBullet multibody object from given vertices by computing their convex hull.
        Number of verticies must be no more than 1,500.

        Arguments:
            vertices (np.ndarray): A flat array of vertex positions (x, y, z), of shape [3 * k] where k is the number of vertices.
            position (list): Base position of the multibody [x, y, z]. Default: [0, 0, 0].
            orientation (list): Base orientation as quaternion [x, y, z, w]. Default: [0, 0, 0, 1].
            mass (float): Mass of the object. Default: 0.0 (static object).
            visual_color (list): RGBA color values [r, g, b, a]. Default: [0.5, 0.5, 0.5, 1.0].
            visual_specular (list): RGB specular color values [r, g, b]. Default: [0.4, 0.4, 0].
            physics_client_id (int): PyBullet physics client ID. Default: 0.
        Returns:
            int: PyBullet multibody ID.
        """

    def add_pybullet_soft_body(self, vertices, indices, position=[0, 0, 0], orientation=[0, 0, 0, 1], mass=1.0, visual_color=[0.7, 0.3, 0.3, 1.0], physics_client_id=0, preset="fabric"):
        """
        Create a PyBullet soft body with preset properties from given vertices and indices.
        Both collision and visual shapes are created with customizable properties.
        If used, code must call p.resetSimulation(p.RESET_USE_DEFORMABLE_WORLD) for soft bodies to work.
        Number of verticies must be no more than 1,500.
        Mesh must be fully connected (no separate pieces).
        Note that soft bodies in PyBullet can be unstable. If a rigid body would suffice for your application (such as a compacted piece of paper), consider using add_pybullet_mesh or add_pybullet_convex_hull instead.

        Arguments:
            vertices (np.ndarray): Array of vertex positions, of shape (3 * k,) where k is the number of vertices.
            indices (np.ndarray): Array of triangle indices, of shape (3 * m,) where m is the number of triangles.
            position (list): [x, y, z] position to place the soft body.
            orientation (list): [x, y, z, w] quaternion for orientation.
            mass (float): Mass of the soft body.
            visual_color (list): [r, g, b, a] color for visualization.
            physics_client_id (int): PyBullet physics client ID.
            preset (str): Preset configuration for the soft body. Must be either "fabric", "paper" or "cushion".
        Returns:
            int: PyBullet soft body ID.
        """

    def add_pybullet_tube(self, radius, height, thickness, glass_material=True, position=[0, 0, 0], orientation=[0, 0, 0, 1], 
                       mass=0.0, visual_color=[0.5, 0.5, 0.5, 1.0], visual_specular=[0.4, 0.4, 0],
                       physics_client_id=0, num_segments=16) -> int:
        """
        A special function to create a hollow tube mesh (no end caps) in PyBullet. If glass_material, then backfaces are rendered (for materials like glass/water), so the interior of the tube is visible.
        To be used for objects like cups, vases, and other hollow shapes.
        The tube is aligned along the z-axis, centered at the origin before applying position and orientation.
        
        Arguments:
            radius (float): Radius of the tube.
            height (float): Height of the tube.
            thickness (float): Thickness of the tube walls.
            glass_material (bool): If True, sets material properties to resemble transparent material (e.g. transparent glass) by only rendering backfaces, so the interior of the tube is visible. Default is True.
            position (list): Position of centroid of cylinder [x, y, z]. Default: [0, 0, 0].
            orientation (list): Base orientation as quaternion [x, y, z, w]. Default: [0, 0, 0, 1].
            mass (float): Mass of the object. Default: 0.0 (static object).
            visual_color (list): RGBA color values [r, g, b, a]. Default: [0.5, 0.5, 0.5, 1.0].
            visual_specular (list): RGB specular color values [r, g, b]. Default: [0.4, 0.4, 0].
            physics_client_id (int): PyBullet physics client ID. Default: 0.
            num_segments (int): Number of segments to approximate the circular cross-section.
        Returns:
            int: PyBullet multibody ID.
        """

    def add_pybullet_open_box(self, size, wall_thickness=0.01, center=[0, 0, 0], 
                    mass=0.0, visual_color=[0.5, 0.5, 0.5, 1.0],
                    physics_client_id=0) -> List[int]:
        """
        Create and add an open box (no top) - as a PyBullet multibody.
        The box is created from 5 rectangular box parts (bottom and 4 sides).
        Arguments:
            size (list): Full size of the box [size_x, size_y, size_z].
            wall_thickness (float): Thickness of the box walls.
            center (list): Center position of the box [x, y, z]. Default: [0, 0, 0].
            mass (float): Mass of the object. Default: 0.0 (static object).
            visual_color (list): RGBA color values [r, g, b, a]. Default: [0.5, 0.5, 0.5, 1.0].
            physics_client_id (int): PyBullet physics client ID. Default: 0.
        Returns:
            list: List of PyBullet body IDs for each part of the open box.
        """

    def get_2d_points_from_mask(self, mask: np.ndarray) -> np.ndarray:
        """
        Extracts the 2D pixel coordinates of all `True` values in a boolean mask.

        Arguments:
            mask (np.ndarray):
                A 2D boolean numpy array representing the segmentation mask.

        Returns:
            np.ndarray:
                An array of shape `[N, 2]`, where N is the number of `True` pixels.
                Each row is an `[x, y]` coordinate. Returns an empty array if the
                mask has no true values.
        """
        pass

    def clean_mask(self, mask: np.ndarray, kernel_size: int = 5) -> np.ndarray:
        """
        Cleans a binary mask by applying morphological operations (opening) to remove
        noise and then selects only the largest contiguous object.

        Arguments:
            mask (np.ndarray):
                A 2D boolean numpy array.
            kernel_size (int):
                The size of the kernel for morphological operations. Must be an odd integer.

        Returns:
            np.ndarray:
                A cleaned 2D boolean numpy array containing only the largest object.
                Returns an empty mask if no objects are found.
        """
        pass

    def calculate_ground_line(self, mask: np.ndarray) -> int:
        """
        Calculates the y-value of the ground from the mask of a 2D object at rest.
        This is determined by finding the maximum y-coordinate (lowest point) of the mask.

        Limitation: This method assumes that the object and any motion will be entirely in plane for the camera view. 

        Arguments:
            mask (np.ndarray):
                A 2D boolean numpy array of the object resting on the ground.

        Returns:
            int:
                The integer y-coordinate of the ground line.
        """
        pass

    def fit_2d_shape(self, points: np.ndarray, shape_class: str) -> dict:
        """
        Fits a 2D geometric primitive (rectangle or circle) to a set of 2D points.

        Arguments:
            points (np.ndarray):
                An `[N, 2]` array of `[x, y]` points.
            shape_class (str):
                The type of shape to fit. Must be 'rectangle' or 'circle'.

        Returns:
            dict:
                A dictionary containing the fitted shape parameters:
                - For 'rectangle': {'center': [x, y], 'size': [width, height], 'angle': angle_in_degrees}
                - For 'circle': {'center': [x, y], 'radius': r}
        """
        pass

    def generate_polygon_from_mask(self, mask: np.ndarray, simplification_factor: float = 0.005) -> np.ndarray:
        """
        Generates a simplified 2D polygon from a binary mask by finding and
        approximating its contour.

        Arguments:
            mask (np.ndarray):
                A 2D boolean numpy array. Assumes the mask has been cleaned
                and contains one contiguous object.
            simplification_factor (float):
                Controls the precision of the polygon approximation. Smaller values
                result in more vertices and higher detail. Value is relative to
                the contour's perimeter.

        Returns:
            np.ndarray:
                An array of shape `[k, 2]` representing the polygon's vertex
                coordinates `[x, y]`.
        """
        pass

    def add_pymunk_polygon(self,
                           pymunk_space: pymunk.Space,
                           vertices: np.ndarray,
                           mass: float = 1.0,
                           position: Tuple[float, float] = (0, 0),
                           body_type: int = pymunk.Body.DYNAMIC) -> Tuple[pymunk.Body, pymunk.Poly]:
        """
        Adds a convex polygon shape to a Pymunk space.

        Note: Pymunk requires that polygon shapes be convex. This function will
        raise a ValueError if the provided vertices form a concave polygon.
        Consider decomposing concave polygons into multiple convex ones first.

        Arguments:
            pymunk_space (pymunk.Space):
                The Pymunk space to add the polygon to.
            vertices (np.ndarray):
                An array of shape `[N, 2]` representing the polygon's vertices in
                clockwise or counter-clockwise order.
            mass (float):
                The mass of the object. If set to 0, the body will be static.
            position (Tuple[float, float]):
                The initial `(x, y)` world position of the body's center of gravity.
            body_type (int):
                The type of Pymunk body (DYNAMIC, KINEMATIC, or STATIC).
                Mass must be 0 for STATIC bodies.

        Returns:
            Tuple[pymunk.Body, pymunk.Poly]:
                A tuple containing the created Pymunk Body and Poly shape.
        """
        pass
```

For this task, you must only respond with the definition of a class `VideoSimulation` and any library imports. Assume that all imports such as the `API` class and `Simulator` class are handled for you. You do not need to import them yourself. You may define helper functions or classes if you wish, but the main class must be named `VideoSimulation` and must inherit from `Simulator`. Your class must implement all abstract methods of the `Simulator` class.

In any trade-off between implementation simplicity and robustness to sensor noise, prioritize robustness. The functions in the provided API are known to produce occasional but significant outliers.