From 7c8d7796fb26c7a7784bb1fda5f8cc69776d63fa Mon Sep 17 00:00:00 2001 From: Maciej Buszko Date: Mon, 5 Jan 2026 09:42:49 -0800 Subject: [PATCH 1/7] ManiSkill Scenic Integration (#400) * Add interface to ManiSkill simulator * Add optional maniskill dependencies * Format with black --- examples/maniskill/README.md | 3 + examples/maniskill/robot_arm.scenic | 31 ++ examples/maniskill/stacked_cubes.scenic | 33 ++ pyproject.toml | 7 + src/scenic/simulators/maniskill/__init__.py | 13 + src/scenic/simulators/maniskill/model.scenic | 105 +++++ src/scenic/simulators/maniskill/simulator.py | 409 +++++++++++++++++++ 7 files changed, 601 insertions(+) create mode 100644 examples/maniskill/README.md create mode 100644 examples/maniskill/robot_arm.scenic create mode 100644 examples/maniskill/stacked_cubes.scenic create mode 100644 src/scenic/simulators/maniskill/__init__.py create mode 100644 src/scenic/simulators/maniskill/model.scenic create mode 100644 src/scenic/simulators/maniskill/simulator.py diff --git a/examples/maniskill/README.md b/examples/maniskill/README.md new file mode 100644 index 000000000..b853fbdf0 --- /dev/null +++ b/examples/maniskill/README.md @@ -0,0 +1,3 @@ +# ManiSkill Examples + +This directory contains example scenarios for the [ManiSkill](https://github.com/haosulab/ManiSkill) robotics simulator. diff --git a/examples/maniskill/robot_arm.scenic b/examples/maniskill/robot_arm.scenic new file mode 100644 index 000000000..eb9c2e3e0 --- /dev/null +++ b/examples/maniskill/robot_arm.scenic @@ -0,0 +1,31 @@ +model scenic.simulators.maniskill.model + +import math +pi = math.pi + +# Create ground plane +ground = new Ground with position (0, 0, -0.920) + +# Create Panda robot arm with a neutral pose +# Joint angles: [joint1, joint2, joint3, joint4, joint5, joint6, joint7, gripper_left, gripper_right] +panda = new Robot, + with uuid "panda", + with jointAngles [0.0, pi/8, 0.0, -pi/2, 0.0, pi*5/8, pi/4, 0.04, 0.04] + +# Create camera positioned to view the robot and cube +camera = new Camera, + at (-0.64, -0.4, 0.758), + with name "base_camera", + with img_width 640, + with img_height 480, + with fov 1.0 + +# Create a cube in front of the robot (within reach) +cube = new ManiskillObject on ground, + at (0.4, 0.0, 0.035), + with name "target_cube", + with shape BoxShape(), + with width 0.05, + with length 0.05, + with height 0.05, + with color [1.0, 0.5, 0.0, 1.0] # Orange cube diff --git a/examples/maniskill/stacked_cubes.scenic b/examples/maniskill/stacked_cubes.scenic new file mode 100644 index 000000000..5e2ffea71 --- /dev/null +++ b/examples/maniskill/stacked_cubes.scenic @@ -0,0 +1,33 @@ +model scenic.simulators.maniskill.model + +# Create ground plane +ground = new Ground with position (0, 0, -0.920) + +# Create three cubes stacked on top of each other +# Bottom cube (red) +cube1 = new ManiskillObject on ground, + at (0, 0, 0.035), + with name "cube1", + with shape BoxShape(), + with width 0.07, + with length 0.07, + with height 0.07, + with color [0.9, 0.1, 0.1, 1.0] + +# Middle cube (green) +cube2 = new ManiskillObject on cube1, + with name "cube2", + with shape BoxShape(), + with width 0.07, + with length 0.07, + with height 0.07, + with color [0.1, 0.9, 0.1, 1.0] + +# Top cube (blue) +cube3 = new ManiskillObject on cube2, + with name "cube3", + with shape BoxShape(), + with width 0.07, + with length 0.07, + with height 0.07, + with color [0.1, 0.1, 0.9, 1.0] diff --git a/pyproject.toml b/pyproject.toml index b1ade058c..294e79071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,13 @@ metadrive = [ "metadrive-simulator@git+https://github.com/metadriverse/metadrive.git@85e5dadc6c7436d324348f6e3d8f8e680c06b4db", "sumolib >= 1.21.0", ] +maniskill = [ + "mani-skill >= 3.0.0b20", + "torch >= 2.0.0", + "sapien >= 3.0.0", + "gymnasium >= 0.29.0", + "coacd >= 1.0.0", +] test = [ # minimum dependencies for running tests (used for tox virtualenvs) "pytest >= 7.0.0, <9", "pytest-cov >= 3.0.0", diff --git a/src/scenic/simulators/maniskill/__init__.py b/src/scenic/simulators/maniskill/__init__.py new file mode 100644 index 000000000..ed1143bb7 --- /dev/null +++ b/src/scenic/simulators/maniskill/__init__.py @@ -0,0 +1,13 @@ +""" +Interface to the ManiSkill robotics simulator. +""" + +# Check if mani_skill is installed and import ManiSkillSimulator only if it is +mani_skill = None +try: + import mani_skill +except ImportError: + pass +if mani_skill: + from .simulator import ManiSkillSimulator +del mani_skill diff --git a/src/scenic/simulators/maniskill/model.scenic b/src/scenic/simulators/maniskill/model.scenic new file mode 100644 index 000000000..c07c61c58 --- /dev/null +++ b/src/scenic/simulators/maniskill/model.scenic @@ -0,0 +1,105 @@ +"""Scenic world model for robotic manipulation scenarios in ManiSkill. + +Global Parameters: + render (bool): Whether to render the simulation in a window. If True, it will open + a window and display the simulation. If False (default), the simulation runs headless. + stride (int): Number of simulation steps between action updates. Default is 1. +""" + +try: + from scenic.simulators.maniskill.simulator import ManiSkillSimulator +except ModuleNotFoundError: + # for convenience when testing without the mani_skill package + from scenic.core.simulators import SimulatorInterfaceWarning + import warnings + warnings.warn('The "mani-skill" package is not installed; ' + 'will not be able to run dynamic simulations', + SimulatorInterfaceWarning) + + def ManiSkillSimulator(*args, **kwargs): + """Dummy simulator to allow compilation without the 'mani-skill' package. + + :meta private: + """ + raise RuntimeError('the "mani-skill" package is required to run simulations ' + 'from this scenario') + +param render = False +param stride = 1 + +simulator ManiSkillSimulator( + render=bool(globalParameters.render), + stride=int(globalParameters.stride), +) + +class ManiskillObject(Object): + """Base class for ManiSkill objects. + + Properties: + visualFilename (str or None): Path to a visual mesh file (e.g., .glb, .obj, .usdz). + visualScale (tuple): Scale factor for the visual mesh as (x, y, z). + visualOffset (tuple): Offset for the visual mesh as (x, y, z) to match visual and collision meshes. + kinematic (bool): If True, the object is kinematic (fixed in place). + """ + visualFilename: None + visualScale: (1, 1, 1) + visualOffset: (0, 0, 0) + width: 1 + length: 1 + height: 1 + color: [0.8, 0.8, 0.8, 1.0] + kinematic: False + +class Ground(ManiskillObject): + """The default ground plane from ManiSkill. + + This creates a large flat ground plane using ManiSkill's built-in ground builder. + The ground is always kinematic (fixed in place). + """ + name: "ground" + shape: BoxShape() + width: 100 + length: 100 + height: 0.001 + position: (0, 0, -0.0005) + kinematic: True + visualFilename: None + isSimpleGround: True # Specify to ManiSkill that this is the ground plane + +class Robot(Object): + """Robot object (Panda arm by default). + + This class represents a robot in the scene. The robot is not physically generated + in the scene (skipGeneration=True) but is used to configure the robot's initial state. + + Properties: + uuid (str): Robot type identifier. Default is "panda" for the Franka Emika Panda arm. + jointAngles (list): Initial joint angles for the robot. If empty, uses a default configuration. + """ + position: (10, 0, 0) # Make scenic ignore it for collision checking for now + name: "actor" + uuid: "panda" + jointAngles: [] + skipGeneration: True # Don't generate this object in the scene + +class Camera(OrientedPoint): + """Camera configuration for the scene. + + Cameras are not physical objects but define viewpoints for rendering and observation. + + Properties: + img_width (int): Image width in pixels. Default is 640. + img_height (int): Image height in pixels. Default is 480. + fov (float): Field of view in radians. Default is 1.0. + near (float): Near clipping plane distance. Default is 0.01. + far (float): Far clipping plane distance. Default is 100.0. + shader_pack (str): Shader pack to use for rendering. Default is "rt" (ray tracing). + """ + name: "camera" + img_width: 640 + img_height: 480 + fov: 1.0 + near: 0.01 + far: 100.0 + shader_pack: "rt" + skipGeneration: True # Don't generate this as a physical object diff --git a/src/scenic/simulators/maniskill/simulator.py b/src/scenic/simulators/maniskill/simulator.py new file mode 100644 index 000000000..ba71aced8 --- /dev/null +++ b/src/scenic/simulators/maniskill/simulator.py @@ -0,0 +1,409 @@ +"""Simulator interface for ManiSkill.""" + +import math +import os +import tempfile +from typing import Optional + +import gymnasium as gym +from mani_skill.envs.sapien_env import BaseEnv +from mani_skill.sensors.camera import CameraConfig +from mani_skill.utils import sapien_utils +from mani_skill.utils.building.ground import build_ground +from mani_skill.utils.registration import register_env +import numpy as np +import sapien +import torch + +from scenic.core.simulators import Simulation, Simulator +from scenic.core.vectors import Orientation, Vector + +# Default camera configuration +DEFAULT_CAMERA_EYE = [0.0, -1.0, 1.0] +DEFAULT_CAMERA_TARGET = [0.0, 0.0, 0.0] +DEFAULT_ROBOT_POSITION = [0.0, 0.0, 0.0] + + +@register_env("ScenicEnv") +class ScenicEnv(BaseEnv): + """Custom ManiSkill environment for Scenic scenarios.""" + + def __init__( + self, + *args, + robot_uids="panda", + objects_to_create=[], + camera_configs=None, + **kwargs, + ): + self.robot_uids = robot_uids + self.scenic_objects = {} + self.objects_to_create = objects_to_create + self.scene_camera_configs = camera_configs + super().__init__(*args, robot_uids=robot_uids, **kwargs) + + @property + def _default_sensor_configs(self): + """Return camera configurations for the scene. + + Uses scene-defined cameras if available, otherwise creates a default camera. + """ + if self.scene_camera_configs: + return self.scene_camera_configs + + # Fallback to default camera configuration + pose = sapien_utils.look_at( + eye=DEFAULT_CAMERA_EYE, target=DEFAULT_CAMERA_TARGET + ) + return [ + CameraConfig( + "base_camera", + pose, + width=640, + height=480, + fov=1.0, + near=0.01, + far=100.0, + shader_pack="rt", + ) + ] + + def _load_agent( + self, options: dict, initial_agent_poses=None, build_separate=False + ): + """Load the robot agent into the scene.""" + super()._load_agent(options, sapien.Pose(p=DEFAULT_ROBOT_POSITION)) + + def build_actor_from_trimesh(self, scene, obj, mesh): + """Build a SAPIEN actor from a trimesh object.""" + if getattr(obj, "skipGeneration", False): + return None + + if getattr(obj, "isSimpleGround", False): + return build_ground( + scene, + floor_width=int(obj.width), + floor_length=int(obj.length), + xy_origin=(obj.position[0], obj.position[1]), + altitude=obj.position[2], + name=obj.name, + ) + + # Use a temporary directory for mesh files + tmp_dir = tempfile.gettempdir() + obj_filename = os.path.join(tmp_dir, f"scenic_maniskill_{obj.name}_mesh.obj") + mesh.export(obj_filename) + + builder = scene.create_actor_builder() + + width = getattr(obj, "width", 1.0) + length = getattr(obj, "length", 1.0) + height = getattr(obj, "height", 1.0) + + builder.add_multiple_convex_collisions_from_file( + filename=obj_filename, + pose=sapien.Pose(), + scale=(width, length, height), + decomposition="coacd", + ) + + if hasattr(obj, "visualFilename") and obj.visualFilename is not None: + visualOffset = getattr(obj, "visualOffset", (0.0, 0.0, 0.0)) + visualScale = getattr(obj, "visualScale", (1.0, 1.0, 1.0)) + + builder.add_visual_from_file( + filename=obj.visualFilename, + pose=sapien.Pose(p=[visualOffset[0], visualOffset[1], visualOffset[2]]), + scale=visualScale, + ) + else: + color = getattr(obj, "color", [0.8, 0.8, 0.8, 1.0]) + + builder.add_visual_from_file( + filename=obj_filename, + pose=sapien.Pose(), + scale=(width, length, height), + material=sapien.render.RenderMaterial( + base_color=color, + ), + ) + + builder.set_name(obj.name) + builder.set_initial_pose( + sapien.Pose([obj.position[0], obj.position[1], obj.position[2]]) + ) + + if getattr(obj, "kinematic", False): + actor = builder.build_kinematic(name=obj.name) + else: + actor = builder.build(name=obj.name) + + return actor + + def _load_scene(self, options: dict): + for obj in self.objects_to_create: + mesh = obj.shape.mesh + self.scenic_objects[obj.name] = self.build_actor_from_trimesh( + self.scene, obj, mesh + ) + + def _initialize_episode(self, env_idx, options: dict): + """Initialize the robot configuration for the episode. + + Sets the robot's joint angles from the Scenic scene if provided, + otherwise uses the robot's default configuration. + """ + # Find the robot object in the scene + robot_objects = [ + obj + for obj in self.objects_to_create + if hasattr(obj, "uuid") and obj.uuid == "panda" + ] + + if not robot_objects: + # No robot configuration specified, use default + return + + robot = robot_objects[0] + + if ( + hasattr(robot, "jointAngles") + and robot.jointAngles + and len(robot.jointAngles) > 0 + ): + # Use user-specified joint angles + qpos = np.array(robot.jointAngles, dtype=np.float32) + + # Validate joint angles match robot DOF + robot_dof = self.agent.robot.dof + if len(qpos) != robot_dof: + raise ValueError( + f"Joint angles length ({len(qpos)}) does not match robot DOF ({robot_dof}). " + f"For Panda robot, provide {robot_dof} joint angles." + ) + + qpos = torch.from_numpy(qpos) + self.agent.reset(qpos) + self.agent.robot.set_pose(sapien.Pose(DEFAULT_ROBOT_POSITION)) + # If no joint angles specified, the agent will use its default configuration + + def compute_dense_reward(self, obs, action, info: dict): + return 1.0 + + def compute_normalized_dense_reward(self, obs, action, info: dict): + return 1.0 + + +class ManiSkillSimulator(Simulator): + """Simulator interface for ManiSkill.""" + + def __init__(self, render=False, stride=1): + super().__init__() + self.last_simulation = None + self.render = render + self.stride = stride + + def createSimulation(self, scene, **kwargs): + self.last_simulation = ManiSkillSimulation( + scene, render=self.render, stride=self.stride, **kwargs + ) + return self.last_simulation + + +class ManiSkillSimulation(Simulation): + """Simulation interface for ManiSkill.""" + + def __init__(self, scene, *, timestep=None, render=False, stride=1, **kwargs): + self.env: Optional[gym.Env] = None + self._done = False + self._obs = None + self._info = None + self._reward = None + self.action = None + self.maniskill_scene = scene + self.objects_to_create = [] + self.camera_configs = [] + self.stride = stride + self.stride_idx = 0 + self.step_count = 0 + + # Should it be headless (false) or render (true)? + self.render = render + + super().__init__(scene, timestep=(timestep or 1 / 60), **kwargs) + + def setup(self): + # Calls the createObjectInSimulator method for each object in the scene + super().setup() + + self.extract_camera_configs() + + render_mode = "human" if self.render else None + + self.env = gym.make( + "ScenicEnv", + num_envs=1, + obs_mode="rgb", + control_mode="pd_ee_delta_pose", + render_mode=render_mode, + objects_to_create=self.objects_to_create, + camera_configs=self.camera_configs, + ) + + self.step_count = 0 + self._obs, self._info = self.env.reset(seed=0) + + def extract_camera_configs(self): + """Extract camera configurations from Scenic objects. + + Converts Scenic Camera objects to ManiSkill CameraConfig objects. + Uses the camera's orientation to determine the look-at target. + """ + for obj in self.maniskill_scene.objects: + if hasattr(obj, "__class__") and obj.__class__.__name__ == "Camera": + # Convert scenic camera to ManiSkill CameraConfig + eye_pos = [obj.position.x, obj.position.y, obj.position.z] + + # Calculate target position from camera orientation + # Use the camera's forward direction (1 unit ahead) + if hasattr(obj, "heading"): + # For 2D heading, assume camera looks horizontally + target_x = obj.position.x + math.cos(obj.heading) + target_y = obj.position.y + math.sin(obj.heading) + target_z = obj.position.z + target_pos = [target_x, target_y, target_z] + else: + # Fallback: look at origin + target_pos = [0.0, 0.0, 0.0] + + pose = sapien_utils.look_at(eye=eye_pos, target=target_pos) + + camera_config = CameraConfig( + obj.name, + pose, + width=getattr(obj, "width", 640), + height=getattr(obj, "height", 480), + fov=getattr(obj, "fov", 1.0), + near=getattr(obj, "near", 0.01), + far=getattr(obj, "far", 100.0), + shader_pack=getattr(obj, "shader_pack", "rt"), + ) + + self.camera_configs.append(camera_config) + + def createObjectInSimulator(self, obj): + # Skip camera objects as they're handled separately + if hasattr(obj, "__class__") and obj.__class__.__name__ == "Camera": + return + self.objects_to_create.append(obj) + + def step(self): + if self.should_terminate(): + return + + if self.env is None: + raise RuntimeError("Environment not set up. Call setup() first.") + + if self.action is None: + self.action = self.env.action_space.sample() * 0.0 + + obs, reward, term, trunc, info = self.env.step(self.action) + self._obs = obs + self.step_count += 1 + + if self.stride_idx == 0: + self.action = self.get_action_from_observation(obs) + + self._reward = reward + self._done = bool(term or trunc) + self.stride_idx = (self.stride_idx + 1) % self.stride + + if self.render: + self.env.render() + + # Check for termination condition + if self.should_terminate(): + self._done = True + + def get_action_from_observation(self, obs): + """Override this method to implement custom action selection.""" + return self.env.action_space.sample() * 0.0 + + def should_terminate(self): + """Override this method to implement custom termination logic.""" + return False + + def success(self): + """Override this method to implement custom success logic.""" + return False + + def get_step_count(self): + return self.step_count + + def getObservation(self): + return self._obs + + def tensor_to_vector(self, tensor): + if tensor.dim() != 1 or tensor.size(0) != 3: + raise ValueError("Tensor must be 1D with 3 elements.") + return Vector(tensor[0].item(), tensor[1].item(), tensor[2].item()) + + def getProperties(self, obj, properties): + if self.env is None: + raise ValueError("Environment is not initialized.") + + ms_obj = self.env.unwrapped.scenic_objects[obj.name] + + if getattr(obj, "skipGeneration", False): + return dict( + position=Vector(10, 0, 0), + velocity=Vector(0, 0, 0), + pitch=0.0, + yaw=0.0, + roll=0.0, + speed=0.0, + angularVelocity=Vector(0, 0, 0), + angularSpeed=0.0, + ) + + position = self.tensor_to_vector(ms_obj.pose.p[0]) + if getattr(obj, "kinematic", False): + velocity = Vector(0, 0, 0) + else: + velocity = self.tensor_to_vector(ms_obj.get_linear_velocity()[0]) + speed = (velocity.x**2 + velocity.y**2 + velocity.z**2) ** 0.5 + + q = ms_obj.pose.q[0] + orientation = Orientation.fromQuaternion((q[0], q[1], q[2], q[3])) + if getattr(obj, "kinematic", False): + angular_velocity = Vector(0, 0, 0) + else: + angular_velocity = self.tensor_to_vector(ms_obj.get_angular_velocity()[0]) + angular_speed = ( + angular_velocity.x**2 + angular_velocity.y**2 + angular_velocity.z**2 + ) ** 0.5 + + values = dict( + position=position, + velocity=velocity, + pitch=orientation.pitch, + yaw=orientation.yaw, + roll=orientation.roll, + speed=speed, + angularVelocity=angular_velocity, + angularSpeed=angular_speed, + ) + + return values + + def isDone(self): + return self._done + + def getReward(self): + return self._reward + + def destroy(self): + self.objects_to_create.clear() + if self.env is not None: + self.env.close() + self.env = None From 615eb74ed13e14e1088e6507e779756d9503ad63 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:53:31 -0800 Subject: [PATCH 2/7] Fix ManiSkill docs build and extras install (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix two ManiSkill-related issues: - Sphinx docs were failing when ManiSkill wasn’t installed. Add a shared buildingScenicDocumentation() helper, mock ManiSkill deps in autodoc_mock_imports, and only register ScenicEnv outside doc builds. - pip install "scenic[maniskill]" previously hit pip's "resolution-too-deep" error. Tighten the extra to require gymnasium >= 1.0.0 so the resolver converges with current releases. --- docs/conf.py | 11 ++++++- pyproject.toml | 2 +- src/scenic/core/utils.py | 13 ++++++++ src/scenic/simulators/lgsvl/model.scenic | 13 ++++---- src/scenic/simulators/maniskill/simulator.py | 32 +++++++++++++------- src/scenic/syntax/translator.py | 5 ++- 6 files changed, 53 insertions(+), 23 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 02628a3ec..82e015919 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,6 +55,7 @@ warnings.simplefilter("ignore", SimulatorInterfaceWarning) import scenic.simulators.carla.model import scenic.simulators.lgsvl.model + import scenic.simulators.maniskill.model import scenic.simulators.metadrive.model veneer.deactivate() @@ -108,7 +109,15 @@ autosummary_generate = True autodoc_inherit_docstrings = False autodoc_member_order = "bysource" -autodoc_mock_imports = ["carla", "lgsvl", "metadrive"] +autodoc_mock_imports = [ + "carla", + "lgsvl", + "metadrive", + "mani_skill", + "torch", + "sapien", + "gymnasium", +] autodoc_typehints = "description" autodoc_type_aliases = { "Vectorlike": "`scenic.domains.driving.roads.Vectorlike`", diff --git a/pyproject.toml b/pyproject.toml index 294e79071..c21ee3150 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ maniskill = [ "mani-skill >= 3.0.0b20", "torch >= 2.0.0", "sapien >= 3.0.0", - "gymnasium >= 0.29.0", + "gymnasium >= 1.0.0", "coacd >= 1.0.0", ] test = [ # minimum dependencies for running tests (used for tox virtualenvs) diff --git a/src/scenic/core/utils.py b/src/scenic/core/utils.py index 9817843f5..e5488f3ed 100644 --- a/src/scenic/core/utils.py +++ b/src/scenic/core/utils.py @@ -19,6 +19,19 @@ sqrt2 = math.sqrt(2) + +def buildingScenicDocumentation(): + """Return True if Scenic's Sphinx docs are currently being built. + + This relies on docs/conf.py setting ``sphinx._buildingScenicDocs = True`` + before importing Scenic, and avoids importing Sphinx here. + """ + sphinx = sys.modules.get("sphinx") + if sphinx is None: + return False + return bool(getattr(sphinx, "_buildingScenicDocs", False)) + + if sys.version_info >= (3, 12): from itertools import batched else: diff --git a/src/scenic/simulators/lgsvl/model.scenic b/src/scenic/simulators/lgsvl/model.scenic index 902566f94..d1b8f27cb 100644 --- a/src/scenic/simulators/lgsvl/model.scenic +++ b/src/scenic/simulators/lgsvl/model.scenic @@ -2,15 +2,14 @@ from scenic.domains.driving.model import * from scenic.simulators.lgsvl.behaviors import * +from scenic.core.utils import buildingScenicDocumentation # Deprecation error on import, but allow for doc building. -try: - import sphinx - if not sphinx._buildingScenicDocs: - raise RuntimeError("The LGSVL Simulator interface was deprecated in Scenic 3." - " To continue to use the interface, please use Scenic 2.") -except ModuleNotFoundError: - pass +if not buildingScenicDocumentation(): + raise RuntimeError( + "The LGSVL Simulator interface was deprecated in Scenic 3." + " To continue to use the interface, please use Scenic 2." + ) try: import lgsvl diff --git a/src/scenic/simulators/maniskill/simulator.py b/src/scenic/simulators/maniskill/simulator.py index ba71aced8..f8e2ad711 100644 --- a/src/scenic/simulators/maniskill/simulator.py +++ b/src/scenic/simulators/maniskill/simulator.py @@ -5,26 +5,35 @@ import tempfile from typing import Optional +import numpy as np + +from scenic.core.simulators import Simulation, Simulator +from scenic.core.utils import buildingScenicDocumentation +from scenic.core.vectors import Orientation, Vector + +try: + import mani_skill +except ImportError as e: + raise ModuleNotFoundError( + "ManiSkill scenarios require the optional ManiSkill dependencies. " + "Install with `pip install scenic[maniskill]`." + ) from e + import gymnasium as gym from mani_skill.envs.sapien_env import BaseEnv from mani_skill.sensors.camera import CameraConfig from mani_skill.utils import sapien_utils from mani_skill.utils.building.ground import build_ground from mani_skill.utils.registration import register_env -import numpy as np import sapien import torch -from scenic.core.simulators import Simulation, Simulator -from scenic.core.vectors import Orientation, Vector - # Default camera configuration DEFAULT_CAMERA_EYE = [0.0, -1.0, 1.0] DEFAULT_CAMERA_TARGET = [0.0, 0.0, 0.0] DEFAULT_ROBOT_POSITION = [0.0, 0.0, 0.0] -@register_env("ScenicEnv") class ScenicEnv(BaseEnv): """Custom ManiSkill environment for Scenic scenarios.""" @@ -52,9 +61,7 @@ def _default_sensor_configs(self): return self.scene_camera_configs # Fallback to default camera configuration - pose = sapien_utils.look_at( - eye=DEFAULT_CAMERA_EYE, target=DEFAULT_CAMERA_TARGET - ) + pose = sapien_utils.look_at(eye=DEFAULT_CAMERA_EYE, target=DEFAULT_CAMERA_TARGET) return [ CameraConfig( "base_camera", @@ -68,9 +75,7 @@ def _default_sensor_configs(self): ) ] - def _load_agent( - self, options: dict, initial_agent_poses=None, build_separate=False - ): + def _load_agent(self, options: dict, initial_agent_poses=None, build_separate=False): """Load the robot agent into the scene.""" super()._load_agent(options, sapien.Pose(p=DEFAULT_ROBOT_POSITION)) @@ -194,6 +199,11 @@ def compute_normalized_dense_reward(self, obs, action, info: dict): return 1.0 +# Register the ManiSkill environment in normal runs, but skip this during doc builds. +if not buildingScenicDocumentation(): + ScenicEnv = register_env("ScenicEnv")(ScenicEnv) + + class ManiSkillSimulator(Simulator): """Simulator interface for ManiSkill.""" diff --git a/src/scenic/syntax/translator.py b/src/scenic/syntax/translator.py index 994e65b8b..109744cce 100644 --- a/src/scenic/syntax/translator.py +++ b/src/scenic/syntax/translator.py @@ -41,7 +41,7 @@ from scenic.core.errors import InvalidScenarioError, PythonCompileError from scenic.core.lazy_eval import needsLazyEvaluation import scenic.core.pruning as pruning -from scenic.core.utils import cached_property +from scenic.core.utils import buildingScenicDocumentation, cached_property from scenic.syntax.compiler import compileScenicAST from scenic.syntax.parser import parse_string import scenic.syntax.veneer as veneer @@ -439,8 +439,7 @@ def __setstate__(self, state): # getting confused. (Autodoc doesn't expect modules to have that attribute, # and we can't del it.) We only do this during Sphinx runs since it seems to # sometimes break pickling of the modules. -sphinx = sys.modules.get("sphinx") -buildingDocs = sphinx and getattr(sphinx, "_buildingScenicDocs", False) +buildingDocs = buildingScenicDocumentation() if buildingDocs: ScenicModule.__module__ = None From 81b485a44fd0f2583c879bdf7543d4b2c41156f4 Mon Sep 17 00:00:00 2001 From: Lola Marrero Date: Tue, 31 Mar 2026 17:03:06 -0700 Subject: [PATCH 3/7] Add ManiSkill simulator docs section --- docs/simulators.rst | 22 ++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/simulators.rst b/docs/simulators.rst index 0887a0cfe..93e4ff117 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -125,6 +125,28 @@ 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 `__. +ManiSkill +--------- + +Scenic supports integration with the `ManiSkill `_ +simulator as an optional dependency, enabling users to describe robotic scenarios. + +You can install it with: + +.. code-block:: console + + python -m pip install scenic[maniskill] + +.. note:: + + ManiSkill currently best supports Linux systems. On Windows and macOS, ManiSkill has + limited support. On macOS, additional setup may be required. See the + `ManiSkill installation instructions `_. + +For more information, refer to the documentation of the +`scenic.simulators.maniskill` module. + + Webots ------ diff --git a/pyproject.toml b/pyproject.toml index 2bf6611a1..a02b3fb07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ maniskill = [ "mani-skill >= 3.0.0b20", "torch >= 2.0.0", "sapien >= 3.0.0", - "gymnasium >= 1.0.0", + "gymnasium >= 0.29.0", "coacd >= 1.0.0", ] test = [ # minimum dependencies for running tests (used for tox virtualenvs) From d2230902e6b72aafc82aaccfe574623561fd02bd Mon Sep 17 00:00:00 2001 From: Lola Marrero Date: Wed, 1 Apr 2026 15:46:27 -0700 Subject: [PATCH 4/7] Support ManiSkill scenes without robots --- src/scenic/simulators/maniskill/simulator.py | 52 ++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/maniskill/simulator.py b/src/scenic/simulators/maniskill/simulator.py index f8e2ad711..332184cab 100644 --- a/src/scenic/simulators/maniskill/simulator.py +++ b/src/scenic/simulators/maniskill/simulator.py @@ -40,7 +40,7 @@ class ScenicEnv(BaseEnv): def __init__( self, *args, - robot_uids="panda", + robot_uids="none", objects_to_create=[], camera_configs=None, **kwargs, @@ -79,6 +79,26 @@ def _load_agent(self, options: dict, initial_agent_poses=None, build_separate=Fa """Load the robot agent into the scene.""" super()._load_agent(options, sapien.Pose(p=DEFAULT_ROBOT_POSITION)) + def _get_obs_agent(self): + """Return agent observation data. + + For scenes without a robot, return an empty dict instead of + trying to read proprioception from a nonexistent agent. + """ + if self.agent is None: + return {} + return super()._get_obs_agent() + + def get_state_dict(self): + """Return environment state. + + For scenes without a robot, return only the scene simulation state + instead of trying to read controller state from a nonexistent agent. + """ + if self.agent is None: + return self.scene.get_sim_state() + return super().get_state_dict() + def build_actor_from_trimesh(self, scene, obj, mesh): """Build a SAPIEN actor from a trimesh object.""" if getattr(obj, "skipGeneration", False): @@ -158,6 +178,10 @@ def _initialize_episode(self, env_idx, options: dict): Sets the robot's joint angles from the Scenic scene if provided, otherwise uses the robot's default configuration. """ + # No robots in the scene + if self.agent is None: + return + # Find the robot object in the scene robot_objects = [ obj @@ -242,6 +266,19 @@ def __init__(self, scene, *, timestep=None, render=False, stride=1, **kwargs): super().__init__(scene, timestep=(timestep or 1 / 60), **kwargs) + def extract_robot_uid(self): + robot_objects = [ + obj for obj in self.maniskill_scene.objects if hasattr(obj, "uuid") + ] + + if len(robot_objects) > 1: + raise RuntimeError("Multiple robots are not supported yet.") + + if not robot_objects: + return "none" + + return robot_objects[0].uuid + def setup(self): # Calls the createObjectInSimulator method for each object in the scene super().setup() @@ -250,11 +287,15 @@ def setup(self): render_mode = "human" if self.render else None + robot_uid = self.extract_robot_uid() + control_mode = "pd_ee_delta_pose" if robot_uid != "none" else None + self.env = gym.make( "ScenicEnv", num_envs=1, obs_mode="rgb", - control_mode="pd_ee_delta_pose", + control_mode=control_mode, + robot_uids=robot_uid, render_mode=render_mode, objects_to_create=self.objects_to_create, camera_configs=self.camera_configs, @@ -315,7 +356,10 @@ def step(self): raise RuntimeError("Environment not set up. Call setup() first.") if self.action is None: - self.action = self.env.action_space.sample() * 0.0 + if self.env.action_space is None: + self.action = None + else: + self.action = self.env.action_space.sample() * 0.0 obs, reward, term, trunc, info = self.env.step(self.action) self._obs = obs @@ -337,6 +381,8 @@ def step(self): def get_action_from_observation(self, obs): """Override this method to implement custom action selection.""" + if self.env.action_space is None: + return None return self.env.action_space.sample() * 0.0 def should_terminate(self): From c5ef2f6b2cbe9261f641dfc11458eaaae51195e2 Mon Sep 17 00:00:00 2001 From: Lola Marrero Date: Thu, 2 Apr 2026 09:56:19 -0700 Subject: [PATCH 5/7] Avoid crash when closing ManiSkill viewer --- src/scenic/simulators/maniskill/simulator.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/scenic/simulators/maniskill/simulator.py b/src/scenic/simulators/maniskill/simulator.py index 332184cab..3db4bb3d8 100644 --- a/src/scenic/simulators/maniskill/simulator.py +++ b/src/scenic/simulators/maniskill/simulator.py @@ -373,6 +373,13 @@ def step(self): self.stride_idx = (self.stride_idx + 1) % self.stride if self.render: + viewer = self.env.unwrapped.viewer + + # If the GUI window has already been closed, stop the Scenic simulation cleanly. + if viewer is not None and viewer.closed: + self.screen = "Dead" + return + self.env.render() # Check for termination condition From ccee9b9176a4762ae116c0266de837e9120a2645 Mon Sep 17 00:00:00 2001 From: Lola Marrero Date: Thu, 2 Apr 2026 10:17:07 -0700 Subject: [PATCH 6/7] Use degree literals in ManiSkill example --- examples/maniskill/robot_arm.scenic | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/maniskill/robot_arm.scenic b/examples/maniskill/robot_arm.scenic index eb9c2e3e0..17b4b1c94 100644 --- a/examples/maniskill/robot_arm.scenic +++ b/examples/maniskill/robot_arm.scenic @@ -1,8 +1,5 @@ model scenic.simulators.maniskill.model -import math -pi = math.pi - # Create ground plane ground = new Ground with position (0, 0, -0.920) @@ -10,7 +7,7 @@ ground = new Ground with position (0, 0, -0.920) # Joint angles: [joint1, joint2, joint3, joint4, joint5, joint6, joint7, gripper_left, gripper_right] panda = new Robot, with uuid "panda", - with jointAngles [0.0, pi/8, 0.0, -pi/2, 0.0, pi*5/8, pi/4, 0.04, 0.04] + with jointAngles [0.0, 22.5 deg, 0.0, -90 deg, 0.0, 112.5 deg, 45 deg, 0.04, 0.04] # Create camera positioned to view the robot and cube camera = new Camera, From c96d94efb704cdd65551e47b9d1b22a308814d02 Mon Sep 17 00:00:00 2001 From: lola Date: Thu, 21 May 2026 11:36:37 -0700 Subject: [PATCH 7/7] improve ManiSkill macOS docs and make shader pack configurable --- docs/simulators.rst | 28 +++++++++++++--- src/scenic/simulators/maniskill/model.scenic | 21 +++++++----- src/scenic/simulators/maniskill/simulator.py | 34 +++++++++++++++----- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/docs/simulators.rst b/docs/simulators.rst index 93e4ff117..132003c0c 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -129,19 +129,37 @@ ManiSkill --------- Scenic supports integration with the `ManiSkill `_ -simulator as an optional dependency, enabling users to describe robotic scenarios. +simulator as an optional dependency, enabling users to describe robotic manipulation +scenarios. The current interface is still in an early stage of +development. Example scenarios can be found in :file:`examples/maniskill`, +including :file:`stacked_cubes.scenic` and :file:`robot_arm.scenic`. You can install it with: .. code-block:: console - python -m pip install scenic[maniskill] + python -m pip install "scenic[maniskill]" + +The current interface supports scenes with no robot or a single robot. +Multiple robots are not yet supported. .. note:: - ManiSkill currently best supports Linux systems. On Windows and macOS, ManiSkill has - limited support. On macOS, additional setup may be required. See the - `ManiSkill installation instructions `_. + ManiSkill currently works best on Linux. On macOS, additional setup is + typically required, including Vulkan setup. macOS currently supports CPU + simulation, but GPU simulation is not supported. + + macOS users may also need to install the Pinocchio Python package: + + .. code-block:: console + + python -m pip install pin + + See the official + `ManiSkill installation instructions `_ + and + `macOS installation guide `_ + for platform-specific setup details. For more information, refer to the documentation of the `scenic.simulators.maniskill` module. diff --git a/src/scenic/simulators/maniskill/model.scenic b/src/scenic/simulators/maniskill/model.scenic index c07c61c58..16432ce80 100644 --- a/src/scenic/simulators/maniskill/model.scenic +++ b/src/scenic/simulators/maniskill/model.scenic @@ -4,6 +4,8 @@ Global Parameters: render (bool): Whether to render the simulation in a window. If True, it will open a window and display the simulation. If False (default), the simulation runs headless. stride (int): Number of simulation steps between action updates. Default is 1. + shader_pack (str): ManiSkill shader pack to use for cameras. Default is "default". + Other ManiSkill shader packs such as "minimal" or "rt" can also be used. """ try: @@ -26,15 +28,17 @@ except ModuleNotFoundError: param render = False param stride = 1 +param shader_pack = "default" simulator ManiSkillSimulator( render=bool(globalParameters.render), stride=int(globalParameters.stride), + shader_pack=globalParameters.shader_pack, ) class ManiskillObject(Object): """Base class for ManiSkill objects. - + Properties: visualFilename (str or None): Path to a visual mesh file (e.g., .glb, .obj, .usdz). visualScale (tuple): Scale factor for the visual mesh as (x, y, z). @@ -52,7 +56,7 @@ class ManiskillObject(Object): class Ground(ManiskillObject): """The default ground plane from ManiSkill. - + This creates a large flat ground plane using ManiSkill's built-in ground builder. The ground is always kinematic (fixed in place). """ @@ -68,10 +72,10 @@ class Ground(ManiskillObject): class Robot(Object): """Robot object (Panda arm by default). - + This class represents a robot in the scene. The robot is not physically generated in the scene (skipGeneration=True) but is used to configure the robot's initial state. - + Properties: uuid (str): Robot type identifier. Default is "panda" for the Franka Emika Panda arm. jointAngles (list): Initial joint angles for the robot. If empty, uses a default configuration. @@ -84,16 +88,17 @@ class Robot(Object): class Camera(OrientedPoint): """Camera configuration for the scene. - + Cameras are not physical objects but define viewpoints for rendering and observation. - + Properties: img_width (int): Image width in pixels. Default is 640. img_height (int): Image height in pixels. Default is 480. fov (float): Field of view in radians. Default is 1.0. near (float): Near clipping plane distance. Default is 0.01. far (float): Far clipping plane distance. Default is 100.0. - shader_pack (str): Shader pack to use for rendering. Default is "rt" (ray tracing). + shader_pack (str): Shader pack to use for rendering. Defaults to the global + ``shader_pack`` parameter, which is "default" unless overridden. """ name: "camera" img_width: 640 @@ -101,5 +106,5 @@ class Camera(OrientedPoint): fov: 1.0 near: 0.01 far: 100.0 - shader_pack: "rt" + shader_pack: globalParameters.shader_pack skipGeneration: True # Don't generate this as a physical object diff --git a/src/scenic/simulators/maniskill/simulator.py b/src/scenic/simulators/maniskill/simulator.py index 3db4bb3d8..66ab9f8be 100644 --- a/src/scenic/simulators/maniskill/simulator.py +++ b/src/scenic/simulators/maniskill/simulator.py @@ -43,12 +43,14 @@ def __init__( robot_uids="none", objects_to_create=[], camera_configs=None, + shader_pack="default", **kwargs, ): self.robot_uids = robot_uids self.scenic_objects = {} self.objects_to_create = objects_to_create self.scene_camera_configs = camera_configs + self.shader_pack = shader_pack super().__init__(*args, robot_uids=robot_uids, **kwargs) @property @@ -71,7 +73,7 @@ def _default_sensor_configs(self): fov=1.0, near=0.01, far=100.0, - shader_pack="rt", + shader_pack=self.shader_pack, ) ] @@ -231,15 +233,20 @@ def compute_normalized_dense_reward(self, obs, action, info: dict): class ManiSkillSimulator(Simulator): """Simulator interface for ManiSkill.""" - def __init__(self, render=False, stride=1): + def __init__(self, render=False, stride=1, shader_pack="default"): super().__init__() self.last_simulation = None self.render = render self.stride = stride + self.shader_pack = shader_pack def createSimulation(self, scene, **kwargs): self.last_simulation = ManiSkillSimulation( - scene, render=self.render, stride=self.stride, **kwargs + scene, + render=self.render, + stride=self.stride, + shader_pack=self.shader_pack, + **kwargs, ) return self.last_simulation @@ -247,7 +254,16 @@ def createSimulation(self, scene, **kwargs): class ManiSkillSimulation(Simulation): """Simulation interface for ManiSkill.""" - def __init__(self, scene, *, timestep=None, render=False, stride=1, **kwargs): + def __init__( + self, + scene, + *, + timestep=None, + render=False, + stride=1, + shader_pack="default", + **kwargs, + ): self.env: Optional[gym.Env] = None self._done = False self._obs = None @@ -260,6 +276,7 @@ def __init__(self, scene, *, timestep=None, render=False, stride=1, **kwargs): self.stride = stride self.stride_idx = 0 self.step_count = 0 + self.shader_pack = shader_pack # Should it be headless (false) or render (true)? self.render = render @@ -299,6 +316,7 @@ def setup(self): render_mode=render_mode, objects_to_create=self.objects_to_create, camera_configs=self.camera_configs, + shader_pack=self.shader_pack, ) self.step_count = 0 @@ -332,12 +350,12 @@ def extract_camera_configs(self): camera_config = CameraConfig( obj.name, pose, - width=getattr(obj, "width", 640), - height=getattr(obj, "height", 480), + width=getattr(obj, "img_width", 640), + height=getattr(obj, "img_height", 480), fov=getattr(obj, "fov", 1.0), near=getattr(obj, "near", 0.01), far=getattr(obj, "far", 100.0), - shader_pack=getattr(obj, "shader_pack", "rt"), + shader_pack=getattr(obj, "shader_pack", self.shader_pack), ) self.camera_configs.append(camera_config) @@ -375,7 +393,7 @@ def step(self): if self.render: viewer = self.env.unwrapped.viewer - # If the GUI window has already been closed, stop the Scenic simulation cleanly. + # If the GUI window has already been closed, stop the simulation cleanly. if viewer is not None and viewer.closed: self.screen = "Dead" return