From 1cd2aa65bce0bdc1a3b3d71247da141107e8949f Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Sun, 16 Nov 2025 20:55:51 -0800 Subject: [PATCH 01/21] Work on OpenScenarioXML Export. --- src/scenic/core/serialization.py | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index a7c52367a..e473032b3 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -364,3 +364,79 @@ def readStr(stream): Serializer.addCodec(str, writeStr, readStr) + +from scenariogeneration import ScenarioGenerator, xosc + + +def toOpenScenario( + scenario, + scene, + simulationResult, + wheelbaseRatio=0.6, + maxSteeringAngle=0.523598775598, + wheelDiameter=0.8, + trackWidth=1.68, + groundClearance=0.4, + maxSpeed=69, + maxAcceleration=10, + maxDeceleration=10, +): + # Create catalog + xosc_catalog = xosc.Catalog() + + # Extract map + assert "map" in scenario.params + map_path = scenario.params["map"] + xosc_road = xosc.RoadNetwork(roadfile=map_path) + + # Create entitities + entities = xosc.Entities() + xosc_objects = [] + for obj_i, obj in enumerate(scene.objects): + veh_name = obj.name if hasattr(obj, "name") else f"Vehicle_{obj_i}" + # NOTE: XOSC coordinate system swaps X and Y compared to Scenic. + veh_bb = xosc.BoundingBox( + obj.length, + obj.width, + obj.height, + 0.5 * wheelbaseRatio * obj.length, + 0, + obj.height / 2, + ) + veh_fa = xosc.Axle( + maxSteeringAngle, + wheelDiameter, + trackWidth, + wheelbaseRatio * obj.length, + groundClearance, + ) + veh_ra = xosc.Axle( + maxSteeringAngle, wheelDiameter, trackWidth, 0, groundClearance + ) + xosc_veh = xosc.Vehicle( + name=veh_name, + vehicle_type=xosc.VehicleCategory.car, + boundingbox=veh_bb, + frontaxle=veh_fa, + rearaxle=veh_ra, + max_speed=maxSpeed, + max_acceleration=maxAcceleration, + max_deceleration=maxDeceleration, + mass=None, + model3d=None, + max_acceleration_rate=None, + max_deceleration_rate=None, + role=None, + ) + xosc_objects.append(xosc_veh) + entities.add_scenario_object(veh_name, xosc_veh) + + # Create init + init = xosc.Init() + + for xosc_obj in xosc_objects: + breakpoint() + obj_init_action = xosc.TeleportAction(xosc.LanePosition(25, 0, -1, 1)) + init.add_init_action(xosc_obj.name, obj_init_action) + + assert False From 8c056c2a07e915da540073a82836d7fc8c67cfb8 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 18 Nov 2025 15:09:52 -0800 Subject: [PATCH 02/21] Initial OpenScenarioXML export implementation. --- src/scenic/core/regions.py | 3 + src/scenic/core/serialization.py | 110 ++++++++++++++++++++++++++++--- src/scenic/core/simulators.py | 14 +++- 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 38e876d01..00cb8bca5 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3695,6 +3695,9 @@ def distanceTo(self, point) -> float: dist2D = self.lineString.distance(makeShapelyPoint(point)) return math.hypot(dist2D, point.z) + def distanceAlong(self, point) -> float: + return shapely.line_locate_point(self.lineString, makeShapelyPoint(point)) + def projectVector(self, point, onDirection): raise TypeError('PolylineRegion does not support projection using "on"') diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index e473032b3..e869b2358 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -7,6 +7,7 @@ import io import math +import os import pickle import struct import types @@ -372,6 +373,8 @@ def toOpenScenario( scenario, scene, simulationResult, + mapPath=None, + scenarioName="ScenicScenario", wheelbaseRatio=0.6, maxSteeringAngle=0.523598775598, wheelDiameter=0.8, @@ -384,22 +387,27 @@ def toOpenScenario( # Create catalog xosc_catalog = xosc.Catalog() + # Create parameters + xosc_paramdec = xosc.ParameterDeclarations() + # Extract map assert "map" in scenario.params - map_path = scenario.params["map"] + map_path = mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) xosc_road = xosc.RoadNetwork(roadfile=map_path) + # network = scenario.dynamicScenario._dummyNamespace["network"] + # Create entitities entities = xosc.Entities() - xosc_objects = [] + xosc_objects = {} for obj_i, obj in enumerate(scene.objects): - veh_name = obj.name if hasattr(obj, "name") else f"Vehicle_{obj_i}" + veh_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" # NOTE: XOSC coordinate system swaps X and Y compared to Scenic. veh_bb = xosc.BoundingBox( obj.length, obj.width, obj.height, - 0.5 * wheelbaseRatio * obj.length, + 0, 0, obj.height / 2, ) @@ -428,15 +436,99 @@ def toOpenScenario( max_deceleration_rate=None, role=None, ) - xosc_objects.append(xosc_veh) + xosc_objects[obj] = xosc_veh entities.add_scenario_object(veh_name, xosc_veh) # Create init init = xosc.Init() - for xosc_obj in xosc_objects: - breakpoint() - obj_init_action = xosc.TeleportAction(xosc.LanePosition(25, 0, -1, 1)) + for obj, xosc_obj in xosc_objects.items(): + init_position = xosc.WorldPosition(x=obj.x, y=obj.y, z=obj.z, h=obj.heading) + obj_init_action = xosc.TeleportAction(init_position) init.add_init_action(xosc_obj.name, obj_init_action) - assert False + # Dynamics + xosc_act = xosc.Act( + "MainAct", + xosc.ValueTrigger( + "StartSimulation", + 0, + xosc.ConditionEdge.none, + xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), + ), + ) + + for obj_i, (obj, xosc_obj) in enumerate(xosc_objects.items()): + action_times = [] + action_positions = [] + for t, states in enumerate(simulationResult.trajectory): + state_position = states.positions[obj_i] + state_orientation = states.orientations[obj_i].yaw + action_times.append(simulationResult.timestep * t) + pos = xosc.WorldPosition( + x=state_position.x, + y=state_position.y, + z=state_position.z, + h=state_orientation, + ) + action_positions.append(pos) + + polyline = xosc.Polyline(time=action_times, positions=action_positions) + trajectory = xosc.Trajectory(name=f"Trajectory_{xosc_obj.name}", closed=False) + trajectory.add_shape(polyline) + + traj_action = xosc.FollowTrajectoryAction( + trajectory=trajectory, + following_mode=xosc.FollowingMode.position, + reference_domain=xosc.ReferenceContext.absolute, + scale=1, + offset=0, + ) + + event = xosc.Event(f"Event_{xosc_obj.name}", xosc.Priority.override) + event.add_trigger( + xosc.ValueTrigger( + f"TimeTrigger_{xosc_obj.name}_{t}", + 0, + xosc.ConditionEdge.none, + xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), + ) + ) + event.add_action(f"Action_{xosc_obj.name}", action=traj_action) + + maneuver = xosc.Maneuver("Maneuver_{xosc_obj.name}") + maneuver.add_event(event) + + manuever_group = xosc.ManeuverGroup(f"ManeuverGroup_{xosc_obj.name}") + manuever_group.add_maneuver(maneuver) + manuever_group.add_actor(xosc_obj.name) + + xosc_act.add_maneuver_group(manuever_group) + + # Create storyboard + xosc_sb = xosc.StoryBoard( + init, + xosc.ValueTrigger( + "StopSimulation", + 0, + xosc.ConditionEdge.rising, + xosc.SimulationTimeCondition( + simulationResult.currentRealTime, xosc.Rule.greaterThan + ), + "stop", + ), + ) + xosc_sb.add_act(xosc_act) + + # Create scenario + xosc_scenario = xosc.Scenario( + scenarioName, + "Scenic", + xosc_paramdec, + entities=entities, + storyboard=xosc_sb, + roadnetwork=xosc_road, + catalog=xosc_catalog, + ) + + return xosc_scenario diff --git a/src/scenic/core/simulators.py b/src/scenic/core/simulators.py index 3e9c0308f..f662c451d 100644 --- a/src/scenic/core/simulators.py +++ b/src/scenic/core/simulators.py @@ -811,7 +811,19 @@ def currentState(self): The default implementation returns a tuple of the positions of all objects. """ - return tuple(obj.position for obj in self.objects) + + class SimulationState(tuple): + def __new__(cls, positions, orientations): + return super().__new__(cls, positions) + + def __init__(self, positions, orientations): + self.positions = positions + self.orientations = orientations + + positions = tuple(obj.position for obj in self.objects) + orientation = tuple(obj.orientation for obj in self.objects) + + return SimulationState(positions, orientation) @property def currentRealTime(self): From 048d3f00d9229e9c165703df33e8829e9178d184 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 19 Nov 2025 09:28:24 -0800 Subject: [PATCH 03/21] Tidying up --- src/scenic/core/regions.py | 3 --- src/scenic/core/serialization.py | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 00cb8bca5..38e876d01 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3695,9 +3695,6 @@ def distanceTo(self, point) -> float: dist2D = self.lineString.distance(makeShapelyPoint(point)) return math.hypot(dist2D, point.z) - def distanceAlong(self, point) -> float: - return shapely.line_locate_point(self.lineString, makeShapelyPoint(point)) - def projectVector(self, point, onDirection): raise TypeError('PolylineRegion does not support projection using "on"') diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index e869b2358..006fdf925 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -11,6 +11,7 @@ import pickle import struct import types +import warnings from scenic.core.distributions import Samplable, needsSampling from scenic.core.utils import DefaultIdentityDict @@ -395,12 +396,14 @@ def toOpenScenario( map_path = mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) xosc_road = xosc.RoadNetwork(roadfile=map_path) - # network = scenario.dynamicScenario._dummyNamespace["network"] - # Create entitities entities = xosc.Entities() xosc_objects = {} for obj_i, obj in enumerate(scene.objects): + if not hasattr(obj, "isVehicle") or not obj.isVehicle: + warnings.warn("Non-vehicle object {} is ignored.") + continue + veh_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" # NOTE: XOSC coordinate system swaps X and Y compared to Scenic. veh_bb = xosc.BoundingBox( @@ -488,7 +491,7 @@ def toOpenScenario( event = xosc.Event(f"Event_{xosc_obj.name}", xosc.Priority.override) event.add_trigger( xosc.ValueTrigger( - f"TimeTrigger_{xosc_obj.name}_{t}", + f"TimeTrigger_{xosc_obj.name}", 0, xosc.ConditionEdge.none, xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), From 6ff049afa9eca8eb99baf3d4af52564ba7e9a872 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 25 Nov 2025 13:11:49 -0800 Subject: [PATCH 04/21] Fixed coordinate system --- src/scenic/core/serialization.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 006fdf925..731589fcb 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -405,10 +405,9 @@ def toOpenScenario( continue veh_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" - # NOTE: XOSC coordinate system swaps X and Y compared to Scenic. veh_bb = xosc.BoundingBox( - obj.length, obj.width, + obj.length, obj.height, 0, 0, @@ -446,7 +445,9 @@ def toOpenScenario( init = xosc.Init() for obj, xosc_obj in xosc_objects.items(): - init_position = xosc.WorldPosition(x=obj.x, y=obj.y, z=obj.z, h=obj.heading) + init_position = xosc.WorldPosition( + x=obj.x, y=obj.y, z=obj.z, h=obj.heading + math.radians(90) + ) obj_init_action = xosc.TeleportAction(init_position) init.add_init_action(xosc_obj.name, obj_init_action) @@ -466,7 +467,7 @@ def toOpenScenario( action_positions = [] for t, states in enumerate(simulationResult.trajectory): state_position = states.positions[obj_i] - state_orientation = states.orientations[obj_i].yaw + state_orientation = states.orientations[obj_i].yaw + math.radians(90) action_times.append(simulationResult.timestep * t) pos = xosc.WorldPosition( x=state_position.x, From 0b45e3d4ade8c101b8f26bb90115bb25f4777615 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 25 Nov 2025 13:43:39 -0800 Subject: [PATCH 05/21] Updated dependencies and fixed wheelbase offset --- pyproject.toml | 4 ++++ src/scenic/core/serialization.py | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b1ade058c..cc988206a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,9 @@ metadrive = [ "metadrive-simulator@git+https://github.com/metadriverse/metadrive.git@85e5dadc6c7436d324348f6e3d8f8e680c06b4db", "sumolib >= 1.21.0", ] +openscenario = [ + "scenariogeneration" +] test = [ # minimum dependencies for running tests (used for tox virtualenvs) "pytest >= 7.0.0, <9", "pytest-cov >= 3.0.0", @@ -68,6 +71,7 @@ test-full = [ # like 'test' but adds dependencies for optional features "scenic[test]", # all dependencies from 'test' extra above "scenic[guideways]", # for running guideways modules "scenic[metadrive]", + "scenic[openscenario]", "astor >= 0.8.1", 'carla >= 0.9.12; python_version <= "3.10" and (platform_system == "Linux" or platform_system == "Windows")', "dill", diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 731589fcb..b4a66d516 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -15,6 +15,7 @@ from scenic.core.distributions import Samplable, needsSampling from scenic.core.utils import DefaultIdentityDict +from scenic.core.vectors import Vector ## JSON @@ -411,7 +412,7 @@ def toOpenScenario( obj.height, 0, 0, - obj.height / 2, + 0, ) veh_fa = xosc.Axle( maxSteeringAngle, @@ -445,8 +446,16 @@ def toOpenScenario( init = xosc.Init() for obj, xosc_obj in xosc_objects.items(): + scenic_yaw = obj.yaw + state_orientation = scenic_yaw + math.radians(90) + state_position = obj.position.offsetRotated( + scenic_yaw, Vector(0, -0.5 * wheelbaseRatio * obj.length, 0) + ) init_position = xosc.WorldPosition( - x=obj.x, y=obj.y, z=obj.z, h=obj.heading + math.radians(90) + x=state_position.x, + y=state_position.y, + z=state_position.z, + h=state_orientation, ) obj_init_action = xosc.TeleportAction(init_position) init.add_init_action(xosc_obj.name, obj_init_action) @@ -466,8 +475,11 @@ def toOpenScenario( action_times = [] action_positions = [] for t, states in enumerate(simulationResult.trajectory): - state_position = states.positions[obj_i] - state_orientation = states.orientations[obj_i].yaw + math.radians(90) + scenic_yaw = states.orientations[obj_i].yaw + state_orientation = scenic_yaw + math.radians(90) + state_position = states.positions[obj_i].offsetRotated( + scenic_yaw, Vector(0, -0.5 * wheelbaseRatio * obj.length, 0) + ) action_times.append(simulationResult.timestep * t) pos = xosc.WorldPosition( x=state_position.x, From 8ecd602149f8f06b6ae299f344603a2ed81dfffb Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Mon, 15 Dec 2025 18:45:02 -0800 Subject: [PATCH 06/21] Added pedestrians to OpenScenarioXML export --- src/scenic/core/serialization.py | 96 +++++++++++++++---------- src/scenic/domains/driving/model.scenic | 8 +++ 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index b4a66d516..bba11549d 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -385,6 +385,7 @@ def toOpenScenario( maxSpeed=69, maxAcceleration=10, maxDeceleration=10, + pedestrianMass=65, ): # Create catalog xosc_catalog = xosc.Catalog() @@ -401,46 +402,65 @@ def toOpenScenario( entities = xosc.Entities() xosc_objects = {} for obj_i, obj in enumerate(scene.objects): - if not hasattr(obj, "isVehicle") or not obj.isVehicle: - warnings.warn("Non-vehicle object {} is ignored.") + if hasattr(obj, "isVehicle") and obj.isVehicle: + obj_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" + veh_bb = xosc.BoundingBox( + obj.width, + obj.length, + obj.height, + 0, + 0, + 0, + ) + veh_fa = xosc.Axle( + maxSteeringAngle, + wheelDiameter, + trackWidth, + wheelbaseRatio * obj.length, + groundClearance, + ) + veh_ra = xosc.Axle( + maxSteeringAngle, wheelDiameter, trackWidth, 0, groundClearance + ) + xosc_obj = xosc.Vehicle( + name=obj_name, + vehicle_type=xosc.VehicleCategory.car, + boundingbox=veh_bb, + frontaxle=veh_fa, + rearaxle=veh_ra, + max_speed=maxSpeed, + max_acceleration=maxAcceleration, + max_deceleration=maxDeceleration, + mass=None, + model3d=None, + max_acceleration_rate=None, + max_deceleration_rate=None, + role=None, + ) + elif hasattr(obj, "isPedestrian") and obj.isPedestrian: + obj_name = obj.name if hasattr(obj, "name") else f"Pedestrian{obj_i}" + ped_bb = xosc.BoundingBox( + obj.width, + obj.length, + obj.height, + 0, + 0, + 0, + ) + xosc_obj = xosc.Pedestrian( + name=obj_name, + mass=pedestrianMass, + boundingbox=ped_bb, + category=xosc.PedestrianCategory.pedestrian, + model=None, + role=None, + ) + else: + warnings.warn(f"Unknown object {obj} is ignored.") continue - veh_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" - veh_bb = xosc.BoundingBox( - obj.width, - obj.length, - obj.height, - 0, - 0, - 0, - ) - veh_fa = xosc.Axle( - maxSteeringAngle, - wheelDiameter, - trackWidth, - wheelbaseRatio * obj.length, - groundClearance, - ) - veh_ra = xosc.Axle( - maxSteeringAngle, wheelDiameter, trackWidth, 0, groundClearance - ) - xosc_veh = xosc.Vehicle( - name=veh_name, - vehicle_type=xosc.VehicleCategory.car, - boundingbox=veh_bb, - frontaxle=veh_fa, - rearaxle=veh_ra, - max_speed=maxSpeed, - max_acceleration=maxAcceleration, - max_deceleration=maxDeceleration, - mass=None, - model3d=None, - max_acceleration_rate=None, - max_deceleration_rate=None, - role=None, - ) - xosc_objects[obj] = xosc_veh - entities.add_scenario_object(veh_name, xosc_veh) + xosc_objects[obj] = xosc_obj + entities.add_scenario_object(obj_name, xosc_obj) # Create init init = xosc.Init() diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index b6d0754af..4a3219a56 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -133,6 +133,10 @@ class DrivingObject: def isVehicle(self): return False + @property + def isPedestrian(self): + return False + @property def isCar(self): return False @@ -325,6 +329,10 @@ class Pedestrian(DrivingObject): length: 0.75 color: [0, 0.5, 1] + @property + def isPedestrian(self): + return True + ## Utility functions def withinDistanceToAnyCars(car, thresholdDistance): From 5e2b7e33d84c428b26212033fa49cb28c224fedf Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 28 May 2026 20:07:44 -0700 Subject: [PATCH 07/21] XOSC export improvements, fixes, and test. --- src/scenic/core/serialization.py | 80 ++++++++++---------- src/scenic/domains/driving/model.scenic | 28 +++++++ tests/core/test_serialization.py | 50 +++++++++++- tests/simulators/metadrive/test_metadrive.py | 5 +- 4 files changed, 120 insertions(+), 43 deletions(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 0dab3290c..ab7058a7e 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -403,15 +403,6 @@ def toOpenScenario( simulationResult, mapPath=None, scenarioName="ScenicScenario", - wheelbaseRatio=0.6, - maxSteeringAngle=0.523598775598, - wheelDiameter=0.8, - trackWidth=1.68, - groundClearance=0.4, - maxSpeed=69, - maxAcceleration=10, - maxDeceleration=10, - pedestrianMass=65, ): try: import scenariogeneration @@ -447,14 +438,18 @@ def toOpenScenario( 0, ) veh_fa = xosc.Axle( - maxSteeringAngle, - wheelDiameter, - trackWidth, - wheelbaseRatio * obj.length, - groundClearance, + obj.maxSteeringAngle, + obj.wheelDiameter, + obj.trackWidth, + obj.wheelbase, + obj.groundClearance, ) veh_ra = xosc.Axle( - maxSteeringAngle, wheelDiameter, trackWidth, 0, groundClearance + obj.maxSteeringAngle, + obj.wheelDiameter, + obj.trackWidth, + 0, + obj.groundClearance, ) xosc_obj = xosc.Vehicle( name=obj_name, @@ -462,9 +457,9 @@ def toOpenScenario( boundingbox=veh_bb, frontaxle=veh_fa, rearaxle=veh_ra, - max_speed=maxSpeed, - max_acceleration=maxAcceleration, - max_deceleration=maxDeceleration, + max_speed=obj.maxSpeed, + max_acceleration=obj.maxAcceleration, + max_deceleration=obj.maxDeceleration, mass=None, model3d=None, max_acceleration_rate=None, @@ -483,35 +478,45 @@ def toOpenScenario( ) xosc_obj = xosc.Pedestrian( name=obj_name, - mass=pedestrianMass, + mass=obj.mass, boundingbox=ped_bb, category=xosc.PedestrianCategory.pedestrian, model=None, role=None, ) else: - warnings.warn(f"Unknown object {obj} is ignored.") + warnings.warn( + f"Object {obj} of unsupported type is being ignored during XOSC export." + ) continue xosc_objects[obj] = xosc_obj entities.add_scenario_object(obj_name, xosc_obj) - # Create init - init = xosc.Init() - - for obj, xosc_obj in xosc_objects.items(): - scenic_yaw = obj.yaw - state_orientation = scenic_yaw + math.radians(90) - state_position = obj.position.offsetRotated( - scenic_yaw, Vector(0, -0.5 * wheelbaseRatio * obj.length, 0) + # Helper function + def pos_to_WorldPosition(obj, pos, yaw): + # XOSC Reference point is back axle, so we must translate Scenic's + # convention to this. + state_position = ( + pos.offsetRotated(yaw, Vector(0, -0.5 * obj.wheelbase, 0)) + if obj.isVehicle + else pos ) - init_position = xosc.WorldPosition( + state_orientation = yaw + math.radians(90) + return xosc.WorldPosition( x=state_position.x, y=state_position.y, z=state_position.z, h=state_orientation, ) - obj_init_action = xosc.TeleportAction(init_position) + + # Initial states + init = xosc.Init() + + for obj, xosc_obj in xosc_objects.items(): + obj_init_action = xosc.TeleportAction( + pos_to_WorldPosition(obj, obj.position, obj.yaw) + ) init.add_init_action(xosc_obj.name, obj_init_action) # Dynamics @@ -529,19 +534,12 @@ def toOpenScenario( action_times = [] action_positions = [] for t, states in enumerate(simulationResult.trajectory): - scenic_yaw = states.orientations[obj_i].yaw - state_orientation = scenic_yaw + math.radians(90) - state_position = states.positions[obj_i].offsetRotated( - scenic_yaw, Vector(0, -0.5 * wheelbaseRatio * obj.length, 0) + action_positions.append( + pos_to_WorldPosition( + obj, states.positions[obj_i], states.orientations[obj_i].yaw + ) ) action_times.append(simulationResult.timestep * t) - pos = xosc.WorldPosition( - x=state_position.x, - y=state_position.y, - z=state_position.z, - h=state_orientation, - ) - action_positions.append(pos) polyline = xosc.Polyline(time=action_times, positions=action_positions) trajectory = xosc.Trajectory(name=f"Trajectory_{xosc_obj.name}", closed=False) diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index 7652b43cb..8db0c2805 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -286,6 +286,24 @@ class Vehicle(DrivingObject): color (:obj:`Color` or RGB tuple): Color of the vehicle. The default value is a distribution derived from car color popularity statistics; see :obj:`Color.defaultCarColor`. + wheelbase: The distance between the front and rear axles of the vehicle. Default value is 0.6 + times the length of the vehicle. + maxSteeringAngle: The maximum steering angle of the vehicle. The full steering range would be + two times this value, going from (-maxSteeringAngle, maxSteeringAngle). Default value + 30 degrees. + wheelDiameter: The diameter of the *entire* wheel (including the tire). Default value is 0.7 meters. + trackWidth: Distance between the vehicle's wheels when pointed straight ahead. Default value + is 0.85 times the width of the vehicle. + groundClearance: Default value is half the wheel diameter. + maxSpeed: The maximum rated speed of the vehicle. Default value is 45 meters per second (~100 mph). + This value is not enforced by Scenic and is provided simply for other tools to reference (e.g. + exporting to OpenScenarioXML). + maxAcceleration: The maximum rated acceleration of the vehicle. Default value is 5 meters per second^2. + This value is not enforced by Scenic and is provided simply for other tools to reference (e.g. + exporting to OpenScenarioXML). + maxDeceleration: The maximum rated deceleration of the vehicle. Default value is 10 meters per second^2. + This value is not enforced by Scenic and is provided simply for other tools to reference (e.g. + exporting to OpenScenarioXML). """ regionContainedIn: roadOrShoulder position: new Point on road @@ -295,6 +313,14 @@ class Vehicle(DrivingObject): width: 2 length: 4.5 color: Color.defaultCarColor() + wheelbase: 0.6*self.length + maxSteeringAngle: 35 deg + wheelDiameter: 0.7 + trackWidth: 0.85*self.width + groundClearance: 0.5*self.wheelDiameter + maxSpeed: 45 + maxAcceleration: 5 + maxDeceleration: 10 @property def isVehicle(self): @@ -321,6 +347,7 @@ class Pedestrian(DrivingObject): length: The default length is 0.75 m. color: The default color is turquoise. Pedestrian colors are not necessarily used by simulators, but do appear in the debugging diagram. + mass: Default value is 65 kg. """ regionContainedIn: network.walkableRegion position: new Point on network.walkableRegion @@ -329,6 +356,7 @@ class Pedestrian(DrivingObject): width: 0.75 length: 0.75 color: [0, 0.5, 1] + mass: 65 @property def isPedestrian(self): diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index 6ae1d1c95..e7b88ccc2 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -13,8 +13,14 @@ import numpy import pytest -from scenic.core.serialization import SerializationError, Serializer, deterministicHash +from scenic.core.serialization import ( + SerializationError, + Serializer, + deterministicHash, + toOpenScenario, +) from scenic.core.simulators import DivergenceError, DummySimulator +from tests.simulators.metadrive.test_metadrive import getMetadriveSimulator from tests.utils import ( areEquivalent, compileScenic, @@ -507,3 +513,45 @@ class Foo: digest2 = deterministicHash(mapping2) # Non-scalar values should hash in a stable way, independent of identity. assert digest1 == digest2 + + +def test_xosc_export(getMetadriveSimulator): + simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") + code = f""" + param map = r'{openDrivePath}' + param sumo_map = r'{sumoPath}' + + model scenic.simulators.metadrive.model + + behavior DriveAndBrakeForPedestrians(): + try: + do FollowLaneBehavior() + interrupt when withinDistanceToAnyPedestrians(self, 10): + take SetThrottleAction(0), SetBrakeAction(1) + + behavior CrossRoad(): + while distance from self to ego > 15: + wait + take SetWalkingDirectionAction(self.heading), SetWalkingSpeedAction(1) + + ego = new Car with behavior DriveAndBrakeForPedestrians() + + rightCurb = ego.laneGroup.curb + spot = new OrientedPoint on visible rightCurb + + parkedCar = new Car right of spot by 1, with regionContainedIn None + + require distance from ego to parkedCar > 30 + + new Pedestrian ahead of parkedCar by 3, + facing 90 deg relative to parkedCar, + with behavior CrossRoad() + + terminate after 30 seconds + """ + + scenario = compileScenic(code, mode2D=True, params={"map": openDrivePath}) + scene, _ = scenario.generate() + simulationResult = simulator.simulate(scene) + assert simulationResult is not None + xosc_scenario = toOpenScenario(scenario, scene, simulationResult) diff --git a/tests/simulators/metadrive/test_metadrive.py b/tests/simulators/metadrive/test_metadrive.py index 1c1ea9ad1..4fe573d2e 100644 --- a/tests/simulators/metadrive/test_metadrive.py +++ b/tests/simulators/metadrive/test_metadrive.py @@ -84,7 +84,9 @@ def test_pickle(loadLocalScenario): def getMetadriveSimulator(getAssetPath): base = getAssetPath("maps/CARLA") - def _getMetadriveSimulator(town, *, render=False, render3D=False, **kwargs): + def _getMetadriveSimulator( + town, *, render=False, render3D=False, real_time=False, **kwargs + ): openDrivePath = os.path.join(base, f"{town}.xodr") sumoPath = os.path.join(base, f"{town}.net.xml") simulator = MetaDriveSimulator( @@ -92,6 +94,7 @@ def _getMetadriveSimulator(town, *, render=False, render3D=False, **kwargs): xodr_map=openDrivePath, render=render, render3D=render3D, + real_time=real_time, **kwargs, ) return simulator, openDrivePath, sumoPath From 743cf43ce3f61800acc2f3abdcc33a6b1fee6a96 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 28 May 2026 20:10:51 -0700 Subject: [PATCH 08/21] Fixed metadrive import? --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2aace9537..6f6cdf907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ test = [ # minimum dependencies for running tests (used for tox virtualenvs) test-full = [ # like 'test' but adds dependencies for optional features "scenic[test]", # all dependencies from 'test' extra above "scenic[guideways]", # for running guideways modules - "scenic[metadrive]", "scenic[openscenario]", "astor >= 0.8.1", 'carla >= 0.9.12; python_version <= "3.12" and (platform_system == "Linux" or platform_system == "Windows")', From 992be2554b0519fe112ee5ad2ecb01d3ff68c1cd Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 28 May 2026 20:16:07 -0700 Subject: [PATCH 09/21] Tweaked Metadrive real_time default. --- src/scenic/simulators/metadrive/simulator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/scenic/simulators/metadrive/simulator.py b/src/scenic/simulators/metadrive/simulator.py index 76607d5bc..075134f55 100644 --- a/src/scenic/simulators/metadrive/simulator.py +++ b/src/scenic/simulators/metadrive/simulator.py @@ -39,7 +39,7 @@ def __init__( timestep=0.1, render=True, render3D=False, - real_time=True, + real_time=None, screen_record=False, screen_record_filename=None, screen_record_path="metadrive_gifs", @@ -51,7 +51,10 @@ def __init__( self.timestep = timestep self.sumo_map = sumo_map self.xodr_map = xodr_map - self.real_time = real_time + if real_time is None: + self.real_time = self.render or self.render3D + else: + self.real_time = real_time self.screen_record = screen_record self.screen_record_filename = screen_record_filename self.screen_record_path = screen_record_path From 15c729a59c114340a3afd1f9830c2b894951b337 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 28 May 2026 20:38:41 -0700 Subject: [PATCH 10/21] Added documentation --- docs/api.rst | 7 +++++++ docs/simulators.rst | 5 ++++- src/scenic/core/serialization.py | 22 +++++++++++++++++++--- tests/core/test_serialization.py | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 440f450f2..5cfe99f09 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -173,6 +173,13 @@ it to the replay, but this greatly increases the size of the encoded simulation. can return to one later for further analysis), but it is not guaranteed to be compatible across major versions of Scenic. +.. _xosc_export: +OpenScenarioXML Export +---------------------- +Scenic provides experimental support for exporting completed simulations via `toOpenScenario`. +This function currently only supports cars and pedestrians, and may be subject to breaking changes +in the future. + .. seealso:: If you get exceptions or unexpected behavior when using the API, Scenic provides various debugging features: see :ref:`debugging`. .. rubric:: Footnotes diff --git a/docs/simulators.rst b/docs/simulators.rst index ee5d0b0d1..5a966660b 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -14,6 +14,10 @@ See the individual entries for details on each interface's capabilities and how While Scenic aims to support multiple Python versions, some simulators may have more limited compatibility. Be sure to check the documentation of each simulator to confirm which Python versions are supported. +.. note:: + Scenic also supports outputing data in formats that may be imported into other simulators and tools (e.g. :ref:`xosc_export`). + For more details, see :ref:`serialization`. + .. contents:: List of Simulators :local: @@ -163,7 +167,6 @@ This interface is part of the VerifAI toolkit; documentation and examples can be .. _VerifAI repository: https://github.com/BerkeleyLearnVerify/VerifAI - Deprecated ========== diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index ab7058a7e..77fb68938 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -398,12 +398,22 @@ def readStr(stream): def toOpenScenario( + simulationResult, scenario, scene, - simulationResult, mapPath=None, scenarioName="ScenicScenario", ): + """Export a `SimulationResult` as a `scenariogeneration` `xosc` object. + + Args: + simulationResult: The `SimulationResult` to be exported to XOSC + scenario: The scenario from which simulationResult was sampled. + scene: The scene from which simulationResult was sampled. + mapPath: The path to the XODR map used to run the simulation. If + one is not provided the `map` param of the scenario is used. + scenarioName: The name of the scenario in the generated XOSC file. + """ try: import scenariogeneration from scenariogeneration import ScenarioGenerator, xosc @@ -419,8 +429,14 @@ def toOpenScenario( xosc_paramdec = xosc.ParameterDeclarations() # Extract map - assert "map" in scenario.params - map_path = mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) + if map_path is None: + if "map" not in scenario.params: + raise ValueError( + "No `mapPath` provided and scenario does not have a `map` parameter defined." + ) + map_path = ( + mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) + ) xosc_road = xosc.RoadNetwork(roadfile=map_path) # Create entitities diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index e7b88ccc2..c4ca8ca0d 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -554,4 +554,4 @@ def test_xosc_export(getMetadriveSimulator): scene, _ = scenario.generate() simulationResult = simulator.simulate(scene) assert simulationResult is not None - xosc_scenario = toOpenScenario(scenario, scene, simulationResult) + xosc_scenario = toOpenScenario(simulationResult, scenario, scene) From ab311ca44bc4bf302ba32ed4b3bb5dc2a16673ee Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 28 May 2026 20:45:31 -0700 Subject: [PATCH 11/21] Minor fixes. --- src/scenic/core/serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 77fb68938..3e339699c 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -429,15 +429,15 @@ def toOpenScenario( xosc_paramdec = xosc.ParameterDeclarations() # Extract map - if map_path is None: + if mapPath is None: if "map" not in scenario.params: raise ValueError( "No `mapPath` provided and scenario does not have a `map` parameter defined." ) - map_path = ( + mapPath = ( mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) ) - xosc_road = xosc.RoadNetwork(roadfile=map_path) + xosc_road = xosc.RoadNetwork(roadfile=mapPath) # Create entitities entities = xosc.Entities() From 54a2a5eb8b87922ca0e99f953f6e07ed6fe65376 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Fri, 29 May 2026 06:46:18 -0700 Subject: [PATCH 12/21] Fixed blank line --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 5cfe99f09..23ac150e7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -176,6 +176,7 @@ it to the replay, but this greatly increases the size of the encoded simulation. .. _xosc_export: OpenScenarioXML Export ---------------------- + Scenic provides experimental support for exporting completed simulations via `toOpenScenario`. This function currently only supports cars and pedestrians, and may be subject to breaking changes in the future. From 25fe1a108ae56beefba1d2fc5ea039fd627525de Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Fri, 29 May 2026 06:56:39 -0700 Subject: [PATCH 13/21] Another blank line? --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 23ac150e7..7f33c12d2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -174,6 +174,7 @@ it to the replay, but this greatly increases the size of the encoded simulation. compatible across major versions of Scenic. .. _xosc_export: + OpenScenarioXML Export ---------------------- From 518c30810aedbac9a2331b6bbbd3f850cf19cb9a Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Fri, 29 May 2026 07:09:24 -0700 Subject: [PATCH 14/21] Fix scenariogeneration link --- src/scenic/core/serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 3e339699c..4b0ed9dc7 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -404,7 +404,7 @@ def toOpenScenario( mapPath=None, scenarioName="ScenicScenario", ): - """Export a `SimulationResult` as a `scenariogeneration` `xosc` object. + """Export a `SimulationResult` as a `scenariogeneration.xosc` object. Args: simulationResult: The `SimulationResult` to be exported to XOSC From 96b3ee1bd6419196117a1effe94bb6cc959e4226 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Fri, 29 May 2026 08:09:14 -0700 Subject: [PATCH 15/21] Fixed link --- src/scenic/core/serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 4b0ed9dc7..97c083734 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -404,7 +404,7 @@ def toOpenScenario( mapPath=None, scenarioName="ScenicScenario", ): - """Export a `SimulationResult` as a `scenariogeneration.xosc` object. + """Export a `SimulationResult` as a `scenariogeneration.xosc.scenario `_ object. Args: simulationResult: The `SimulationResult` to be exported to XOSC From 70d6b324d71ac854bb0939ca41f95daa6bbbfef5 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Mon, 22 Jun 2026 17:32:39 -0700 Subject: [PATCH 16/21] PR revisions --- src/scenic/core/serialization.py | 37 +++++++++--------- src/scenic/core/simulators.py | 11 ++++-- src/scenic/domains/driving/model.scenic | 5 ++- tests/core/test_serialization.py | 52 +++++++++---------------- 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 97c083734..f953279a1 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -398,18 +398,16 @@ def readStr(stream): def toOpenScenario( - simulationResult, + simulation, scenario, - scene, mapPath=None, scenarioName="ScenicScenario", ): - """Export a `SimulationResult` as a `scenariogeneration.xosc.scenario `_ object. + """Export a `Simulation` as a `scenariogeneration.xosc.scenario `_ object. Args: - simulationResult: The `SimulationResult` to be exported to XOSC - scenario: The scenario from which simulationResult was sampled. - scene: The scene from which simulationResult was sampled. + simulation: The `Simulation` to be exported to XOSC + scenario: The scenario from which simulation was sampled. mapPath: The path to the XODR map used to run the simulation. If one is not provided the `map` param of the scenario is used. scenarioName: The name of the scenario in the generated XOSC file. @@ -422,6 +420,11 @@ def toOpenScenario( "The `scenariogeneration` package is required to use Scenic's XOSC export functionality." ) from e + if simulation.result is None: + raise RuntimeError("Cannot export incomplete scenario to OpenScenario XML") + + scene = simulation.scene + # Create catalog xosc_catalog = xosc.Catalog() @@ -434,17 +437,15 @@ def toOpenScenario( raise ValueError( "No `mapPath` provided and scenario does not have a `map` parameter defined." ) - mapPath = ( - mapPath if mapPath is not None else os.path.abspath(scenario.params["map"]) - ) + mapPath = os.path.abspath(scenario.params["map"]) xosc_road = xosc.RoadNetwork(roadfile=mapPath) # Create entitities entities = xosc.Entities() xosc_objects = {} for obj_i, obj in enumerate(scene.objects): - if hasattr(obj, "isVehicle") and obj.isVehicle: - obj_name = obj.name if hasattr(obj, "name") else f"Vehicle{obj_i}" + if getattr(obj, "isVehicle", False): + obj_name = getattr(obj, "name", f"Vehicle{obj_i}") veh_bb = xosc.BoundingBox( obj.width, obj.length, @@ -482,8 +483,8 @@ def toOpenScenario( max_deceleration_rate=None, role=None, ) - elif hasattr(obj, "isPedestrian") and obj.isPedestrian: - obj_name = obj.name if hasattr(obj, "name") else f"Pedestrian{obj_i}" + elif getattr(obj, "isPedestrian", False): + obj_name = getattr(obj, "name", f"Pedestrian{obj_i}") ped_bb = xosc.BoundingBox( obj.width, obj.length, @@ -531,7 +532,7 @@ def pos_to_WorldPosition(obj, pos, yaw): for obj, xosc_obj in xosc_objects.items(): obj_init_action = xosc.TeleportAction( - pos_to_WorldPosition(obj, obj.position, obj.yaw) + pos_to_WorldPosition(obj, obj.position, obj.heading) ) init.add_init_action(xosc_obj.name, obj_init_action) @@ -549,13 +550,13 @@ def pos_to_WorldPosition(obj, pos, yaw): for obj_i, (obj, xosc_obj) in enumerate(xosc_objects.items()): action_times = [] action_positions = [] - for t, states in enumerate(simulationResult.trajectory): + for t, states in enumerate(simulation.trajectory): action_positions.append( pos_to_WorldPosition( - obj, states.positions[obj_i], states.orientations[obj_i].yaw + obj, states.positions[obj_i], states.orientations[obj_i].heading ) ) - action_times.append(simulationResult.timestep * t) + action_times.append(simulation.timestep * t) polyline = xosc.Polyline(time=action_times, positions=action_positions) trajectory = xosc.Trajectory(name=f"Trajectory_{xosc_obj.name}", closed=False) @@ -597,7 +598,7 @@ def pos_to_WorldPosition(obj, pos, yaw): 0, xosc.ConditionEdge.rising, xosc.SimulationTimeCondition( - simulationResult.currentRealTime, xosc.Rule.greaterThan + simulation.currentRealTime, xosc.Rule.greaterThan ), "stop", ), diff --git a/src/scenic/core/simulators.py b/src/scenic/core/simulators.py index f662c451d..c971e6dbe 100644 --- a/src/scenic/core/simulators.py +++ b/src/scenic/core/simulators.py @@ -807,9 +807,14 @@ def currentState(self): """Return the current state of the simulation. The definition of 'state' is up to the simulator; the 'state' is simply saved - at each time step to define the 'trajectory' of the simulation. - - The default implementation returns a tuple of the positions of all objects. + at each time step to define the 'trajectory' of the simulation. Changing this + method is however discouraged, unless one is adding additional attributes to + the returned `SimulationState` object. + + The default implementation returns a custom `SimulationState` object, which is a tuple + of positions of all objects (for backwards compatibility) and also has two attributes: + `positions` and `orientations`, which are themselves tuples of the positions and + orientations of all objects. """ class SimulationState(tuple): diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index 8db0c2805..575609142 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -290,11 +290,12 @@ class Vehicle(DrivingObject): times the length of the vehicle. maxSteeringAngle: The maximum steering angle of the vehicle. The full steering range would be two times this value, going from (-maxSteeringAngle, maxSteeringAngle). Default value - 30 degrees. + 35 degrees. wheelDiameter: The diameter of the *entire* wheel (including the tire). Default value is 0.7 meters. trackWidth: Distance between the vehicle's wheels when pointed straight ahead. Default value is 0.85 times the width of the vehicle. - groundClearance: Default value is half the wheel diameter. + groundClearance: The distance between the bottom of the vehicle's chassis and the ground. Default + value is half the wheel diameter. maxSpeed: The maximum rated speed of the vehicle. Default value is 45 meters per second (~100 mph). This value is not enforced by Scenic and is provided simply for other tools to reference (e.g. exporting to OpenScenarioXML). diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index c4ca8ca0d..1dcb531d4 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -20,7 +20,7 @@ toOpenScenario, ) from scenic.core.simulators import DivergenceError, DummySimulator -from tests.simulators.metadrive.test_metadrive import getMetadriveSimulator +from scenic.simulators.newtonian import NewtonianSimulator from tests.utils import ( areEquivalent, compileScenic, @@ -515,43 +515,29 @@ class Foo: assert digest1 == digest2 -def test_xosc_export(getMetadriveSimulator): - simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") +def test_xosc_export(getAssetPath): + simulator = NewtonianSimulator("Town01") code = f""" - param map = r'{openDrivePath}' - param sumo_map = r'{sumoPath}' + param render = False + model scenic.simulators.newtonian.driving_model - model scenic.simulators.metadrive.model + ego = new Car with behavior FollowLaneBehavior() - behavior DriveAndBrakeForPedestrians(): - try: - do FollowLaneBehavior() - interrupt when withinDistanceToAnyPedestrians(self, 10): - take SetThrottleAction(0), SetBrakeAction(1) + immobileCar = new Car visible - behavior CrossRoad(): - while distance from self to ego > 15: - wait - take SetWalkingDirectionAction(self.heading), SetWalkingSpeedAction(1) + behavior Walk(): + take SetWalkingSpeedAction(1) + + new Pedestrian behind ego by 5, + with regionContainedIn None, + with behavior Walk() - ego = new Car with behavior DriveAndBrakeForPedestrians() - - rightCurb = ego.laneGroup.curb - spot = new OrientedPoint on visible rightCurb - - parkedCar = new Car right of spot by 1, with regionContainedIn None - - require distance from ego to parkedCar > 30 - - new Pedestrian ahead of parkedCar by 3, - facing 90 deg relative to parkedCar, - with behavior CrossRoad() - - terminate after 30 seconds + terminate after 5 seconds """ - scenario = compileScenic(code, mode2D=True, params={"map": openDrivePath}) + scenario = compileScenic( + code, mode2D=True, params={"map": getAssetPath("maps/CARLA/Town01.xodr")} + ) scene, _ = scenario.generate() - simulationResult = simulator.simulate(scene) - assert simulationResult is not None - xosc_scenario = toOpenScenario(simulationResult, scenario, scene) + simulation = simulator.simulate(scene) + xosc_scenario = toOpenScenario(simulation, scenario, scene) From c8df0b10754a212ede2094eefec5a235429a1d26 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Mon, 22 Jun 2026 17:36:59 -0700 Subject: [PATCH 17/21] Fix heading -> yaw --- src/scenic/core/serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index f953279a1..962936a19 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -553,7 +553,7 @@ def pos_to_WorldPosition(obj, pos, yaw): for t, states in enumerate(simulation.trajectory): action_positions.append( pos_to_WorldPosition( - obj, states.positions[obj_i], states.orientations[obj_i].heading + obj, states.positions[obj_i], states.orientations[obj_i].yaw ) ) action_times.append(simulation.timestep * t) From 050a7064cc11f1441d9f371f0c8b801111307f99 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 25 Jun 2026 12:53:14 -0700 Subject: [PATCH 18/21] PR fixes and mode2D fix for modular scenarios. --- src/scenic/core/serialization.py | 223 -------------------- src/scenic/core/simulators.py | 6 +- src/scenic/domains/driving/openscenario.py | 232 +++++++++++++++++++++ src/scenic/syntax/translator.py | 92 ++++---- src/scenic/syntax/veneer.py | 6 +- tests/core/test_serialization.py | 38 +--- tests/domains/driving/test_driving.py | 68 ++++++ 7 files changed, 354 insertions(+), 311 deletions(-) create mode 100644 src/scenic/domains/driving/openscenario.py diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index 962936a19..08fca26eb 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -16,7 +16,6 @@ from scenic.core.distributions import Samplable, needsSampling from scenic.core.utils import DefaultIdentityDict -from scenic.core.vectors import Vector def deterministicHash(mapping, *, digest_size=8): @@ -395,225 +394,3 @@ def readStr(stream): Serializer.addCodec(str, writeStr, readStr) - - -def toOpenScenario( - simulation, - scenario, - mapPath=None, - scenarioName="ScenicScenario", -): - """Export a `Simulation` as a `scenariogeneration.xosc.scenario `_ object. - - Args: - simulation: The `Simulation` to be exported to XOSC - scenario: The scenario from which simulation was sampled. - mapPath: The path to the XODR map used to run the simulation. If - one is not provided the `map` param of the scenario is used. - scenarioName: The name of the scenario in the generated XOSC file. - """ - try: - import scenariogeneration - from scenariogeneration import ScenarioGenerator, xosc - except ModuleNotFoundError as e: - raise ModuleNotFoundError( - "The `scenariogeneration` package is required to use Scenic's XOSC export functionality." - ) from e - - if simulation.result is None: - raise RuntimeError("Cannot export incomplete scenario to OpenScenario XML") - - scene = simulation.scene - - # Create catalog - xosc_catalog = xosc.Catalog() - - # Create parameters - xosc_paramdec = xosc.ParameterDeclarations() - - # Extract map - if mapPath is None: - if "map" not in scenario.params: - raise ValueError( - "No `mapPath` provided and scenario does not have a `map` parameter defined." - ) - mapPath = os.path.abspath(scenario.params["map"]) - xosc_road = xosc.RoadNetwork(roadfile=mapPath) - - # Create entitities - entities = xosc.Entities() - xosc_objects = {} - for obj_i, obj in enumerate(scene.objects): - if getattr(obj, "isVehicle", False): - obj_name = getattr(obj, "name", f"Vehicle{obj_i}") - veh_bb = xosc.BoundingBox( - obj.width, - obj.length, - obj.height, - 0, - 0, - 0, - ) - veh_fa = xosc.Axle( - obj.maxSteeringAngle, - obj.wheelDiameter, - obj.trackWidth, - obj.wheelbase, - obj.groundClearance, - ) - veh_ra = xosc.Axle( - obj.maxSteeringAngle, - obj.wheelDiameter, - obj.trackWidth, - 0, - obj.groundClearance, - ) - xosc_obj = xosc.Vehicle( - name=obj_name, - vehicle_type=xosc.VehicleCategory.car, - boundingbox=veh_bb, - frontaxle=veh_fa, - rearaxle=veh_ra, - max_speed=obj.maxSpeed, - max_acceleration=obj.maxAcceleration, - max_deceleration=obj.maxDeceleration, - mass=None, - model3d=None, - max_acceleration_rate=None, - max_deceleration_rate=None, - role=None, - ) - elif getattr(obj, "isPedestrian", False): - obj_name = getattr(obj, "name", f"Pedestrian{obj_i}") - ped_bb = xosc.BoundingBox( - obj.width, - obj.length, - obj.height, - 0, - 0, - 0, - ) - xosc_obj = xosc.Pedestrian( - name=obj_name, - mass=obj.mass, - boundingbox=ped_bb, - category=xosc.PedestrianCategory.pedestrian, - model=None, - role=None, - ) - else: - warnings.warn( - f"Object {obj} of unsupported type is being ignored during XOSC export." - ) - continue - - xosc_objects[obj] = xosc_obj - entities.add_scenario_object(obj_name, xosc_obj) - - # Helper function - def pos_to_WorldPosition(obj, pos, yaw): - # XOSC Reference point is back axle, so we must translate Scenic's - # convention to this. - state_position = ( - pos.offsetRotated(yaw, Vector(0, -0.5 * obj.wheelbase, 0)) - if obj.isVehicle - else pos - ) - state_orientation = yaw + math.radians(90) - return xosc.WorldPosition( - x=state_position.x, - y=state_position.y, - z=state_position.z, - h=state_orientation, - ) - - # Initial states - init = xosc.Init() - - for obj, xosc_obj in xosc_objects.items(): - obj_init_action = xosc.TeleportAction( - pos_to_WorldPosition(obj, obj.position, obj.heading) - ) - init.add_init_action(xosc_obj.name, obj_init_action) - - # Dynamics - xosc_act = xosc.Act( - "MainAct", - xosc.ValueTrigger( - "StartSimulation", - 0, - xosc.ConditionEdge.none, - xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), - ), - ) - - for obj_i, (obj, xosc_obj) in enumerate(xosc_objects.items()): - action_times = [] - action_positions = [] - for t, states in enumerate(simulation.trajectory): - action_positions.append( - pos_to_WorldPosition( - obj, states.positions[obj_i], states.orientations[obj_i].yaw - ) - ) - action_times.append(simulation.timestep * t) - - polyline = xosc.Polyline(time=action_times, positions=action_positions) - trajectory = xosc.Trajectory(name=f"Trajectory_{xosc_obj.name}", closed=False) - trajectory.add_shape(polyline) - - traj_action = xosc.FollowTrajectoryAction( - trajectory=trajectory, - following_mode=xosc.FollowingMode.position, - reference_domain=xosc.ReferenceContext.absolute, - scale=1, - offset=0, - ) - - event = xosc.Event(f"Event_{xosc_obj.name}", xosc.Priority.override) - event.add_trigger( - xosc.ValueTrigger( - f"TimeTrigger_{xosc_obj.name}", - 0, - xosc.ConditionEdge.none, - xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), - ) - ) - event.add_action(f"Action_{xosc_obj.name}", action=traj_action) - - maneuver = xosc.Maneuver("Maneuver_{xosc_obj.name}") - maneuver.add_event(event) - - manuever_group = xosc.ManeuverGroup(f"ManeuverGroup_{xosc_obj.name}") - manuever_group.add_maneuver(maneuver) - manuever_group.add_actor(xosc_obj.name) - - xosc_act.add_maneuver_group(manuever_group) - - # Create storyboard - xosc_sb = xosc.StoryBoard( - init, - xosc.ValueTrigger( - "StopSimulation", - 0, - xosc.ConditionEdge.rising, - xosc.SimulationTimeCondition( - simulation.currentRealTime, xosc.Rule.greaterThan - ), - "stop", - ), - ) - xosc_sb.add_act(xosc_act) - - # Create scenario - xosc_scenario = xosc.Scenario( - scenarioName, - "Scenic", - xosc_paramdec, - entities=entities, - storyboard=xosc_sb, - roadnetwork=xosc_road, - catalog=xosc_catalog, - ) - - return xosc_scenario diff --git a/src/scenic/core/simulators.py b/src/scenic/core/simulators.py index c971e6dbe..0e42f9662 100644 --- a/src/scenic/core/simulators.py +++ b/src/scenic/core/simulators.py @@ -809,11 +809,11 @@ def currentState(self): The definition of 'state' is up to the simulator; the 'state' is simply saved at each time step to define the 'trajectory' of the simulation. Changing this method is however discouraged, unless one is adding additional attributes to - the returned `SimulationState` object. + the returned ``SimulationState`` object. - The default implementation returns a custom `SimulationState` object, which is a tuple + The default implementation returns a custom ``SimulationState`` object, which is a tuple of positions of all objects (for backwards compatibility) and also has two attributes: - `positions` and `orientations`, which are themselves tuples of the positions and + ``positions`` and ``orientations``, which are themselves tuples of the positions and orientations of all objects. """ diff --git a/src/scenic/domains/driving/openscenario.py b/src/scenic/domains/driving/openscenario.py new file mode 100644 index 000000000..6c317b365 --- /dev/null +++ b/src/scenic/domains/driving/openscenario.py @@ -0,0 +1,232 @@ +"""Functionality for interfacing the Scenic driving domain with OpenScenario.""" + +import math +import os +import warnings + +from scenic.core.vectors import Vector + + +def toOpenScenario( + simulation, + scenario, + mapPath=None, + scenarioName="ScenicScenario", +): + """Export a `Simulation` as a `scenariogeneration.xosc.scenario `_ object. + + Args: + simulation: The `Simulation` to be exported to XOSC + scenario: The scenario from which simulation was sampled. + mapPath: The path to the XODR map used to run the simulation. If + one is not provided the `map` param of the scenario is used. + scenarioName: The name of the scenario in the generated XOSC file. + """ + try: + import scenariogeneration + from scenariogeneration import ScenarioGenerator, xosc + except ModuleNotFoundError as e: + raise ModuleNotFoundError( + "The `scenariogeneration` package is required to use Scenic's XOSC export functionality." + ) from e + + if simulation.result is None: + raise RuntimeError("Cannot export incomplete scenario to OpenScenario XML") + + scene = simulation.scene + + if len(scene.objects) != len(simulation.trajectory[-1].positions): + raise RuntimeError("Cannot export scenario with dynamically created objects.") + + # Create catalog + xosc_catalog = xosc.Catalog() + + # Create parameters + xosc_paramdec = xosc.ParameterDeclarations() + + # Extract map + if mapPath is None: + if "map" not in scenario.params: + raise ValueError( + "No `mapPath` provided and scenario does not have a `map` parameter defined." + ) + mapPath = os.path.abspath(scenario.params["map"]) + xosc_road = xosc.RoadNetwork(roadfile=mapPath) + + # Create entitities + entities = xosc.Entities() + xosc_objects = {} + for obj_i, obj in enumerate(scene.objects): + if getattr(obj, "isVehicle", False): + obj_name = getattr(obj, "name", f"Vehicle{obj_i}") + veh_bb = xosc.BoundingBox( + obj.width, + obj.length, + obj.height, + 0, + 0, + 0, + ) + veh_fa = xosc.Axle( + obj.maxSteeringAngle, + obj.wheelDiameter, + obj.trackWidth, + obj.wheelbase, + obj.groundClearance, + ) + veh_ra = xosc.Axle( + obj.maxSteeringAngle, + obj.wheelDiameter, + obj.trackWidth, + 0, + obj.groundClearance, + ) + xosc_obj = xosc.Vehicle( + name=obj_name, + vehicle_type=xosc.VehicleCategory.car, + boundingbox=veh_bb, + frontaxle=veh_fa, + rearaxle=veh_ra, + max_speed=obj.maxSpeed, + max_acceleration=obj.maxAcceleration, + max_deceleration=obj.maxDeceleration, + mass=None, + model3d=None, + max_acceleration_rate=None, + max_deceleration_rate=None, + role=None, + ) + elif getattr(obj, "isPedestrian", False): + obj_name = getattr(obj, "name", f"Pedestrian{obj_i}") + ped_bb = xosc.BoundingBox( + obj.width, + obj.length, + obj.height, + 0, + 0, + 0, + ) + xosc_obj = xosc.Pedestrian( + name=obj_name, + mass=obj.mass, + boundingbox=ped_bb, + category=xosc.PedestrianCategory.pedestrian, + model=None, + role=None, + ) + else: + warnings.warn( + f"Object {obj} of unsupported type is being ignored during XOSC export." + ) + continue + + xosc_objects[obj] = xosc_obj + entities.add_scenario_object(obj_name, xosc_obj) + + # Helper function + def pos_to_WorldPosition(obj, pos, yaw): + # XOSC Reference point is back axle, so we must translate Scenic's + # convention to this. + state_position = ( + pos.offsetRotated(yaw, Vector(0, -0.5 * obj.wheelbase, 0)) + if obj.isVehicle + else pos + ) + state_orientation = yaw + math.radians(90) + return xosc.WorldPosition( + x=state_position.x, + y=state_position.y, + z=state_position.z, + h=state_orientation, + ) + + # Initial states + init = xosc.Init() + + for obj, xosc_obj in xosc_objects.items(): + obj_init_action = xosc.TeleportAction( + pos_to_WorldPosition(obj, obj.position, obj.heading) + ) + init.add_init_action(xosc_obj.name, obj_init_action) + + # Dynamics + xosc_act = xosc.Act( + "MainAct", + xosc.ValueTrigger( + "StartSimulation", + 0, + xosc.ConditionEdge.none, + xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), + ), + ) + + for obj_i, (obj, xosc_obj) in enumerate(xosc_objects.items()): + action_times = [] + action_positions = [] + for t, states in enumerate(simulation.trajectory): + action_positions.append( + pos_to_WorldPosition( + obj, states.positions[obj_i], states.orientations[obj_i].yaw + ) + ) + action_times.append(simulation.timestep * t) + + polyline = xosc.Polyline(time=action_times, positions=action_positions) + trajectory = xosc.Trajectory(name=f"Trajectory_{xosc_obj.name}", closed=False) + trajectory.add_shape(polyline) + + traj_action = xosc.FollowTrajectoryAction( + trajectory=trajectory, + following_mode=xosc.FollowingMode.position, + reference_domain=xosc.ReferenceContext.absolute, + scale=1, + offset=0, + ) + + event = xosc.Event(f"Event_{xosc_obj.name}", xosc.Priority.override) + event.add_trigger( + xosc.ValueTrigger( + f"TimeTrigger_{xosc_obj.name}", + 0, + xosc.ConditionEdge.none, + xosc.SimulationTimeCondition(0, xosc.Rule.greaterThan), + ) + ) + event.add_action(f"Action_{xosc_obj.name}", action=traj_action) + + maneuver = xosc.Maneuver("Maneuver_{xosc_obj.name}") + maneuver.add_event(event) + + manuever_group = xosc.ManeuverGroup(f"ManeuverGroup_{xosc_obj.name}") + manuever_group.add_maneuver(maneuver) + manuever_group.add_actor(xosc_obj.name) + + xosc_act.add_maneuver_group(manuever_group) + + # Create storyboard + xosc_sb = xosc.StoryBoard( + init, + xosc.ValueTrigger( + "StopSimulation", + 0, + xosc.ConditionEdge.rising, + xosc.SimulationTimeCondition( + simulation.currentRealTime, xosc.Rule.greaterThan + ), + "stop", + ), + ) + xosc_sb.add_act(xosc_act) + + # Create scenario + xosc_scenario = xosc.Scenario( + scenarioName, + "Scenic", + xosc_paramdec, + entities=entities, + storyboard=xosc_sb, + roadnetwork=xosc_road, + catalog=xosc_catalog, + ) + + return xosc_scenario diff --git a/src/scenic/syntax/translator.py b/src/scenic/syntax/translator.py index 58e856607..9a7e054c7 100644 --- a/src/scenic/syntax/translator.py +++ b/src/scenic/syntax/translator.py @@ -165,12 +165,15 @@ def _scenarioFromStream( oldModules = list(sys.modules.keys()) try: with topLevelNamespace(path) as namespace: + veneer.activate(compileOptions, namespace) compileStream(stream, namespace, compileOptions, filename) + + # Construct a Scenario from the resulting namespace + return constructScenarioFrom(namespace, scenario) finally: + veneer.deactivate() if not _cacheImports: purgeModulesUnsafeToCache(oldModules) - # Construct a Scenario from the resulting namespace - return constructScenarioFrom(namespace, scenario) @contextmanager @@ -274,53 +277,50 @@ def compileStream(stream, namespace, compileOptions, filename): if errors.verbosityLevel >= 2: veneer.verbosePrint(f" Compiling Scenic module from {filename}...") startTime = time.time() - veneer.activate(compileOptions, namespace) - try: - # Execute preamble - exec(compile(preamble, "", "exec"), namespace) - namespace[namespaceReference] = namespace - - # Parse the source - source = stream.read().decode("utf-8") - scenic_tree = parse_string(source, "exec", filename=filename) - - if dumpScenicAST: - print(f"### Begin Scenic AST of {filename}") - print(dump(scenic_tree, include_attributes=False, indent=4)) - print("### End Scenic AST") - - # Compile the Scenic AST into a Python AST - tree, requirements = compileScenicAST(scenic_tree, filename=filename) - astHasher = hashlib.blake2b(digest_size=4) - astHasher.update(ast.dump(tree).encode()) - - if dumpFinalAST: - print(f"### Begin final AST of {filename}") - print(dump(tree, include_attributes=True, indent=4)) - print("### End final AST") - - pythonSource = astToSource(tree) - if dumpASTPython: - if pythonSource is None: - raise RuntimeError( - "dumping the Python equivalent of the AST" - " requires the astor package" - ) - print(f"### Begin Python equivalent of final AST of {filename}") - print(pythonSource) - print("### End Python equivalent of final AST") - # Compile the Python AST tree - code = compileTranslatedTree(tree, filename) + # Execute preamble + exec(compile(preamble, "", "exec"), namespace) + namespace[namespaceReference] = namespace - # Execute it - executeCodeIn(code, namespace) + # Parse the source + source = stream.read().decode("utf-8") + scenic_tree = parse_string(source, "exec", filename=filename) + + if dumpScenicAST: + print(f"### Begin Scenic AST of {filename}") + print(dump(scenic_tree, include_attributes=False, indent=4)) + print("### End Scenic AST") + + # Compile the Scenic AST into a Python AST + tree, requirements = compileScenicAST(scenic_tree, filename=filename) + astHasher = hashlib.blake2b(digest_size=4) + astHasher.update(ast.dump(tree).encode()) + + if dumpFinalAST: + print(f"### Begin final AST of {filename}") + print(dump(tree, include_attributes=True, indent=4)) + print("### End final AST") + + pythonSource = astToSource(tree) + if dumpASTPython: + if pythonSource is None: + raise RuntimeError( + "dumping the Python equivalent of the AST" " requires the astor package" + ) + print(f"### Begin Python equivalent of final AST of {filename}") + print(pythonSource) + print("### End Python equivalent of final AST") + + # Compile the Python AST tree + code = compileTranslatedTree(tree, filename) + + # Execute it + executeCodeIn(code, namespace) + + # Extract scenario state from veneer and store it + astHash = astHasher.digest() + storeScenarioStateIn(namespace, requirements, astHash, compileOptions) - # Extract scenario state from veneer and store it - astHash = astHasher.digest() - storeScenarioStateIn(namespace, requirements, astHash, compileOptions) - finally: - veneer.deactivate() if errors.verbosityLevel >= 2: totalTime = time.time() - startTime veneer.verbosePrint(f" Compiled Scenic module in {totalTime:.4g} seconds.") diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index b79745030..65a1bbbb4 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -477,7 +477,7 @@ def instantiateSimulator(factory, params): def beginSimulation(sim): global currentSimulation, currentScenario, inInitialScenario, runningScenarios - global _globalParameters + global _globalParameters, mode2D if isActive(): raise RuntimeError("tried to start simulation during Scenic compilation!") assert currentSimulation is None @@ -489,6 +489,7 @@ def beginSimulation(sim): inInitialScenario = currentScenario._setup is None currentScenario._bindTo(sim.scene) _globalParameters = dict(sim.scene.params) + mode2D = currentSimulation.scene.compileOptions.mode2D # rebind globals that could be referenced by behaviors to their sampled values for modName, ( @@ -502,12 +503,13 @@ def beginSimulation(sim): def endSimulation(sim): global currentSimulation, currentScenario, currentBehavior, runningScenarios - global _globalParameters + global _globalParameters, mode2D currentSimulation = None currentScenario = None runningScenarios = [] currentBehavior = None _globalParameters = {} + mode2D = False for modName, ( namespace, diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index 1dcb531d4..31056daf3 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -7,20 +7,12 @@ import io import math import random -import subprocess -import sys import numpy import pytest -from scenic.core.serialization import ( - SerializationError, - Serializer, - deterministicHash, - toOpenScenario, -) +from scenic.core.serialization import SerializationError, Serializer, deterministicHash from scenic.core.simulators import DivergenceError, DummySimulator -from scenic.simulators.newtonian import NewtonianSimulator from tests.utils import ( areEquivalent, compileScenic, @@ -513,31 +505,3 @@ class Foo: digest2 = deterministicHash(mapping2) # Non-scalar values should hash in a stable way, independent of identity. assert digest1 == digest2 - - -def test_xosc_export(getAssetPath): - simulator = NewtonianSimulator("Town01") - code = f""" - param render = False - model scenic.simulators.newtonian.driving_model - - ego = new Car with behavior FollowLaneBehavior() - - immobileCar = new Car visible - - behavior Walk(): - take SetWalkingSpeedAction(1) - - new Pedestrian behind ego by 5, - with regionContainedIn None, - with behavior Walk() - - terminate after 5 seconds - """ - - scenario = compileScenic( - code, mode2D=True, params={"map": getAssetPath("maps/CARLA/Town01.xodr")} - ) - scene, _ = scenario.generate() - simulation = simulator.simulate(scene) - xosc_scenario = toOpenScenario(simulation, scenario, scene) diff --git a/tests/domains/driving/test_driving.py b/tests/domains/driving/test_driving.py index 50d09266d..22ba0e95e 100644 --- a/tests/domains/driving/test_driving.py +++ b/tests/domains/driving/test_driving.py @@ -7,7 +7,9 @@ from scenic.core.distributions import RejectionException from scenic.core.errors import InvalidScenarioError from scenic.core.geometry import TriangulationError +from scenic.domains.driving.openscenario import toOpenScenario from scenic.domains.driving.roads import Network +from scenic.simulators.newtonian import NewtonianSimulator from tests.utils import compileScenic, pickle_test, sampleEgo, sampleScene, tryPickling # Suppress all warnings from OpenDRIVE parser @@ -229,3 +231,69 @@ def test_invalid_road_scenario(cached_maps): param foo = ego.lane """, ) + + +def test_xosc_export(getAssetPath): + simulator = NewtonianSimulator("Town01") + code = f""" + param render = False + model scenic.simulators.newtonian.driving_model + + ego = new Car with behavior FollowLaneBehavior() + + immobileCar = new Car visible + + behavior Walk(): + take SetWalkingSpeedAction(1) + + new Pedestrian behind ego by 5, + with regionContainedIn None, + with behavior Walk() + + terminate after 5 seconds + """ + + scenario = compileScenic( + code, mode2D=True, params={"map": getAssetPath("maps/CARLA/Town01.xodr")} + ) + scene, _ = scenario.generate() + simulation = simulator.simulate(scene) + toOpenScenario(simulation, scenario, scene) + + +def test_xosc_export_dynamic_objects(getAssetPath): + simulator = NewtonianSimulator("Town01") + code = f""" + param render = False + model scenic.simulators.newtonian.driving_model + + scenario SpawnCar(): + setup: + new Car at 0@0 + + scenario Main(): + setup: + ego = new Car with behavior FollowLaneBehavior() + + immobileCar = new Car visible + + behavior Walk(): + take SetWalkingSpeedAction(1) + + new Pedestrian behind ego by 5, + with regionContainedIn None, + with behavior Walk() + + terminate after 5 seconds + + compose: + wait for 1 seconds + do SpawnCar() + """ + scenario = compileScenic( + code, mode2D=True, params={"map": getAssetPath("maps/CARLA/Town01.xodr")} + ) + scene, _ = scenario.generate() + simulation = simulator.simulate(scene) + with pytest.raises(RuntimeError, match="(.*)dynamically created objects(.*)"): + toOpenScenario(simulation, scenario, scene) From 1b8851f1c363f31abfb54fee0164c84e84b9d4ad Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 25 Jun 2026 13:07:29 -0700 Subject: [PATCH 19/21] Minor changes. --- src/scenic/syntax/translator.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/scenic/syntax/translator.py b/src/scenic/syntax/translator.py index 9a7e054c7..ab0ae9ea5 100644 --- a/src/scenic/syntax/translator.py +++ b/src/scenic/syntax/translator.py @@ -164,16 +164,17 @@ def _scenarioFromStream( # Compile the code as if it were a top-level module oldModules = list(sys.modules.keys()) try: - with topLevelNamespace(path) as namespace: - veneer.activate(compileOptions, namespace) - compileStream(stream, namespace, compileOptions, filename) - + try: + with topLevelNamespace(path) as namespace: + veneer.activate(compileOptions, namespace) + compileStream(stream, namespace, compileOptions, filename) + finally: + if not _cacheImports: + purgeModulesUnsafeToCache(oldModules) # Construct a Scenario from the resulting namespace return constructScenarioFrom(namespace, scenario) finally: veneer.deactivate() - if not _cacheImports: - purgeModulesUnsafeToCache(oldModules) @contextmanager From 05159d23a7cef767d6b7489a0fbdd3cce518d976 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 25 Jun 2026 13:35:18 -0700 Subject: [PATCH 20/21] Attempted moving cache purge. --- src/scenic/syntax/translator.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scenic/syntax/translator.py b/src/scenic/syntax/translator.py index ab0ae9ea5..066fdd2c2 100644 --- a/src/scenic/syntax/translator.py +++ b/src/scenic/syntax/translator.py @@ -164,17 +164,15 @@ def _scenarioFromStream( # Compile the code as if it were a top-level module oldModules = list(sys.modules.keys()) try: - try: - with topLevelNamespace(path) as namespace: - veneer.activate(compileOptions, namespace) - compileStream(stream, namespace, compileOptions, filename) - finally: - if not _cacheImports: - purgeModulesUnsafeToCache(oldModules) + with topLevelNamespace(path) as namespace: + veneer.activate(compileOptions, namespace) + compileStream(stream, namespace, compileOptions, filename) # Construct a Scenario from the resulting namespace return constructScenarioFrom(namespace, scenario) finally: veneer.deactivate() + if not _cacheImports: + purgeModulesUnsafeToCache(oldModules) @contextmanager From 752e1dd31809c5b824d6e13cdec29d87229c0f03 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 8 Jul 2026 16:57:37 -0700 Subject: [PATCH 21/21] Proper 2D setting/cleanup in simulations --- docs/simulators.rst | 2 +- src/scenic/syntax/veneer.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/simulators.rst b/docs/simulators.rst index 5a966660b..4abe649d7 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -15,7 +15,7 @@ See the individual entries for details on each interface's capabilities and how Be sure to check the documentation of each simulator to confirm which Python versions are supported. .. note:: - Scenic also supports outputing data in formats that may be imported into other simulators and tools (e.g. :ref:`xosc_export`). + Scenic also supports outputting data in formats that may be imported into other simulators and tools (e.g. :ref:`xosc_export`). For more details, see :ref:`serialization`. .. contents:: List of Simulators diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index 65a1bbbb4..0e8878e4b 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -477,7 +477,7 @@ def instantiateSimulator(factory, params): def beginSimulation(sim): global currentSimulation, currentScenario, inInitialScenario, runningScenarios - global _globalParameters, mode2D + global _globalParameters if isActive(): raise RuntimeError("tried to start simulation during Scenic compilation!") assert currentSimulation is None @@ -489,7 +489,18 @@ def beginSimulation(sim): inInitialScenario = currentScenario._setup is None currentScenario._bindTo(sim.scene) _globalParameters = dict(sim.scene.params) - mode2D = currentSimulation.scene.compileOptions.mode2D + + # If we are in 2D mode, set the global flag and replace all classes + # with their 2D compatibility counterparts. + if currentSimulation.scene.compileOptions.mode2D: + global mode2D, Point, OrientedPoint, Object + mode2D = True + Point = Point2D + OrientedPoint = OrientedPoint2D + Object = Object2D + scenic.core.object_types.Point = Point + scenic.core.object_types.OrientedPoint = OrientedPoint + scenic.core.object_types.Object = Object # rebind globals that could be referenced by behaviors to their sampled values for modName, ( @@ -509,7 +520,14 @@ def endSimulation(sim): runningScenarios = [] currentBehavior = None _globalParameters = {} - mode2D = False + + if mode2D: + global Point, OrientedPoint, Object + mode2D = False + Point, OrientedPoint, Object = _originalConstructibles + scenic.core.object_types.Point = Point + scenic.core.object_types.OrientedPoint = OrientedPoint + scenic.core.object_types.Object = Object for modName, ( namespace,