From 6e3ebc204f59c81db5a0e2ef88718ba0d7c2f176 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Wed, 15 May 2024 15:05:36 -0700 Subject: [PATCH 01/28] Driving Domain 3D Compatibility (#254) * Updated driving domain for optional 3D mode and added tests. * Updated docs. * Remove old util function. * Reverted unintended example change. * Another example revert. * Adjust comment. * Updated error message. * Another error typo fix. * Test fixes/cleanup. * Fixed bad parameter. * fix pickling of some scenarios in 2D mode * Pr fixes. * Apply suggestions from code review Co-authored-by: Daniel Fremont * Fixed broken test. --------- Co-authored-by: Daniel Fremont --- src/scenic/core/object_types.py | 7 ++-- src/scenic/core/scenarios.py | 4 +++ src/scenic/core/serialization.py | 25 +++++++++++++ src/scenic/domains/driving/model.scenic | 38 +++++++++++++++----- src/scenic/simulators/carla/model.scenic | 10 ++++-- src/scenic/simulators/carla/simulator.py | 5 ++- src/scenic/simulators/carla/utils/utils.py | 17 +++------ tests/core/test_pickle.py | 19 ++++++++++ tests/domains/driving/test_driving.py | 23 ++++++++++-- tests/simulators/newtonian/test_newtonian.py | 2 +- tests/syntax/test_basic.py | 36 ++++++++++++++++++- tests/syntax/test_specifiers.py | 5 +++ tests/utils.py | 4 +-- 13 files changed, 161 insertions(+), 34 deletions(-) diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index 1604c36ca..5f46f5aec 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -1770,11 +1770,10 @@ def __init_subclass__(cls): cls._props_transformed = str(cls) props = cls._scenic_properties - # Raise error if parentOrientation already defined - if "parentOrientation" in props: + # Raise error if parentOrientation and heading already defined + if "parentOrientation" in props and "heading" in props: raise RuntimeError( - "this scenario cannot be run with the --2d flag (the " - f'{cls.__name__} class defines "parentOrientation")' + f'{cls.__name__} defines both "parentOrientation" and "heading"' ) # Map certain properties to their 3D analog diff --git a/src/scenic/core/scenarios.py b/src/scenic/core/scenarios.py index 6e339b6ec..133911013 100644 --- a/src/scenic/core/scenarios.py +++ b/src/scenic/core/scenarios.py @@ -533,6 +533,10 @@ def generateDefaultRequirements(self): for obj in filter( lambda x: x.requireVisible and x is not self.egoObject, self.objects ): + if not self.egoObject: + raise InvalidScenarioError( + "requireVisible set to true but no ego is defined" + ) requirements.append(VisibilityRequirement(self.egoObject, obj, self.objects)) return tuple(requirements) diff --git a/src/scenic/core/serialization.py b/src/scenic/core/serialization.py index e66439125..a7c52367a 100644 --- a/src/scenic/core/serialization.py +++ b/src/scenic/core/serialization.py @@ -9,6 +9,7 @@ import math import pickle import struct +import types from scenic.core.distributions import Samplable, needsSampling from scenic.core.utils import DefaultIdentityDict @@ -41,6 +42,30 @@ def dumpAsScenicCode(value, stream): stream.write(repr(value)) +## Pickles + +# If dill is installed, register some custom handlers to improve the pickling +# of Scene and Scenario objects. + +try: + import dill +except Exception: + pass +else: + _orig_save_module = dill.Pickler.dispatch[types.ModuleType] + + @dill.register(types.ModuleType) + def patched_save_module(pickler, obj): + # Save Scenic's internal modules by reference to avoid inconsistent versions + # as well as some unpicklable objects (and shrink the size of pickles while + # we're at it). + name = obj.__name__ + if name == "scenic" or name.startswith("scenic."): + pickler.save_reduce(dill._dill._import_module, (name,), obj=obj) + return + _orig_save_module(pickler, obj) + + ## Binary serialization format diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index 0dea752f2..b6732d2fb 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -17,6 +17,14 @@ If you are writing a generic scenario that supports multiple maps, you may leave ``map`` parameter undefined; then running the scenario will produce an error unless the user uses the :option:`--param` command-line option to specify the map. +The ``use2DMap`` global parameter determines whether or not maps are generated in 2D. Currently +3D maps are not supported, but are under development. By default, this parameter is `False` +(so that future versions of Scenic will automatically use 3D maps), unless +:ref:`2D compatibility mode` is enabled, in which case the default is `True`. The parameter +can be manually set to `True` to ensure 2D maps are used even if the scenario is not compiled +in 2D compatibility mode. + + .. note:: If you are using a simulator, you may have to also define simulator-specific global @@ -38,6 +46,22 @@ from scenic.domains.driving.behaviors import * from scenic.core.distributions import RejectionException from scenic.simulators.utils.colors import Color +## 2D mode flag & checks + +def is2DMode(): + from scenic.syntax.veneer import mode2D + return mode2D + +param use2DMap = True if is2DMode() else False + +if is2DMode() and not globalParameters.use2DMap: + raise RuntimeError('in 2D mode, global parameter "use2DMap" must be True') + +# Note: The following should be removed when 3D maps are supported +if not globalParameters.use2DMap: + raise RuntimeError('3D maps not supported at this time.' + '(to use 2D maps set global parameter "use2DMap" to True)') + ## Load map and set up workspace if 'map' not in globalParameters: @@ -80,10 +104,6 @@ roadDirection : VectorField = network.roadDirection ## Standard object types -def is2DMode(): - from scenic.syntax.veneer import mode2D - return mode2D - class DrivingObject: """Abstract class for objects in a road network. @@ -250,10 +270,10 @@ class Vehicle(DrivingObject): Properties: position: The default position is uniformly random over the `road`. - heading: The default heading is aligned with `roadDirection`, plus an offset + parentOrientation: The default parentOrientation is aligned with `roadDirection`, plus an offset given by **roadDeviation**. roadDeviation (float): Relative heading with respect to the road direction at - the `Vehicle`'s position. Used by the default value for **heading**. + the `Vehicle`'s position. Used by the default value for **parentOrientation**. regionContainedIn: The default container is :obj:`roadOrShoulder`. viewAngle: The default view angle is 90 degrees. width: The default width is 2 meters. @@ -264,7 +284,7 @@ class Vehicle(DrivingObject): """ regionContainedIn: roadOrShoulder position: new Point on road - heading: (roadDirection at self.position) + self.roadDeviation + parentOrientation: (roadDirection at self.position) + self.roadDeviation roadDeviation: 0 viewAngle: 90 deg width: 2 @@ -290,7 +310,7 @@ class Pedestrian(DrivingObject): Properties: position: The default position is uniformly random over sidewalks and crosswalks. - heading: The default heading is uniformly random. + parentOrientation: The default parentOrientation has uniformly random yaw. viewAngle: The default view angle is 90 degrees. width: The default width is 0.75 m. length: The default length is 0.75 m. @@ -299,7 +319,7 @@ class Pedestrian(DrivingObject): """ regionContainedIn: network.walkableRegion position: new Point on network.walkableRegion - heading: Range(0, 360) deg + parentOrientation: Range(0, 360) deg viewAngle: 90 deg width: 0.75 length: 0.75 diff --git a/src/scenic/simulators/carla/model.scenic b/src/scenic/simulators/carla/model.scenic index 030e31bca..c9cac9dfb 100644 --- a/src/scenic/simulators/carla/model.scenic +++ b/src/scenic/simulators/carla/model.scenic @@ -18,6 +18,8 @@ Global Parameters: timestep (float): Timestep to use for simulations (i.e., how frequently Scenic interrupts CARLA to run behaviors, check requirements, etc.), in seconds. Default is 0.1 seconds. + snapToGroundDefault (bool): Default value for :prop:`snapToGround` on `CarlaActor` objects. + Default is True if :ref:`2D compatibility mode` is enabled and False otherwise. weather (str or dict): Weather to use for the simulation. Can be either a string identifying one of the CARLA weather presets (e.g. 'ClearSunset') or a @@ -96,6 +98,7 @@ param weather = Uniform( 'MidRainSunset', 'HardRainSunset' ) +param snapToGroundDefault = is2DMode() simulator CarlaSimulator( carla_map=globalParameters.carla_map, @@ -118,12 +121,15 @@ class CarlaActor(DrivingObject): rolename (str): Can be used to differentiate specific actors during runtime. Default value ``None``. physics (bool): Whether physics is enabled for this object in CARLA. Default true. + snapToGround (bool): Whether or not to snap this object to the ground when placed in CARLA. + The default is set by the ``snapToGroundDefault`` global parameter above. """ carlaActor: None blueprint: None rolename: None color: None physics: True + snapToGround: globalParameters.snapToGroundDefault def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -222,12 +228,12 @@ class Prop(CarlaActor): """Abstract class for props, i.e. non-moving objects. Properties: - heading (float): Default value overridden to be uniformly random. + parentOrientation (Orientation): Default value overridden to have uniformly random yaw. physics (bool): Default value overridden to be false. """ regionContainedIn: road position: new Point on road - heading: Range(0, 360) deg + parentOrientation: Range(0, 360) deg width: 0.5 length: 0.5 physics: False diff --git a/src/scenic/simulators/carla/simulator.py b/src/scenic/simulators/carla/simulator.py index 6e7022bae..6469281d7 100644 --- a/src/scenic/simulators/carla/simulator.py +++ b/src/scenic/simulators/carla/simulator.py @@ -205,7 +205,10 @@ def createObjectInSimulator(self, obj): # Set up transform loc = utils.scenicToCarlaLocation( - obj.position, world=self.world, blueprint=obj.blueprint + obj.position, + world=self.world, + blueprint=obj.blueprint, + snapToGround=obj.snapToGround, ) rot = utils.scenicToCarlaRotation(obj.orientation) transform = carla.Transform(loc, rot) diff --git a/src/scenic/simulators/carla/utils/utils.py b/src/scenic/simulators/carla/utils/utils.py index 72047e73b..638161163 100644 --- a/src/scenic/simulators/carla/utils/utils.py +++ b/src/scenic/simulators/carla/utils/utils.py @@ -7,7 +7,7 @@ from scenic.core.vectors import Orientation, Vector -def snapToGround(world, location, blueprint): +def _snapToGround(world, location, blueprint): """Mutates @location to have the same z-coordinate as the nearest waypoint in @world.""" waypoint = world.get_map().get_waypoint(location) # patch to avoid the spawn error issue with vehicles and walkers. @@ -25,11 +25,11 @@ def scenicToCarlaVector3D(x, y, z=0.0): return carla.Vector3D(x, -y, z) -def scenicToCarlaLocation(pos, z=None, world=None, blueprint=None): - if z is None: +def scenicToCarlaLocation(pos, world=None, blueprint=None, snapToGround=False): + if snapToGround: assert world is not None - return snapToGround(world, carla.Location(pos.x, -pos.y, 0.0), blueprint) - return carla.Location(pos.x, -pos.y, z) + return _snapToGround(world, carla.Location(pos.x, -pos.y, 0.0), blueprint) + return carla.Location(pos.x, -pos.y, pos.z) def scenicToCarlaRotation(orientation): @@ -40,13 +40,6 @@ def scenicToCarlaRotation(orientation): return carla.Rotation(pitch=pitch, yaw=yaw, roll=roll) -def scenicSpeedToCarlaVelocity(speed, heading): - currYaw = scenicToCarlaRotation(heading).yaw - xVel = speed * math.cos(currYaw) - yVel = speed * math.sin(currYaw) - return scenicToCarlaVector3D(xVel, yVel) - - def carlaToScenicPosition(loc): return Vector(loc.x, -loc.y, loc.z) diff --git a/tests/core/test_pickle.py b/tests/core/test_pickle.py index 17955be7b..71ffe82d8 100644 --- a/tests/core/test_pickle.py +++ b/tests/core/test_pickle.py @@ -1,5 +1,8 @@ +import sys + import pytest +import scenic from scenic.core.distributions import ( Normal, Options, @@ -95,6 +98,22 @@ def test_pickle_scene(): tryPickling(scene) +def test_pickle_scenario_2D_module(): + """Tests a nasty bug involving pickling the scenic module in 2D mode.""" + scenario = compileScenic( + """ + import scenic + ego = new Object with favoriteModule scenic + """, + mode2D=True, + ) + sc = tryPickling(scenario) + assert sys.modules["scenic.core.object_types"].Object is Object + ego = sampleEgo(sc) + assert isinstance(ego, Object) + assert ego.favoriteModule is scenic + + def test_pickle_scenario_dynamic(): scenario = compileScenic( """ diff --git a/tests/domains/driving/test_driving.py b/tests/domains/driving/test_driving.py index f42c330ee..4ea809437 100644 --- a/tests/domains/driving/test_driving.py +++ b/tests/domains/driving/test_driving.py @@ -33,13 +33,32 @@ from tests.domains.driving.conftest import map_params, mapFolder -def compileDrivingScenario(cached_maps, code="", useCache=True, path=None): +def compileDrivingScenario( + cached_maps, code="", useCache=True, path=None, mode2D=True, params={} +): if not path: path = mapFolder / "CARLA" / "Town01.xodr" path = cached_maps[str(path)] preamble = template.format(map=path, cache=useCache) whole = preamble + "\n" + inspect.cleandoc(code) - return compileScenic(whole, mode2D=True) + return compileScenic(whole, mode2D=mode2D, params=params) + + +def test_driving_2D_map(cached_maps): + compileDrivingScenario( + cached_maps, + code=basicScenario, + useCache=False, + mode2D=False, + params={"use2DMap": True}, + ) + + +def test_driving_3D(cached_maps): + with pytest.raises(RuntimeError): + compileDrivingScenario( + cached_maps, code=basicScenario, useCache=False, mode2D=False + ) @pytest.mark.slow diff --git a/tests/simulators/newtonian/test_newtonian.py b/tests/simulators/newtonian/test_newtonian.py index dae7d82f1..c8dafae1d 100644 --- a/tests/simulators/newtonian/test_newtonian.py +++ b/tests/simulators/newtonian/test_newtonian.py @@ -21,7 +21,7 @@ def test_render(loadLocalScenario): simulator.simulate(scene, maxSteps=3) -def test_driving(loadLocalScenario): +def test_driving_2D(loadLocalScenario): def check(): scenario = loadLocalScenario("driving.scenic", mode2D=True) scene, _ = scenario.generate(maxIterations=1000) diff --git a/tests/syntax/test_basic.py b/tests/syntax/test_basic.py index 0a2a75cdc..dd022b208 100644 --- a/tests/syntax/test_basic.py +++ b/tests/syntax/test_basic.py @@ -13,7 +13,13 @@ setDebuggingOptions, ) from scenic.core.object_types import Object -from tests.utils import compileScenic, sampleEgo, sampleParamPFrom, sampleScene +from tests.utils import ( + compileScenic, + sampleEgo, + sampleEgoFrom, + sampleParamPFrom, + sampleScene, +) def test_minimal(): @@ -296,3 +302,31 @@ def test_mode2D_interference(): scene, _ = scenario.generate() assert any(obj.position[2] != 0 for obj in scene.objects) + + +def test_mode2D_heading_parentOrientation(): + program = """ + class Foo: + heading: 0.56 + + class Bar(Foo): + parentOrientation: 1.2 + + ego = new Bar + """ + + obj = sampleEgoFrom(program, mode2D=True) + assert obj.heading == obj.parentOrientation.yaw == 1.2 + + program = """ + class Bar: + parentOrientation: 1.2 + + class Foo(Bar): + heading: 0.56 + + ego = new Foo + """ + + obj = sampleEgoFrom(program, mode2D=True) + assert obj.heading == obj.parentOrientation.yaw == 0.56 diff --git a/tests/syntax/test_specifiers.py b/tests/syntax/test_specifiers.py index 07b86fee1..fe5e677f3 100644 --- a/tests/syntax/test_specifiers.py +++ b/tests/syntax/test_specifiers.py @@ -555,6 +555,11 @@ def test_visible_no_ego(): compileScenic("ego = new Object visible") +def test_visible_no_ego_2(): + with pytest.raises(InvalidScenarioError): + compileScenic("new Object visible") + + def test_visible_from_point(): scenario = compileScenic( "x = new Point at 300@200, with visibleDistance 2\n" diff --git a/tests/utils.py b/tests/utils.py index 4d2f6d801..7dd5dcaee 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -25,12 +25,12 @@ # Compilation -def compileScenic(code, removeIndentation=True, scenario=None, mode2D=False): +def compileScenic(code, removeIndentation=True, scenario=None, mode2D=False, params={}): if removeIndentation: # to allow indenting code to line up with test function code = inspect.cleandoc(code) checkVeneerIsInactive() - scenario = scenarioFromString(code, scenario=scenario, mode2D=mode2D) + scenario = scenarioFromString(code, scenario=scenario, mode2D=mode2D, params=params) checkVeneerIsInactive() return scenario From 6f37d6cf7379bb95191d8b2a0b0bf46a66ae9e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Thu, 16 May 2024 11:54:35 -0700 Subject: [PATCH 02/28] fix: CodeCov `.yml` enhancements (#264) * fix: Adding modifications to codecov.yml * fix: Adding modifications to codecov.yml * fix: adding patch coverage back * Removing some file types from code coverage updates * fix: ignore any non-python file * Match patch coverage target to project --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Daniel Fremont --- codecov.yml | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/codecov.yml b/codecov.yml index fd88927cf..93db0f3e9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,25 +1,33 @@ -codecov: - coverage: - precision: 2 - round: down - range: "70...100" - status: - project: - default: - target: 80% - threshold: 1% - changes: - target: 80% - threshold: 1% - comment: - layout: "reach, diff, flags, files" - behavior: default +codecov: require_ci_to_pass: true - ignore: - - "src/scenic/simulators/**" - - "!src/scenic/simulators/newtonian/**" - - "!src/scenic/simulators/utils/**" + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + target: 80% + threshold: 5% + patch: + default: + target: 80% + threshold: 5% +ignore: + - "tests/" + - "docs/" + - "src/scenic/simulators/airsim/" + - "src/scenic/simulators/carla/" + - "src/scenic/simulators/gta/" + - "src/scenic/simulators/lgsvl/" + - "src/scenic/simulators/webots/" + - "src/scenic/simulators/xplane/" + - "!**/*.py" +comment: + layout: "reach, diff, flags, files" + behavior: default cli: plugins: pycoverage: - report_type: "json" \ No newline at end of file + report_type: "json" From e6832d1c71dabf11c4c776c1e6a9f2b97eb70bc3 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Fri, 17 May 2024 14:38:05 -0700 Subject: [PATCH 03/28] Behavior Override Fix (#252) * Added needed tests. * Behavior override and lastAction fixes. * Added test for property override leakage. * Fix formatting. * PR requested fixes. * Tweaked agent list defs. * Updated utility functions. * Update src/scenic/core/simulators.py Co-authored-by: Daniel Fremont * Apply suggestions from code review Co-authored-by: Daniel Fremont --------- Co-authored-by: Daniel Fremont --- src/scenic/core/object_types.py | 4 +- src/scenic/core/simulators.py | 38 ++++++++--- tests/core/test_simulators.py | 30 +++++++++ tests/syntax/test_dynamics.py | 30 +++++++++ tests/syntax/test_modular.py | 109 ++++++++++++++++++++++++++++++++ tests/utils.py | 10 ++- 6 files changed, 208 insertions(+), 13 deletions(-) diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index 5f46f5aec..8ecdff064 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -1017,7 +1017,7 @@ class Object(OrientedPoint): behavior: Behavior for dynamic agents, if any (see :ref:`dynamics`). Default value ``None``. lastActions: Tuple of :term:`actions` taken by this agent in the last time step - (or `None` if the object is not an agent or this is the first time step). + (an empty tuple if the object is not an agent or this is the first time step). """ _scenic_properties = { @@ -1042,7 +1042,7 @@ class Object(OrientedPoint): "angularVelocity": PropertyDefault((), {"dynamic"}, lambda self: Vector(0, 0, 0)), "angularSpeed": PropertyDefault((), {"dynamic"}, lambda self: 0), "behavior": None, - "lastActions": None, + "lastActions": tuple(), # weakref to scenario which created this object, for internal use "_parentScenario": None, } diff --git a/src/scenic/core/simulators.py b/src/scenic/core/simulators.py index 539c8fdab..832b03632 100644 --- a/src/scenic/core/simulators.py +++ b/src/scenic/core/simulators.py @@ -11,7 +11,7 @@ """ import abc -from collections import OrderedDict, defaultdict +from collections import defaultdict import enum import math import numbers @@ -294,7 +294,10 @@ class Simulation(abc.ABC): timestep (float): Length of each time step in seconds. objects: List of Scenic objects (instances of `Object`) existing in the simulation. This list will change if objects are created dynamically. - agents: List of :term:`agents` in the simulation. + agents: List of :term:`agents` in the simulation. An agent is any object that has + or had a behavior at any point in the simulation. The agents list may have objects + appended to the end as the simulation progresses (if a non-agent object has its + behavior overridden), but once an object is in the agents list its position is fixed. result (`SimulationResult`): Result of the simulation, or `None` if it has not yet completed. This is the primary object which should be inspected to get data out of the simulation: the other undocumented attributes of this class @@ -331,7 +334,6 @@ def __init__( self.result = None self.scene = scene self.objects = [] - self.agents = [] self.trajectory = [] self.records = defaultdict(list) self.currentTime = 0 @@ -398,7 +400,7 @@ def __init__( for obj in self.objects: disableDynamicProxyFor(obj) for agent in self.agents: - if agent.behavior._isRunning: + if agent.behavior and agent.behavior._isRunning: agent.behavior._stop() # If the simulation was terminated by an exception (including rejections), # some scenarios may still be running; we need to clean them up without @@ -441,10 +443,25 @@ def _run(self, dynamicScenario, maxSteps): if maxSteps and self.currentTime >= maxSteps: return TerminationType.timeLimit, f"reached time limit ({maxSteps} steps)" + # Clear lastActions for all objects + for obj in self.objects: + obj.lastActions = tuple() + + # Update agents with any objects that now have behaviors (and are not already agents) + self.agents += [ + obj for obj in self.objects if obj.behavior and obj not in self.agents + ] + # Compute the actions of the agents in this time step - allActions = OrderedDict() + allActions = defaultdict(tuple) schedule = self.scheduleForAgents() + if not set(self.agents) == set(schedule): + raise RuntimeError("Simulator schedule does not contain all agents") for agent in schedule: + # If agent doesn't have a behavior right now, continue + if not agent.behavior: + continue + # Run the agent's behavior to get its actions actions = agent.behavior._step() @@ -472,11 +489,13 @@ def _run(self, dynamicScenario, maxSteps): # Save actions for execution below allActions[agent] = actions + # Log lastActions + agent.lastActions = actions + # Execute the actions if self.verbosity >= 3: for agent, actions in allActions.items(): print(f" Agent {agent} takes action(s) {actions}") - agent.lastActions = actions self.actionSequence.append(allActions) self.executeActions(allActions) @@ -492,6 +511,7 @@ def setup(self): but should call the parent implementation to create the objects in the initial scene (through `createObjectInSimulator`). """ + self.agents = [] for obj in self.scene.objects: self._createObject(obj) @@ -624,9 +644,9 @@ def executeActions(self, allActions): functionality. Args: - allActions: an :obj:`~collections.OrderedDict` mapping each agent to a tuple - of actions. The order of agents in the dict should be respected in case - the order of actions matters. + allActions: a :obj:`~collections.defaultdict` mapping each agent to a tuple + of actions, with the default value being an empty tuple. The order of + agents in the dict should be respected in case the order of actions matters. """ for agent, actions in allActions.items(): for action in actions: diff --git a/tests/core/test_simulators.py b/tests/core/test_simulators.py index 70851495e..149c1cad1 100644 --- a/tests/core/test_simulators.py +++ b/tests/core/test_simulators.py @@ -64,3 +64,33 @@ class TestObj: assert result is not None assert result.records["test_val_1"] == [(0, "bar"), (1, "bar"), (2, "bar")] assert result.records["test_val_2"] == result.records["test_val_3"] == "bar" + + +def test_simulator_bad_scheduler(): + class TestSimulation(DummySimulation): + def scheduleForAgents(self): + # Don't include the last agent + return self.agents[:-1] + + class TestSimulator(DummySimulator): + def createSimulation(self, scene, **kwargs): + return TestSimulation(scene, **kwargs) + + scenario = compileScenic( + """ + behavior Foo(): + take 1 + + class TestObj: + allowCollisions: True + behavior: Foo + + for _ in range(5): + new TestObj + """ + ) + + scene, _ = scenario.generate(maxIterations=1) + simulator = TestSimulator() + with pytest.raises(RuntimeError): + result = simulator.simulate(scene, maxSteps=2) diff --git a/tests/syntax/test_dynamics.py b/tests/syntax/test_dynamics.py index 513796cd1..0bd724b03 100644 --- a/tests/syntax/test_dynamics.py +++ b/tests/syntax/test_dynamics.py @@ -2085,3 +2085,33 @@ def test_record(): (2, (4, 0, 0)), (3, (6, 0, 0)), ) + + +## lastActions Property +def test_lastActions(): + scenario = compileScenic( + """ + behavior Foo(): + for i in range(4): + take i + ego = new Object with behavior Foo, with allowCollisions True + other = new Object with allowCollisions True + record ego.lastActions as ego_lastActions + record other.lastActions as other_lastActions + """ + ) + result = sampleResult(scenario, maxSteps=4) + assert tuple(result.records["ego_lastActions"]) == ( + (0, tuple()), + (1, (0,)), + (2, (1,)), + (3, (2,)), + (4, (3,)), + ) + assert tuple(result.records["other_lastActions"]) == ( + (0, tuple()), + (1, tuple()), + (2, tuple()), + (3, tuple()), + (4, tuple()), + ) diff --git a/tests/syntax/test_modular.py b/tests/syntax/test_modular.py index 2a967831a..a7a89903e 100644 --- a/tests/syntax/test_modular.py +++ b/tests/syntax/test_modular.py @@ -9,6 +9,7 @@ from scenic.core.simulators import DummySimulator, TerminationType from tests.utils import ( compileScenic, + sampleActionsFromScene, sampleEgo, sampleEgoActions, sampleEgoFrom, @@ -809,6 +810,75 @@ def test_override_behavior(): assert tuple(actions) == (1, -1, -2, 2) +def test_override_none_behavior(): + scenario = compileScenic( + """ + scenario Main(): + setup: + ego = new Object + compose: + wait + do Sub() for 2 steps + wait + scenario Sub(): + setup: + override ego with behavior Bar + behavior Bar(): + x = -1 + while True: + take x + x -= 1 + """, + scenario="Main", + ) + actions = sampleEgoActions(scenario, maxSteps=4) + assert tuple(actions) == (None, -1, -2, None) + + +def test_override_leakage(): + scenario = compileScenic( + """ + scenario Main(): + setup: + ego = new Object with prop 1 + compose: + do Sub1() + scenario Sub1(): + setup: + override ego with prop 2, with behavior Bar + behavior Bar(): + terminate + """, + scenario="Main", + ) + scene = sampleScene(scenario) + assert scene.objects[0].prop == 1 + sampleActionsFromScene(scene) + assert scene.objects[0].prop == 1 + + scenario = compileScenic( + """ + scenario Main(): + setup: + ego = new Object with prop 1 + compose: + do Sub1() + scenario Sub1(): + setup: + override ego with prop 2, with behavior Bar + behavior Bar(): + raise NotImplementedError() + wait + """, + scenario="Main", + ) + scene = sampleScene(scenario) + assert scene.objects[0].prop == 1 + with pytest.raises(NotImplementedError): + sampleActionsFromScene(scene) + assert scene.objects[0].prop == 1 + + def test_override_dynamic(): with pytest.raises(SpecifierError): compileScenic( @@ -1048,3 +1118,42 @@ def test_scenario_signature(body): assert name4 == "qux" assert p4.default is inspect.Parameter.empty assert p4.kind is inspect.Parameter.VAR_KEYWORD + + +# lastActions Property +def test_lastActions_modular(): + scenario = compileScenic( + """ + scenario Main(): + setup: + ego = new Object + record ego.lastActions as lastActions + compose: + do Sub1() for 2 steps + do Sub2() for 2 steps + do Sub1() for 2 steps + wait + scenario Sub1(): + setup: + override ego with behavior Bar + scenario Sub2(): + setup: + override ego with behavior None + behavior Bar(): + x = -1 + while True: + take x + x -= 1 + """, + scenario="Main", + ) + result = sampleResult(scenario, maxSteps=6) + assert tuple(result.records["lastActions"]) == ( + (0, tuple()), + (1, (-1,)), + (2, (-2,)), + (3, tuple()), + (4, tuple()), + (5, (-1,)), + (6, (-2,)), + ) diff --git a/tests/utils.py b/tests/utils.py index 7dd5dcaee..9f4816ad2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -92,7 +92,10 @@ def sampleEgoActions( asMapping=False, timestep=timestep, ) - return [actions[0] for actions in allActions] + return [ + actions[0] if actions else (None if singleAction else tuple()) + for actions in allActions + ] def sampleEgoActionsFromScene( @@ -108,7 +111,10 @@ def sampleEgoActionsFromScene( ) if allActions is None: return None - return [actions[0] for actions in allActions] + return [ + actions[0] if actions else (None if singleAction else tuple()) + for actions in allActions + ] def sampleActions( From abde0f923f77de41aed34c9b50d042f2e7055675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Wed, 22 May 2024 10:09:54 -0700 Subject: [PATCH 04/28] fix: updating pillow version for MacOS Apple Silicon installation (#270) * fix: Adding modifications to codecov.yml * fix: updating pillow contraint --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- codecov.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 93db0f3e9..9b0516a8b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -30,4 +30,4 @@ comment: cli: plugins: pycoverage: - report_type: "json" + report_type: "json" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e69b47d05..2978792fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "numpy ~= 1.24", "opencv-python ~= 4.5", "pegen >= 0.3.0", - "pillow ~= 9.1", + "pillow >= 9.1", 'pygame >= 2.1.3.dev8, <3; python_version >= "3.11"', 'pygame ~= 2.0; python_version < "3.11"', "pyglet ~= 1.5", From 836b0f8dedcf1f7111b11103c962b51caa49cbf6 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Wed, 22 May 2024 10:32:13 -0700 Subject: [PATCH 05/28] Prepare 3.0.0rc1 (#271) * update version number * add Armando to credits --- docs/credits.rst | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/credits.rst b/docs/credits.rst index e73a49ffd..b93567717 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -18,6 +18,7 @@ Shun Kashiwa developed the auto-generated parser for Scenic 3.0 and its support The Scenic tool and example scenarios have benefitted from additional code contributions from: + * Armando BaƱuelos * Johnathan Chiu * Greg Crow * Francis Indaheng diff --git a/pyproject.toml b/pyproject.toml index 2978792fe..7b47374f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scenic" -version = "3.0.0b2" +version = "3.0.0rc1" description = "The Scenic scenario description language." authors = [ { name = "Daniel Fremont" }, From 63725a3e95bb1c1a657474e9ee6a2d9e1cd0a9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:49:00 -0500 Subject: [PATCH 06/28] feat: adding output gif for newtonian simulator (#273) * fix: Adding modifications to codecov.yml * feat: adding produced gif * Adding colab * fix: readding commented line * feat: making gif export optional * test: adding test case to test gif creation * fix: adding graphical label * fix: adding extra_gif options * fix: reformatting files --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- src/scenic/simulators/newtonian/simulator.py | 27 +++++++++++++++++--- tests/simulators/newtonian/driving.scenic | 2 ++ tests/simulators/newtonian/test_newtonian.py | 17 ++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/newtonian/simulator.py b/src/scenic/simulators/newtonian/simulator.py index da453b0ab..fd38aa427 100644 --- a/src/scenic/simulators/newtonian/simulator.py +++ b/src/scenic/simulators/newtonian/simulator.py @@ -7,6 +7,9 @@ import pathlib import time +from PIL import Image +import numpy as np + import scenic.core.errors as errors # isort: skip if errors.verbosityLevel == 0: # suppress pygame advertisement at zero verbosity @@ -55,21 +58,29 @@ class NewtonianSimulator(DrivingSimulator): when not otherwise specified is still 0.1 seconds. """ - def __init__(self, network=None, render=False): + def __init__(self, network=None, render=False, export_gif=False): super().__init__() + self.export_gif = export_gif self.render = render self.network = network def createSimulation(self, scene, **kwargs): - return NewtonianSimulation(scene, self.network, self.render, **kwargs) + simulation = NewtonianSimulation( + scene, self.network, self.render, self.export_gif, **kwargs + ) + if self.export_gif and self.render: + simulation.generate_gif("simulation.gif") + return simulation class NewtonianSimulation(DrivingSimulation): """Implementation of `Simulation` for the Newtonian simulator.""" - def __init__(self, scene, network, render, timestep, **kwargs): + def __init__(self, scene, network, render, export_gif, timestep, **kwargs): + self.export_gif = export_gif self.render = render self.network = network + self.frames = [] if timestep is None: timestep = 0.1 @@ -213,8 +224,18 @@ def draw_objects(self): pygame.draw.polygon(self.screen, color, corners) pygame.display.update() + + if self.export_gif: + frame = pygame.surfarray.array3d(self.screen) + frame = np.transpose(frame, (1, 0, 2)) + self.frames.append(frame) + time.sleep(self.timestep) + def generate_gif(self, filename="simulation.gif"): + imgs = [Image.fromarray(frame) for frame in self.frames] + imgs[0].save(filename, save_all=True, append_images=imgs[1:], duration=50, loop=0) + def getProperties(self, obj, properties): yaw, _, _ = obj.parentOrientation.globalToLocalAngles(obj.heading, 0, 0) diff --git a/tests/simulators/newtonian/driving.scenic b/tests/simulators/newtonian/driving.scenic index 5fb4bd695..9a7b84d51 100644 --- a/tests/simulators/newtonian/driving.scenic +++ b/tests/simulators/newtonian/driving.scenic @@ -21,3 +21,5 @@ third = new Car on visible ego.road, with behavior Potpourri require abs((apparent heading of third) - 180 deg) <= 30 deg new Object visible, with width 0.1, with length 0.1 + +terminate after 2 steps diff --git a/tests/simulators/newtonian/test_newtonian.py b/tests/simulators/newtonian/test_newtonian.py index c8dafae1d..a1ac5e4ad 100644 --- a/tests/simulators/newtonian/test_newtonian.py +++ b/tests/simulators/newtonian/test_newtonian.py @@ -1,5 +1,10 @@ +import os +from pathlib import Path + +from PIL import Image as IPImage import pytest +from scenic.domains.driving.roads import Network from scenic.simulators.newtonian import NewtonianSimulator from tests.utils import pickle_test, sampleScene, tryPickling @@ -33,6 +38,18 @@ def check(): check() # If we fail here, something is leaking. +@pytest.mark.graphical +def test_gif_creation(loadLocalScenario): + scenario = loadLocalScenario("driving.scenic", mode2D=True) + scene, _ = scenario.generate(maxIterations=1000) + path = Path("assets") / "maps" / "CARLA" / "Town01.xodr" + network = Network.fromFile(path) + simulator = NewtonianSimulator(render=True, network=network, export_gif=True) + simulation = simulator.simulate(scene, maxSteps=100) + gif_path = Path("") / "simulation.gif" + assert os.path.exists(gif_path) + + @pickle_test def test_pickle(loadLocalScenario): scenario = tryPickling(loadLocalScenario("basic.scenic")) From 1d434f9566a98112fece6bff89db57167c81bbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:59:49 -0500 Subject: [PATCH 07/28] feat: Adding Discourse Links to Relevant Sections (#272) * fix: Adding modifications to codecov.yml * feat: Adding references to Discourse channel where applicable * Update README.md Co-authored-by: Daniel Fremont * Update .github/ISSUE_TEMPLATE/config.yml Co-authored-by: Daniel Fremont --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Daniel Fremont --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 427eb214e..92854d330 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - name: Questions - url: https://forms.gle/uUhQNuPzQrvvBFJX9 - about: Send your questions via Google Form \ No newline at end of file + url: https://forum.scenic-lang.org/ + about: Post your questions on our community forum \ No newline at end of file diff --git a/README.md b/README.md index c8cf05417..f04c8f785 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Scenic was initially designed and implemented by Daniel J. Fremont, Tommaso Dreo Additionally, Edward Kim made major contributions to Scenic 2, and Eric Vin, Shun Kashiwa, Matthew Rhea, and Ellen Kalvan to Scenic 3. Please see our [Credits](https://scenic-lang.readthedocs.io/en/latest/credits.html) page for details and more contributors. -If you have any problems using Scenic, please submit an issue to [our GitHub repository](https://github.com/BerkeleyLearnVerify/Scenic). +If you have any problems using Scenic, please submit an issue to [our GitHub repository](https://github.com/BerkeleyLearnVerify/Scenic) or start a conversation on our [community forum](https://forum.scenic-lang.org/). The repository is organized as follows: From 2a1b29c7670daa88e902cdc6ba1d0403789d26c0 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Thu, 6 Jun 2024 20:18:28 -0700 Subject: [PATCH 08/28] Prepare 3.0.0 (#277) * add logo and favicon * bump version number and add link to homepage --- README.md | 2 +- docs/_static/custom.css | 6 +++ docs/conf.py | 7 ++++ docs/images/favicon.ico | Bin 0 -> 15086 bytes docs/images/logo-full.svg | 84 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 docs/images/favicon.ico create mode 100644 docs/images/logo-full.svg diff --git a/README.md b/README.md index f04c8f785..bb6b2911e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Scenic +[Scenic Logo](https://scenic-lang.org/) [![Documentation Status](https://readthedocs.org/projects/scenic-lang/badge/?version=latest)](https://scenic-lang.readthedocs.io/en/latest/?badge=latest) [![Tests Status](https://github.com/BerkeleyLearnVerify/Scenic/actions/workflows/run-tests.yml/badge.svg)](https://github.com/BerkeleyLearnVerify/Scenic/actions/workflows/run-tests.yml) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 88790a6a8..8968ef400 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -12,6 +12,12 @@ pre {tab-size: 4;} .wy-table-responsive table td, .wy-table-responsive table th {white-space: normal;} /* Increase maximum body width to support 100-character lines */ .wy-nav-content {max-width:900px;} +/* Make SVG logo render at the correct size */ +.wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { + width: 100%; +} +/* Modify background color behind logo to make it more readable */ +.wy-side-nav-search {background-color:#48ac92} /* Shrink the sidebar to 270 pixels wide */ .wy-tray-container li, .wy-menu-vertical, .wy-side-nav-search, .wy-nav-side, .rst-versions {width:270px;} diff --git a/docs/conf.py b/docs/conf.py index 16130e15d..69badcde9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -151,6 +151,13 @@ "custom.css", ] +html_logo = "images/logo-full.svg" +html_favicon = "images/favicon.ico" + +html_theme_options = { + "logo_only": True, +} + # -- Generate lists of keywords for the language reference ------------------- import itertools diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a60ee429b9d6eadf18b027048dd8768e8bb8707 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/docs/images/logo-full.svg b/docs/images/logo-full.svg new file mode 100644 index 000000000..e9c4caf5e --- /dev/null +++ b/docs/images/logo-full.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7b47374f3..53d351864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scenic" -version = "3.0.0rc1" +version = "3.0.0" description = "The Scenic scenario description language." authors = [ { name = "Daniel Fremont" }, @@ -84,6 +84,7 @@ dev = [ ] [project.urls] +Homepage = "https://scenic-lang.org/" Repository = "https://github.com/BerkeleyLearnVerify/Scenic" Documentation = "https://scenic-lang.readthedocs.io" From 71269167caf953120caede4aa5077325fb93d7ce Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Thu, 6 Jun 2024 20:49:36 -0700 Subject: [PATCH 09/28] add logo to sdist for README (#278) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 53d351864..3dc615e32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,9 @@ scenic = "scenic.syntax.pygment:ScenicStyle" requires = ["flit_core >= 3.2, <4"] build-backend = "flit_core.buildapi" +[tool.flit.sdist] +include = ["docs/images/logo-full.svg"] + [tool.black] line-length = 90 force-exclude = ''' From 0c667bdde92501a35d58bf6e0129bf22d1b8bba1 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:41:42 -0700 Subject: [PATCH 10/28] Minor Misc Fixes (#279) * Modified pruning test. * Webots example fix and docs minor update. * Update tests/syntax/test_pruning.py Co-authored-by: Daniel Fremont * Made path general. --------- Co-authored-by: Daniel Fremont --- docs/reference/functions.rst | 4 ++++ examples/webots/city_intersection/city_intersection.scenic | 6 +++--- tests/syntax/test_pruning.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/reference/functions.rst b/docs/reference/functions.rst index 066d3ede4..b2e589d94 100644 --- a/docs/reference/functions.rst +++ b/docs/reference/functions.rst @@ -70,6 +70,10 @@ localPath --------- The `localPath` function takes a relative path with respect to the directory containing the ``.scenic`` file where it is used, and converts it to an absolute path. Note that the path is returned as a `pathlib.Path` object. +.. versionchanged:: 3.0 + + This function now returns a `pathlib.Path` object instead of a string. + .. _verbosePrint_func: verbosePrint diff --git a/examples/webots/city_intersection/city_intersection.scenic b/examples/webots/city_intersection/city_intersection.scenic index 8b8533351..562c17402 100644 --- a/examples/webots/city_intersection/city_intersection.scenic +++ b/examples/webots/city_intersection/city_intersection.scenic @@ -52,13 +52,13 @@ class LogImageAction(Action): def applyTo(self, obj, sim): print("Other Car Visible:", self.visible) - target_path = self.path + "/" - target_path += "visible" if self.visible else "invisible" + target_path = self.path + target_path /= "visible" if self.visible else "invisible" if not os.path.exists(target_path): os.makedirs(target_path) - target_path += "/" + str(self.count) + ".jpeg" + target_path /= f"{self.count}.jpeg" print("IMG Path:", target_path) diff --git a/tests/syntax/test_pruning.py b/tests/syntax/test_pruning.py index 061e44379..6a0c693f6 100644 --- a/tests/syntax/test_pruning.py +++ b/tests/syntax/test_pruning.py @@ -55,14 +55,14 @@ def test_containment_2d_region(): # Test both combined, in a slightly more complicated case. # Specifically, there is a non vertical component to baseOffset - # that should be accounted for and the height is random. + # that should be accounted for. scenario = compileScenic( """ class TestObject: baseOffset: (0.1, 0, self.height/2) workspace = Workspace(PolygonalRegion([0@0, 2@0, 2@2, 0@2])) - ego = new TestObject on workspace, with height Range(0.1,0.5) + ego = new TestObject on workspace, with height 100 """ ) # Sampling should fail ~30.56% of the time, so From d7679fb88fbe825b5f980ff6ece828637232af41 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Wed, 12 Jun 2024 21:31:43 -0700 Subject: [PATCH 11/28] Docs update (#282) * use new documentation URL * add links to forum from documentation * fix rendering of logo on PyPI page --- .github/ISSUE_TEMPLATE/2-docs.yml | 6 +++--- README.md | 10 +++++----- docs/index.rst | 2 +- docs/quickstart.rst | 2 ++ examples/carla/NHTSA_Scenarios/README.md | 2 +- examples/webots/README.md | 2 +- pyproject.toml | 2 +- src/scenic/syntax/pygment.py | 2 +- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/2-docs.yml b/.github/ISSUE_TEMPLATE/2-docs.yml index a892af1c1..a5de65eac 100644 --- a/.github/ISSUE_TEMPLATE/2-docs.yml +++ b/.github/ISSUE_TEMPLATE/2-docs.yml @@ -1,5 +1,5 @@ name: Documentation -description: Report an issue or enhancement related to https://scenic-lang.readthedocs.io/ +description: Report an issue or enhancement related to https://docs.scenic-lang.org/ labels: - "type: documentation" - "status: triage" @@ -14,9 +14,9 @@ body: attributes: label: Describe the doc issue or enhancement description: > - Please provide a clear and concise description of what content in https://scenic-lang.readthedocs.io/ has an issue or needs enhancement. + Please provide a clear and concise description of what content in https://docs.scenic-lang.org/ has an issue or needs enhancement. placeholder: | - Link to location in the docs: https://scenic-lang.readthedocs.io/ + Link to location in the docs: https://docs.scenic-lang.org/ validations: required: true diff --git a/README.md b/README.md index bb6b2911e..a6732338e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -[Scenic Logo](https://scenic-lang.org/) +[Scenic Logo](https://scenic-lang.org/) -[![Documentation Status](https://readthedocs.org/projects/scenic-lang/badge/?version=latest)](https://scenic-lang.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/scenic-lang/badge/?version=latest)](https://docs.scenic-lang.org/en/latest/?badge=latest) [![Tests Status](https://github.com/BerkeleyLearnVerify/Scenic/actions/workflows/run-tests.yml/badge.svg)](https://github.com/BerkeleyLearnVerify/Scenic/actions/workflows/run-tests.yml) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) A compiler and scenario generator for Scenic, a domain-specific probabilistic programming language for modeling the environments of cyber-physical systems. -Please see the [documentation](https://scenic-lang.readthedocs.io/) for installation instructions, as well as tutorials and other information about the Scenic language, its implementation, and its interfaces to various simulators. +Please see the [documentation](https://docs.scenic-lang.org/) for installation instructions, as well as tutorials and other information about the Scenic language, its implementation, and its interfaces to various simulators. For an overview of the language and some of its applications, see our [2022 journal paper](https://link.springer.com/article/10.1007/s10994-021-06120-5) on Scenic 2, which extends our [PLDI 2019 paper](https://arxiv.org/abs/1809.09310) on Scenic 1. The new syntax and features of Scenic 3 are described in our [CAV 2023 paper](https://arxiv.org/abs/2307.03325). -Our [Publications](https://scenic-lang.readthedocs.io/en/latest/publications.html) page lists additional relevant publications. +Our [Publications](https://docs.scenic-lang.org/en/latest/publications.html) page lists additional relevant publications. Scenic was initially designed and implemented by Daniel J. Fremont, Tommaso Dreossi, Shromona Ghosh, Xiangyu Yue, Alberto L. Sangiovanni-Vincentelli, and Sanjit A. Seshia. Additionally, Edward Kim made major contributions to Scenic 2, and Eric Vin, Shun Kashiwa, Matthew Rhea, and Ellen Kalvan to Scenic 3. -Please see our [Credits](https://scenic-lang.readthedocs.io/en/latest/credits.html) page for details and more contributors. +Please see our [Credits](https://docs.scenic-lang.org/en/latest/credits.html) page for details and more contributors. If you have any problems using Scenic, please submit an issue to [our GitHub repository](https://github.com/BerkeleyLearnVerify/Scenic) or start a conversation on our [community forum](https://forum.scenic-lang.org/). diff --git a/docs/index.rst b/docs/index.rst index 2ef4a5881..cc5fbcd76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Our :doc:`publications ` page lists additional papers using Scenic Old code can likely be easily ported; you can also install older releases if necessary from `GitHub `__. -If you have any problems using Scenic, please submit an issue to `our GitHub repository `_ or contact Daniel at dfremont@ucsc.edu. +If you have any problems using Scenic, please submit an issue to `our GitHub repository `_ or ask a question on `our community forum `_. Table of Contents ================= diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 34fe517d0..aeabe83c3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -171,3 +171,5 @@ Depending on what you'd like to do with Scenic, different parts of the documenta * If you want to control Scenic from Python rather than using the command-line tool (for example if you want to collect data from the generated scenarios), see :doc:`api`. * If you want to add a feature to the language or otherwise need to understand Scenic's inner workings, see our pages on :doc:`developing` and :ref:`internals`. + +If you can't find something in the documentation, or have any question about Scenic, feel free to post on our `community forum `_. diff --git a/examples/carla/NHTSA_Scenarios/README.md b/examples/carla/NHTSA_Scenarios/README.md index b5eb7435f..3aaa8748e 100644 --- a/examples/carla/NHTSA_Scenarios/README.md +++ b/examples/carla/NHTSA_Scenarios/README.md @@ -4,7 +4,7 @@ This folder includes a library of Scenic programs written for use with the CARLA For questions and concerns, please contact Francis Indaheng at or post an issue to this repo. -*Note:* These scenarios require [VerifAI](https://verifai.readthedocs.io/) to be installed, since they use VerifAI's Halton sampler by default (the sampler type can be configured as explained [here](https://scenic-lang.readthedocs.io/en/latest/modules/scenic.core.external_params.html): for example, you can add `--param verifaiSamplerType random` when running Scenic to use random sampling instead). +*Note:* These scenarios require [VerifAI](https://verifai.readthedocs.io/) to be installed, since they use VerifAI's Halton sampler by default (the sampler type can be configured as explained [here](https://docs.scenic-lang.org/en/latest/modules/scenic.core.external_params.html): for example, you can add `--param verifaiSamplerType random` when running Scenic to use random sampling instead). ## Intersection diff --git a/examples/webots/README.md b/examples/webots/README.md index bcf3b0255..114279468 100644 --- a/examples/webots/README.md +++ b/examples/webots/README.md @@ -2,6 +2,6 @@ This folder contains example Scenic scenarios for use with the Webots robotics simulator. -In the **generic** folder we provide several Webots worlds (``.wbt`` files inside ``webots_data/worlds``) demonstrating scenarios with Scenic's [generic Webots interface](https://scenic-lang.readthedocs.io/en/latest/modules/scenic.simulators.webots.simulator.html). To run these, either install Scenic in the version of Python used by Webots or launch Webots from inside a virtual environment where Scenic is installed (the latter works as of Webots R2023a) then open one of the ``.wbt`` files. Starting the simulation will automatically start Scenic and repeatedly generate scenarios. +In the **generic** folder we provide several Webots worlds (``.wbt`` files inside ``webots_data/worlds``) demonstrating scenarios with Scenic's [generic Webots interface](https://docs.scenic-lang.org/en/latest/modules/scenic.simulators.webots.simulator.html). To run these, either install Scenic in the version of Python used by Webots or launch Webots from inside a virtual environment where Scenic is installed (the latter works as of Webots R2023a) then open one of the ``.wbt`` files. Starting the simulation will automatically start Scenic and repeatedly generate scenarios. __Licensing Note:__ The ``mars.wbt`` file is a modified version of the [Sojourner Rover example](https://cyberbotics.com/doc/guide/sojourner#sojourner-wbt) included in Webots. The original was written by Nicolas Uebelhart and is copyrighted by Cyberbotics Ltd. under the [Webots asset license](https://cyberbotics.com/webots_assets_license). Under the terms of that license, the modified version remains property of Cyberbotics; however, all other files in this directory are covered by the Scenic license. In particular, please feel free to model your own supervisor implementation on ``generic/webots_data/controllers/scenic_supervisor.py``. diff --git a/pyproject.toml b/pyproject.toml index 3dc615e32..8ef20b1c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ dev = [ [project.urls] Homepage = "https://scenic-lang.org/" Repository = "https://github.com/BerkeleyLearnVerify/Scenic" -Documentation = "https://scenic-lang.readthedocs.io" +Documentation = "https://docs.scenic-lang.org" [project.scripts] scenic = 'scenic.__main__:dummy' diff --git a/src/scenic/syntax/pygment.py b/src/scenic/syntax/pygment.py index b93ac5ce2..8af6d5630 100644 --- a/src/scenic/syntax/pygment.py +++ b/src/scenic/syntax/pygment.py @@ -353,7 +353,7 @@ class ScenicLexer(BetterPythonLexer): filenames = ["*.scenic"] alias_filenames = ["*.sc"] mimetypes = ["application/x-scenic", "text/x-scenic"] - url = "https://scenic-lang.readthedocs.org/" + url = "https://scenic-lang.org/" uni_name = PythonLexer.uni_name obj_name = rf"(?:(ego)|({uni_name}))" From 846b44d3923acb1dd47503918c8771d2f0e96d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:33:05 -0700 Subject: [PATCH 12/28] chore: gif cleanup for pytests (#285) * fix: Adding modifications to codecov.yml * chore: add cleanup to simulator.gif for pytests --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- .gitignore | 2 ++ tests/simulators/newtonian/test_newtonian.py | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 31bd1e964..d2bf7f6f9 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,5 @@ dmypy.json # generated parser src/scenic/syntax/parser.py + +simulation.gif \ No newline at end of file diff --git a/tests/simulators/newtonian/test_newtonian.py b/tests/simulators/newtonian/test_newtonian.py index a1ac5e4ad..b714479e2 100644 --- a/tests/simulators/newtonian/test_newtonian.py +++ b/tests/simulators/newtonian/test_newtonian.py @@ -48,6 +48,7 @@ def test_gif_creation(loadLocalScenario): simulation = simulator.simulate(scene, maxSteps=100) gif_path = Path("") / "simulation.gif" assert os.path.exists(gif_path) + os.remove(gif_path) @pickle_test From 7cde613cf79af5db100661c1f8a109eb4d3ec98a Mon Sep 17 00:00:00 2001 From: Daniel He Date: Tue, 9 Jul 2024 15:15:34 -0600 Subject: [PATCH 13/28] Fix Verifai Sampler with more than 10 objects (#280) * Fix verifai sampler for more than 10 objects * formatting * Fix typo --------- Co-authored-by: Daniel Fremont --- src/scenic/core/external_params.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/scenic/core/external_params.py b/src/scenic/core/external_params.py index 3fe6f453d..88848db4b 100644 --- a/src/scenic/core/external_params.py +++ b/src/scenic/core/external_params.py @@ -195,7 +195,7 @@ def __init__(self, params, globalParams): usingProbs = True space = verifai.features.FeatureSpace( { - f"param{index}": verifai.features.Feature(param.domain) + self.nameForParam(index): verifai.features.Feature(param.domain) for index, param in enumerate(self.params) } ) @@ -262,7 +262,12 @@ def getSample(self): return self.sampler.getSample() def valueFor(self, param): - return self.cachedSample[param.index] + return getattr(self.cachedSample, self.nameForParam(param.index)) + + @staticmethod + def nameForParam(i): + """Parameter name for a given index in the Feature Space.""" + return f"param{i}" class ExternalParameter(Distribution): From 2d26e8de757ddbbf3d884322965796c79fecd180 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Wed, 10 Jul 2024 14:04:41 -0700 Subject: [PATCH 14/28] Add test for VerifaiSampler with more than 10 features (#288) --- tests/syntax/test_verifai_samplers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/syntax/test_verifai_samplers.py b/tests/syntax/test_verifai_samplers.py index be57756c2..27e64607f 100644 --- a/tests/syntax/test_verifai_samplers.py +++ b/tests/syntax/test_verifai_samplers.py @@ -195,3 +195,11 @@ def test_noninterference(): for j in range(5): scene, iterations = scenario.generate(maxIterations=1) assert len(scenario.externalSampler.cachedSample) == 1 + + +def test_feature_order(): + scenario = compileScenic("param p = [VerifaiRange(x, x + 0.5) for x in range(105)]") + values = sampleParamP(scenario) + assert len(values) == 105 + for x, val in enumerate(values): + assert x <= val <= x + 0.5 From a34460a0c2dbeb392b730b7bc5425edd73e85120 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:34:12 +0200 Subject: [PATCH 15/28] Better Handling of Type Casting Functions (#283) * Type casting functions to classes to support type checking. * Revert "Type casting functions to classes to support type checking." This reverts commit 7c24395ebae8e1ef35822b96f0c8bad7dc3b6ebe. * Compiler based approach to Scenic friendly primitive type conversion. * Re-added tests and type signatures to cast functions. * Enhanced type checking tests. * Added documentation about int/float/str overrides. * Banned assignment to builtin type keywords. * Updated docs. * Added explicit compiler import. --- docs/conf.py | 5 +++ docs/reference/general.rst | 7 ++++ src/scenic/syntax/compiler.py | 18 ++++++++-- src/scenic/syntax/veneer.py | 33 ++++++++++-------- tests/syntax/test_errors.py | 13 +++++++ tests/syntax/test_typing.py | 64 +++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 17 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 69badcde9..d20aee6ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,7 @@ sphinx._buildingScenicDocs = True from scenic.core.simulators import SimulatorInterfaceWarning +import scenic.syntax.compiler from scenic.syntax.translator import CompileOptions import scenic.syntax.veneer as veneer @@ -185,6 +186,10 @@ def maketable(items, columns=5, gap=4): with open("_build/keywords_soft.txt", "w") as outFile: for row in maketable(ScenicParser.SOFT_KEYWORDS): outFile.write(row + "\n") +with open("_build/builtin_names.txt", "w") as outFile: + for row in maketable(scenic.syntax.compiler.builtinNames): + outFile.write(row + "\n") + # -- Monkeypatch ModuleAnalyzer to handle Scenic modules --------------------- diff --git a/docs/reference/general.rst b/docs/reference/general.rst index 58155e536..6d6e33ef5 100644 --- a/docs/reference/general.rst +++ b/docs/reference/general.rst @@ -29,3 +29,10 @@ To avoid confusion, we recommend not using ``distance``, ``angle``, ``offset``, .. literalinclude:: /_build/keywords_soft.txt :language: text + +.. rubric:: Builtin Names + +The following names are built into Scenic and can be used but not overwritten . + +.. literalinclude:: /_build/builtin_names.txt + :language: text diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index 9c8c567b4..a1996e32b 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -58,7 +58,7 @@ def compileScenicAST( trackedNames = {"ego", "workspace"} globalParametersName = "globalParameters" -builtinNames = {globalParametersName} +builtinNames = {globalParametersName, "str", "int", "float"} # shorthands for convenience @@ -540,7 +540,11 @@ def visit_Name(self, node: ast.Name) -> Any: if node.id in builtinNames: if not isinstance(node.ctx, ast.Load): raise self.makeSyntaxError(f'unexpected keyword "{node.id}"', node) - node = ast.copy_location(ast.Call(ast.Name(node.id, loadCtx), [], []), node) + # Convert global parameters name to a call + if node.id == globalParametersName: + node = ast.copy_location( + ast.Call(ast.Name(node.id, loadCtx), [], []), node + ) elif node.id in trackedNames: if not isinstance(node.ctx, ast.Load): raise self.makeSyntaxError( @@ -1078,6 +1082,16 @@ def visit_Call(self, node: ast.Call) -> Any: newArgs.append(self.visit(arg)) newKeywords = [self.visit(kwarg) for kwarg in node.keywords] newFunc = self.visit(node.func) + + # Convert primitive type conversions to their Scenic equivalents + if isinstance(newFunc, ast.Name): + if newFunc.id == "str": + newFunc.id = "_toStrScenic" + elif newFunc.id == "float": + newFunc.id = "_toFloatScenic" + elif newFunc.id == "int": + newFunc.id = "_toIntScenic" + if wrappedStar: newNode = ast.Call( ast.Name("callWithStarArgs", ast.Load()), diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index af6ce426c..7b03cc799 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -36,10 +36,10 @@ "hypot", "max", "min", + "_toStrScenic", + "_toFloatScenic", + "_toIntScenic", "filter", - "str", - "float", - "int", "round", "len", "range", @@ -2068,32 +2068,35 @@ def helper(context): ) -### Primitive functions overriding Python builtins - -# N.B. applying functools.wraps to preserve the metadata of the original -# functions seems to break pickling/unpickling - - -@distributionFunction -def filter(function, iterable): - return list(builtins.filter(function, iterable)) +### Primitive internal functions, utilized after compiler conversion @distributionFunction -def str(*args, **kwargs): +def _toStrScenic(*args, **kwargs) -> str: return builtins.str(*args, **kwargs) @distributionFunction -def float(*args, **kwargs): +def _toFloatScenic(*args, **kwargs) -> float: return builtins.float(*args, **kwargs) @distributionFunction -def int(*args, **kwargs): +def _toIntScenic(*args, **kwargs) -> int: return builtins.int(*args, **kwargs) +### Primitive functions overriding Python builtins + +# N.B. applying functools.wraps to preserve the metadata of the original +# functions seems to break pickling/unpickling + + +@distributionFunction +def filter(function, iterable): + return list(builtins.filter(function, iterable)) + + @distributionFunction def round(*args, **kwargs): return builtins.round(*args, **kwargs) diff --git a/tests/syntax/test_errors.py b/tests/syntax/test_errors.py index fded4f7ac..f51ad6e3b 100644 --- a/tests/syntax/test_errors.py +++ b/tests/syntax/test_errors.py @@ -25,6 +25,19 @@ def test_bad_extension(tmpdir): ### Parse errors + +## Reserved names +def test_reserved_type_names(): + with pytest.raises(ScenicSyntaxError): + compileScenic("float = 3") + + with pytest.raises(ScenicSyntaxError): + compileScenic("int = 3") + + with pytest.raises(ScenicSyntaxError): + compileScenic("str = 3") + + ## Constructor definitions diff --git a/tests/syntax/test_typing.py b/tests/syntax/test_typing.py index e8b6205c5..b70f2dc53 100644 --- a/tests/syntax/test_typing.py +++ b/tests/syntax/test_typing.py @@ -63,3 +63,67 @@ def test_list_as_vector_3(): param p = distance to [-2, -2, 0, 6] """ ) + + +# Builtin Type Conversion Tests +def test_isinstance_str(): + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = str(1) + assert isinstance(globalParameters.p, str) + assert isA(globalParameters.p, str) + """ + ) + assert isinstance(p, str) + + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = str(Range(0,2)) + assert isA(globalParameters.p, str) + """ + ) + assert isinstance(p, str) + + +def test_isinstance_float(): + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = float(1) + assert isinstance(globalParameters.p, float) + assert isA(globalParameters.p, float) + """ + ) + assert isinstance(p, float) + + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = float(Range(0,2)) + assert isA(globalParameters.p, float) + """ + ) + assert isinstance(p, float) + + +def test_isinstance_int(): + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = int(1.5) + assert isinstance(globalParameters.p, int) + assert isA(globalParameters.p, int) + """ + ) + assert isinstance(p, int) + + p = sampleParamPFrom( + """ + from scenic.core.type_support import isA + param p = int(Range(0,2)) + assert isA(globalParameters.p, int) + """ + ) + assert isinstance(p, int) From 652a7ec7441f61d02817911e81160bd6aa2a1f06 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Thu, 11 Jul 2024 23:43:02 +0200 Subject: [PATCH 16/28] Requirement Boolean Negation Fix (#289) * Added test to reproduce Issue#286 * Initial attempt for not transforming not. * convert globals used in requirements to distributions as needed * Simplified test. * Update src/scenic/syntax/compiler.py Co-authored-by: Daniel Fremont * Fixed renaming. --------- Co-authored-by: Daniel Fremont --- src/scenic/core/requirements.py | 6 +++++- src/scenic/syntax/compiler.py | 11 ++++++++++- tests/syntax/test_requirements.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/scenic/core/requirements.py b/src/scenic/core/requirements.py index 7ea177d21..f143ae552 100644 --- a/src/scenic/core/requirements.py +++ b/src/scenic/core/requirements.py @@ -9,7 +9,7 @@ import rv_ltl import trimesh -from scenic.core.distributions import Samplable, needsSampling +from scenic.core.distributions import Samplable, needsSampling, toDistribution from scenic.core.errors import InvalidScenarioError from scenic.core.lazy_eval import needsLazyEvaluation from scenic.core.propositions import Atomic, PropositionNode @@ -71,6 +71,10 @@ def compile(self, namespace, scenario, syntax=None): bindings, ego, line = self.bindings, self.egoObject, self.line condition, ty = self.condition, self.ty + # Convert bound values to distributions as needed + for name, value in bindings.items(): + bindings[name] = toDistribution(value) + # Check whether requirement implies any relations used for pruning canPrune = condition.check_constrains_sampling() if canPrune: diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index a1996e32b..bc15f1fee 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -240,6 +240,7 @@ class PropositionTransformer(Transformer): def __init__(self, filename="") -> None: super().__init__(filename) self.nextSyntaxId = 0 + self.inAtomic = False def transform( self, node: ast.AST, nextSyntaxId=0 @@ -260,6 +261,14 @@ def transform( newNode = self._create_atomic_proposition_factory(node) return newNode, self.nextSyntaxId + def generic_visit(self, node): + # Don't recurse inside atomics. + old_inAtomic = self.inAtomic + self.inAtomic = True + super_val = super().generic_visit(node) + self.inAtomic = old_inAtomic + return super_val + def _register_requirement_syntax(self, syntax): """register requirement syntax for later use returns an ID for retrieving the syntax @@ -337,7 +346,7 @@ def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST: def visit_UnaryOp(self, node): # rewrite `not` in requirements into a proposition factory - if not isinstance(node.op, ast.Not): + if not isinstance(node.op, ast.Not) or self.inAtomic: return self.generic_visit(node) lineNum = ast.Constant(node.lineno) diff --git a/tests/syntax/test_requirements.py b/tests/syntax/test_requirements.py index 1495afc19..0c06dd832 100644 --- a/tests/syntax/test_requirements.py +++ b/tests/syntax/test_requirements.py @@ -497,3 +497,14 @@ def test_random_occlusion(): hasattr(obj, "name") and obj.name == "wall" and (not obj.occluding) for obj in scene.objects ) + + +def test_deep_not(): + """Test that a not deep inside a requirement is interpreted correctly.""" + with pytest.raises(RejectionException): + sampleSceneFrom( + """ + objs = [new Object at 10@10, new Object at 20@20] + require all(not o.x > 0 for o in objs) + """ + ) From 6c3bc2b0a870e8def5be23b323e6919483dd4048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:13:16 -0700 Subject: [PATCH 17/28] feat: adding dynamic scenario and object creation tests for CARLA (#227) * feat: [wip] adding dynamic scenario tests for CARLA * fix: attempting to add some stateful behaviors * feat: adding throttle and brake dynamic scenario tests * feat: apply formatting for dynamic scenarios * feat: adding object creation tests * fix: reformatting CarlaSimulator import * fix: fixing BusStop error and adding all objects test * fix: removing typo * fix: adding compileScenic inline for testing files * fix: adding old blueprint names * fix: adding syntax error fix * fix: adding reworked examples with blueprint * fix: adding test cases using pytest parameterize * fix: attempting to rework some logic * fix: removing blueprint tests * fix: applying black and isort * fix: updating tests with specific position * fix: editing pyproject.toml to separate carla related tests * fix: add env variable for CARLA path and blueprint tests with manual skip * fix: correcting environment references * fix: reformatting file with black * fix: adding generic CARLA version to install * fix: CARLA not supported with python 3.11 * feat: adding flaky * fix: moving flaky to .[test] * fix: adding linux and windows check for CARLA pip installation * fix: adding suggested fixes, testing fixes soon * fix: adding fixes * fix: attempting to ignore all tests dependent on carla package in test_carla.py * fix: adding launchCarlaServer to getCarlaSimulator * fix: adding reformatting * fix: dividing dynamic tests into test_actions.py file * fix: reverting test/conftest.py to original settings * fix: blueprints getCarlaSimulator reference * fix: reworking CARLA simulator code * fix: removing unnecessary pytest skips in blueprints file * fix: further paramterizing blueprint tests and adding package scope for getCarlaSimulator * test: making changes to throttle and brake tests, will check if working * fix: addressing remaining revisions * fix: addressing more round of comments * fix: addressing more comments --- pyproject.toml | 2 +- src/scenic/simulators/carla/model.scenic | 2 +- tests/conftest.py | 3 +- tests/simulators/carla/test_actions.py | 120 ++++++++++++++++++ .../carla/{test_carla.py => test_basic.py} | 0 tests/simulators/carla/test_blueprints.py | 98 ++++++++++++++ 6 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 tests/simulators/carla/test_actions.py rename tests/simulators/carla/{test_carla.py => test_basic.py} (100%) create mode 100644 tests/simulators/carla/test_blueprints.py diff --git a/pyproject.toml b/pyproject.toml index 8ef20b1c1..960bc4724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,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 "astor >= 0.8.1", - 'carla >= 0.9.12; python_version <= "3.8" and (platform_system == "Linux" or platform_system == "Windows")', + 'carla >= 0.9.12; python_version <= "3.10" and (platform_system == "Linux" or platform_system == "Windows")', "dill", "exceptiongroup", "inflect ~= 5.5", diff --git a/src/scenic/simulators/carla/model.scenic b/src/scenic/simulators/carla/model.scenic index c9cac9dfb..66433de53 100644 --- a/src/scenic/simulators/carla/model.scenic +++ b/src/scenic/simulators/carla/model.scenic @@ -259,7 +259,7 @@ class Chair(Prop): class BusStop(Prop): - blueprint: Uniform(*blueprints.busStopsModels) + blueprint: Uniform(*blueprints.busStopModels) class Advertisement(Prop): diff --git a/tests/conftest.py b/tests/conftest.py index e5cfadcde..3fe2f4d92 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import os.path from pathlib import Path import re -import subprocess import sys import pytest @@ -41,7 +40,7 @@ def manager(): return manager -@pytest.fixture +@pytest.fixture(scope="session") def getAssetPath(): base = Path(__file__).parent.parent / "assets" diff --git a/tests/simulators/carla/test_actions.py b/tests/simulators/carla/test_actions.py new file mode 100644 index 000000000..e3dd51980 --- /dev/null +++ b/tests/simulators/carla/test_actions.py @@ -0,0 +1,120 @@ +import os +from pathlib import Path +import signal +import socket +import subprocess +import time + +import pytest + +try: + import carla + + from scenic.simulators.carla import CarlaSimulator +except ModuleNotFoundError: + pytest.skip("carla package not installed", allow_module_level=True) + +from tests.utils import compileScenic, sampleScene + + +def checkCarlaPath(): + CARLA_ROOT = os.environ.get("CARLA_ROOT") + if not CARLA_ROOT: + pytest.skip("CARLA_ROOT env variable not set.") + return CARLA_ROOT + + +def isCarlaServerRunning(host="localhost", port=2000): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(1) + try: + sock.connect((host, port)) + return True + except (socket.timeout, ConnectionRefusedError): + return False + + +@pytest.fixture(scope="package") +def getCarlaSimulator(getAssetPath): + carla_process = None + if not isCarlaServerRunning(): + CARLA_ROOT = checkCarlaPath() + carla_process = subprocess.Popen( + f"bash {CARLA_ROOT}/CarlaUE4.sh -RenderOffScreen", shell=True + ) + + for _ in range(30): + if isCarlaServerRunning(): + break + time.sleep(1) + + # Extra 5 seconds to ensure server startup + time.sleep(5) + + base = getAssetPath("maps/CARLA") + + def _getCarlaSimulator(town): + path = os.path.join(base, f"{town}.xodr") + simulator = CarlaSimulator(map_path=path, carla_map=town) + return simulator, town, path + + yield _getCarlaSimulator + + if carla_process: + subprocess.run("killall -9 CarlaUE4-Linux-Shipping", shell=True) + + +def test_throttle(getCarlaSimulator): + simulator, town, mapPath = getCarlaSimulator("Town01") + code = f""" + param map = r'{mapPath}' + param carla_map = '{town}' + param time_step = 1.0/10 + + model scenic.simulators.carla.model + + behavior DriveWithThrottle(): + while True: + take SetThrottleAction(1) + + ego = new Car at (369, -326), with behavior DriveWithThrottle + record ego.speed as CarSpeed + terminate after 5 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + records = simulation.result.records["CarSpeed"] + assert records[len(records) // 2][1] < records[-1][1] + + +def test_brake(getCarlaSimulator): + simulator, town, mapPath = getCarlaSimulator("Town01") + code = f""" + param map = r'{mapPath}' + param carla_map = '{town}' + param time_step = 1.0/10 + + model scenic.simulators.carla.model + + behavior DriveWithThrottle(): + while True: + take SetThrottleAction(1) + + behavior Brake(): + while True: + take SetThrottleAction(0), SetBrakeAction(1) + + behavior DriveThenBrake(): + do DriveWithThrottle() for 2 steps + do Brake() for 4 steps + + ego = new Car at (369, -326), with behavior DriveThenBrake + record final ego.speed as CarSpeed + terminate after 6 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + finalSpeed = simulation.result.records["CarSpeed"] + assert finalSpeed == pytest.approx(0.0, abs=1e-1) diff --git a/tests/simulators/carla/test_carla.py b/tests/simulators/carla/test_basic.py similarity index 100% rename from tests/simulators/carla/test_carla.py rename to tests/simulators/carla/test_basic.py diff --git a/tests/simulators/carla/test_blueprints.py b/tests/simulators/carla/test_blueprints.py new file mode 100644 index 000000000..4b7dcba25 --- /dev/null +++ b/tests/simulators/carla/test_blueprints.py @@ -0,0 +1,98 @@ +import pytest + +try: + import carla +except ModuleNotFoundError: + pytest.skip("carla package not installed", allow_module_level=True) + +from test_actions import getCarlaSimulator + +from scenic.simulators.carla.blueprints import ( + advertisementModels, + atmModels, + barrelModels, + barrierModels, + benchModels, + bicycleModels, + boxModels, + busStopModels, + carModels, + caseModels, + chairModels, + coneModels, + containerModels, + creasedboxModels, + debrisModels, + garbageModels, + gnomeModels, + ironplateModels, + kioskModels, + mailboxModels, + motorcycleModels, + plantpotModels, + tableModels, + trafficwarningModels, + trashModels, + truckModels, + vendingMachineModels, + walkerModels, +) +from tests.utils import compileScenic, sampleScene + + +def model_blueprint(simulator, mapPath, town, modelType, modelName): + code = f""" + param map = r'{mapPath}' + param carla_map = '{town}' + param time_step = 1.0/10 + + model scenic.simulators.carla.model + ego = new {modelType} with blueprint '{modelName}' + terminate after 1 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + obj = simulation.objects[0] + assert obj.blueprint == modelName + + +model_data = { + "Car": carModels, + "Bicycle": bicycleModels, + "Motorcycle": motorcycleModels, + "Truck": truckModels, + "Trash": trashModels, + "Cone": coneModels, + "Debris": debrisModels, + "VendingMachine": vendingMachineModels, + "Chair": chairModels, + "BusStop": busStopModels, + "Advertisement": advertisementModels, + "Garbage": garbageModels, + "Container": containerModels, + "Table": tableModels, + "Barrier": barrierModels, + "PlantPot": plantpotModels, + "Mailbox": mailboxModels, + "Gnome": gnomeModels, + "CreasedBox": creasedboxModels, + "Case": caseModels, + "Box": boxModels, + "Bench": benchModels, + "Barrel": barrelModels, + "ATM": atmModels, + "Kiosk": kioskModels, + "IronPlate": ironplateModels, + "TrafficWarning": trafficwarningModels, + "Pedestrian": walkerModels, +} + + +@pytest.mark.parametrize( + "modelType, modelName", + [(type, name) for type, names in model_data.items() for name in names], +) +def test_model_blueprints(getCarlaSimulator, modelType, modelName): + simulator, town, mapPath = getCarlaSimulator("Town01") + model_blueprint(simulator, mapPath, town, modelType, modelName) From f2c61b1e836e51558ac5515987570d80e9bb64e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:06:31 -0700 Subject: [PATCH 18/28] fix: Update simulator workflow (#291) * fix: Adding modifications to codecov.yml * fix: updating simulator workflow * fix: adding kill for background display --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- .github/workflows/run-simulators.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-simulators.yml b/.github/workflows/run-simulators.yml index 5b35cf85c..885e386a4 100644 --- a/.github/workflows/run-simulators.yml +++ b/.github/workflows/run-simulators.yml @@ -43,7 +43,7 @@ jobs: fi # wait for status checks to pass - TIMEOUT=120 # Timeout in seconds + TIMEOUT=300 # Timeout in seconds START_TIME=$(date +%s) END_TIME=$((START_TIME + TIMEOUT)) while true; do @@ -135,7 +135,7 @@ jobs: for version in "${carla_versions[@]}"; do echo "============================= CARLA $version =============================" export CARLA_ROOT="$version" - pytest tests/simulators/carla/test_carla.py + pytest tests/simulators/carla done ' @@ -160,8 +160,9 @@ jobs: for version in "${webots_versions[@]}"; do echo "============================= Webots $version =============================" export WEBOTS_ROOT="$version" - pytest tests/simulators/webots/test_webots.py + pytest tests/simulators/webots done + kill %1 ' stop_ec2_instance: From 488f45a8fa65499c8f267b750a85aca40b33b1cc Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:14:35 -0700 Subject: [PATCH 19/28] Heuristic Sampling Improvements (#287) * Better handling of heuristic sampling + alwaysProvidesOrientation stability. * Refactor and added test. * Removed breakpoint. * Refactor to handle rejection errors at a higher level. * Added additional test and cleanup. * Additional cleanup. * Added additional comment. * Car method improvements. * Removed old comment --- src/scenic/domains/driving/model.scenic | 28 ++++++++++++------------- src/scenic/domains/driving/roads.py | 12 ++--------- tests/domains/driving/test_driving.py | 23 ++++++++++++++++++++ tests/syntax/test_distributions.py | 16 ++++++++++++++ 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index b6732d2fb..988d03072 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -146,12 +146,12 @@ class DrivingObject: The simulation is rejected if the object is not in a lane. (Use `DrivingObject._lane` to get `None` instead.) """ - return network.laneAt(self, reject='object is not in a lane') + return network.laneAt(self.position, reject='object is not in a lane') @property def _lane(self) -> Optional[Lane]: """The `Lane` at the object's current position, if any.""" - return network.laneAt(self) + return network.laneAt(self.position) @property def laneSection(self) -> LaneSection: @@ -159,12 +159,12 @@ class DrivingObject: The simulation is rejected if the object is not in a lane. """ - return network.laneSectionAt(self, reject='object is not in a lane') + return network.laneSectionAt(self.position, reject='object is not in a lane') @property def _laneSection(self) -> Optional[LaneSection]: """The `LaneSection` at the object's current position, if any.""" - return network.laneSectionAt(self) + return network.laneSectionAt(self.position) @property def laneGroup(self) -> LaneGroup: @@ -172,12 +172,12 @@ class DrivingObject: The simulation is rejected if the object is not in a lane. """ - return network.laneGroupAt(self, reject='object is not in a lane') + return network.laneGroupAt(self.position, reject='object is not in a lane') @property def _laneGroup(self) -> Optional[LaneGroup]: """The `LaneGroup` at the object's current position, if any.""" - return network.laneGroupAt(self) + return network.laneGroupAt(self.position) @property def oppositeLaneGroup(self) -> LaneGroup: @@ -193,12 +193,12 @@ class DrivingObject: The simulation is rejected if the object is not on a road. """ - return network.roadAt(self, reject='object is not on a road') + return network.roadAt(self.position, reject='object is not on a road') @property def _road(self) -> Optional[Road]: """The `Road` at the object's current position, if any.""" - return network.roadAt(self) + return network.roadAt(self.position) @property def intersection(self) -> Intersection: @@ -206,12 +206,12 @@ class DrivingObject: The simulation is rejected if the object is not in an intersection. """ - return network.intersectionAt(self, reject='object is not in an intersection') + return network.intersectionAt(self.position, reject='object is not in an intersection') @property def _intersection(self) -> Optional[Intersection]: """The `Intersection` at the object's current position, if any.""" - return network.intersectionAt(self) + return network.intersectionAt(self.position) @property def crossing(self) -> PedestrianCrossing: @@ -219,12 +219,12 @@ class DrivingObject: The simulation is rejected if the object is not in a crosswalk. """ - return network.crossingAt(self, reject='object is not in a crossing') + return network.crossingAt(self.position, reject='object is not in a crossing') @property def _crossing(self) -> Optional[PedestrianCrossing]: """The `PedestrianCrossing` at the object's current position, if any.""" - return network.crossingAt(self) + return network.crossingAt(self.position) @property def element(self) -> NetworkElement: @@ -233,12 +233,12 @@ class DrivingObject: See `Network.elementAt` for the details of how this is determined. The simulation is rejected if the object is not in any network element. """ - return network.elementAt(self, reject='object is not on any network element') + return network.elementAt(self.position, reject='object is not on any network element') @property def _element(self) -> Optional[NetworkElement]: """The highest-level `NetworkElement` at the object's current position, if any.""" - return network.elementAt(self) + return network.elementAt(self.position) # Utility functions diff --git a/src/scenic/domains/driving/roads.py b/src/scenic/domains/driving/roads.py index ff247f8ab..48289f0c8 100644 --- a/src/scenic/domains/driving/roads.py +++ b/src/scenic/domains/driving/roads.py @@ -34,7 +34,6 @@ distributionFunction, distributionMethod, ) -from scenic.core.errors import InvalidScenarioError import scenic.core.geometry as geometry from scenic.core.object_types import Point from scenic.core.regions import PolygonalRegion, PolylineRegion @@ -56,16 +55,9 @@ def _toVector(thing: Vectorlike) -> Vector: return type_support.toVector(thing) -def _rejectSample(message): - if veneer.isActive(): - raise InvalidScenarioError(message) - else: - raise RejectionException(message) - - def _rejectIfNonexistent(element, name="network element"): if element is None: - _rejectSample(f"requested {name} does not exist") + raise RejectionException(f"requested {name} does not exist") return element @@ -1219,7 +1211,7 @@ def findElementWithin(distance): message = reject else: message = "requested element does not exist" - _rejectSample(message) + raise RejectionException(message) return None def _findPointInAll(self, point, things, key=lambda e: e): diff --git a/tests/domains/driving/test_driving.py b/tests/domains/driving/test_driving.py index 4ea809437..50d09266d 100644 --- a/tests/domains/driving/test_driving.py +++ b/tests/domains/driving/test_driving.py @@ -5,6 +5,7 @@ import pytest from scenic.core.distributions import RejectionException +from scenic.core.errors import InvalidScenarioError from scenic.core.geometry import TriangulationError from scenic.domains.driving.roads import Network from tests.utils import compileScenic, pickle_test, sampleEgo, sampleScene, tryPickling @@ -206,3 +207,25 @@ def test_pickle(cached_maps): unpickled = tryPickling(scenario) scene = sampleScene(unpickled, maxIterations=1000) tryPickling(scene) + + +def test_invalid_road_scenario(cached_maps): + with pytest.raises(InvalidScenarioError): + scenario = compileDrivingScenario( + cached_maps, + """ + ego = new Car at 80.6354425964952@-327.5431187869811 + param foo = ego.oppositeLaneGroup.sidewalk + """, + ) + + with pytest.raises(InvalidScenarioError): + # Set regionContainedIn to everywhere to hit driving domain specific code + # instead of high level not contained in workspace rejection. + scenario = compileDrivingScenario( + cached_maps, + """ + ego = new Car at 10000@10000, with regionContainedIn everywhere + param foo = ego.lane + """, + ) diff --git a/tests/syntax/test_distributions.py b/tests/syntax/test_distributions.py index c7fda04fc..c45572988 100644 --- a/tests/syntax/test_distributions.py +++ b/tests/syntax/test_distributions.py @@ -797,3 +797,19 @@ def test_object_expression(): for i in range(3): scene = sampleScene(scenario, maxIterations=50) assert len(scene.objects) == 3 + + +## Rejection vs Invalid Scenario Errors + + +def test_rejection_invalid(): + with pytest.raises(InvalidScenarioError): + compileScenic( + """ + from scenic.core.distributions import RejectionException + def foo(): + raise RejectionException("foo") + return Vector(1,1,1) + new Object at foo() + """ + ) From df5595c5264262e4f60e3ced0fae93475a686a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:02:11 -0700 Subject: [PATCH 20/28] fix: add blueprint for test_brake for CARLA simulator tests (#292) * fix: Adding modifications to codecov.yml * fix: test_brake test increase steps * fix: adding blueprint --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- tests/simulators/carla/test_actions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/simulators/carla/test_actions.py b/tests/simulators/carla/test_actions.py index e3dd51980..f0aede475 100644 --- a/tests/simulators/carla/test_actions.py +++ b/tests/simulators/carla/test_actions.py @@ -107,11 +107,13 @@ def test_brake(getCarlaSimulator): behavior DriveThenBrake(): do DriveWithThrottle() for 2 steps - do Brake() for 4 steps + do Brake() for 6 steps - ego = new Car at (369, -326), with behavior DriveThenBrake + ego = new Car at (369, -326), + with blueprint 'vehicle.toyota.prius', + with behavior DriveThenBrake record final ego.speed as CarSpeed - terminate after 6 steps + terminate after 8 steps """ scenario = compileScenic(code, mode2D=True) scene = sampleScene(scenario) From 5f134ad8a1442e540e8a2a2f11bcb6d37811c570 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:25:57 -0700 Subject: [PATCH 21/28] Minor Tweaks to PolygonalRegion (#290) * Minor tweaks to PolygonalRegion. * Fixed PolygonalRegion points normalization * Deprecated PolygonalRegion points method. * Added deprecation test for points. * Minor tweaks. * Clarified deprecationTest wrapper message and param. --- src/scenic/core/regions.py | 33 ++++++++++++------- .../simulators/webots/road/interface.py | 11 ++++--- tests/core/test_regions.py | 19 ++++++++++- tests/utils.py | 21 ++++++++++++ 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 529b516a9..fd5ca7b36 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -2748,7 +2748,7 @@ class PolygonalRegion(Region): def __init__( self, - points=None, + points=(), polygon=None, z=0, orientation=None, @@ -2759,8 +2759,8 @@ def __init__( name, points, polygon, z, *additionalDeps, orientation=orientation ) - # Store main parameter - self._points = points + # Normalize and store main parameters + self._points = () if points is None else tuple(points) self._polygon = polygon self.z = z @@ -2774,7 +2774,6 @@ def __init__( points = tuple(pt[:2] for pt in points) if len(points) == 0: raise ValueError("tried to create PolygonalRegion from empty point list!") - self.points = points polygon = shapely.geometry.Polygon(points) if isinstance(polygon, shapely.geometry.Polygon): @@ -2791,13 +2790,6 @@ def __init__( "tried to create PolygonalRegion with " f"invalid polygon {self.polygons}" ) - if ( - points is None - and len(self.polygons.geoms) == 1 - and len(self.polygons.geoms[0].interiors) == 0 - ): - self.points = tuple(self.polygons.geoms[0].exterior.coords[:-1]) - if self.polygons.is_empty: raise ValueError("tried to create empty PolygonalRegion") shapely.prepare(self.polygons) @@ -2972,6 +2964,16 @@ def unionAll(regions, buf=0): z = 0 if z is None else z return PolygonalRegion(polygon=union, orientation=orientation, z=z) + @property + @distributionFunction + def points(self): + warnings.warn( + "The `points` method is deprecated and will be removed in Scenic 3.3.0." + "Users should use the `boundary` method instead.", + DeprecationWarning, + ) + return self.boundary.points + @property @distributionFunction def boundary(self) -> "PolylineRegion": @@ -3049,7 +3051,14 @@ def __eq__(self, other): @cached def __hash__(self): - return hash((self.polygons, self.orientation, self.z)) + return hash( + ( + self._points, + self._polygon, + self.orientation, + self.z, + ) + ) class CircularRegion(PolygonalRegion): diff --git a/src/scenic/simulators/webots/road/interface.py b/src/scenic/simulators/webots/road/interface.py index 661d460bf..f9eed5356 100644 --- a/src/scenic/simulators/webots/road/interface.py +++ b/src/scenic/simulators/webots/road/interface.py @@ -210,13 +210,13 @@ def computeGeometry(self, crossroads, snapTolerance=0.05): def show(self, plt): if self.hasLeftSidewalk: - x, y = zip(*self.leftSidewalk.points) + x, y = zip(*[p[:2] for p in self.leftSidewalk.boundary.points]) plt.fill(x, y, "#A0A0FF") if self.hasRightSidewalk: - x, y = zip(*self.rightSidewalk.points) + x, y = zip(*[p[:2] for p in self.rightSidewalk.boundary.points]) plt.fill(x, y, "#A0A0FF") self.region.show(plt, style="r:") - x, y = zip(*self.lanes[0].points) + x, y = zip(*[p[:2] for p in self.lanes[0].boundary.points]) plt.fill(x, y, color=(0.8, 1.0, 0.8)) for lane, markers in enumerate(self.laneMarkers): x, y = zip(*markers) @@ -296,7 +296,10 @@ def __init__(self, world): allCells = [] drivableAreas = [] for road in self.roads: - assert road.region.polygons.is_valid, (road.waypoints, road.region.points) + assert road.region.polygons.is_valid, ( + road.waypoints, + road.region.boundary.points, + ) allCells.extend(road.cells) for crossroad in self.crossroads: if crossroad.region is not None: diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index ea48796b2..35b5077b0 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -5,10 +5,11 @@ import shapely.geometry import trimesh.voxel +from scenic.core.distributions import RandomControlFlowError, Range from scenic.core.object_types import Object, OrientedPoint from scenic.core.regions import * from scenic.core.vectors import VectorField -from tests.utils import sampleSceneFrom +from tests.utils import deprecationTest, sampleSceneFrom def sample_ignoring_rejections(region, num_samples): @@ -222,6 +223,14 @@ def test_polygon_region(): PolygonalRegion([(1, 1), (3, 1), (2, 2), (1.3, 1.15)], z=3).uniformPointInner().z == 3 ) + assert i != d + hash(i) + e = CircularRegion((0, 0), Range(1, 3)) + with pytest.raises(RandomControlFlowError): + i == e + with pytest.raises(RandomControlFlowError): + e == i + hash(e) def test_polygon_unionAll(): @@ -808,3 +817,11 @@ def test_region_combinations(A, B): # difference() difference_out = region_a.difference(region_b) assert isinstance(difference_out, Region) + + +## Deprecation Tests +@deprecationTest("3.3.0") +def test_polygons_points(): + points = ((1, 0, 0), (1, 1, 0), (2, 1, 0), (2, 0, 0)) + poly = PolygonalRegion(points) + assert set(poly.points) == set(points) diff --git a/tests/utils.py b/tests/utils.py index 9f4816ad2..3ae1cf8c1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,9 +1,12 @@ """Utilities used throughout the test suite.""" +import functools from importlib import metadata +import importlib.metadata import inspect import math import multiprocessing +import re import sys import types import weakref @@ -576,3 +579,21 @@ def ignorable(attr): fail() return False return True + + +def deprecationTest(removalVersion): + def decorator(function): + @functools.wraps(function) + def wrapper(*args, **kwargs): + m_ver = tuple(re.split(r"\D+", removalVersion)[:3]) + c_ver = tuple(re.split(r"\D+", importlib.metadata.version("scenic"))[:3]) + assert ( + m_ver > c_ver + ), "Maximum version exceeded. The tested functionality and the test itself should be removed." + + with pytest.deprecated_call(): + return function(*args, **kwargs) + + return wrapper + + return decorator From 797efdd62e7b6417cecf68d7de489f63998a6298 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:26:02 -0700 Subject: [PATCH 22/28] AABB Tweaks + Testing (#298) * AABB Tweaks + Testing * Added missing AABB update in workspaces.py. --- src/scenic/core/regions.py | 36 +++++++------ src/scenic/core/workspaces.py | 2 +- tests/core/test_regions.py | 98 +++++++++++++++++++++-------------- 3 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index fd5ca7b36..07f4c58b7 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -999,9 +999,8 @@ def isConvex(self): @property def AABB(self): return ( - tuple(self.mesh.bounds[:, 0]), - tuple(self.mesh.bounds[:, 1]), - tuple(self.mesh.bounds[:, 2]), + tuple(self.mesh.bounds[0]), + tuple(self.mesh.bounds[1]), ) @cached_property @@ -2307,9 +2306,8 @@ def actual_face(index): @property def AABB(self): return ( - tuple(self.voxelGrid.bounds[:, 0]), - tuple(self.voxelGrid.bounds[:, 1]), - tuple(self.voxelGrid.bounds[:, 2]), + tuple(self.voxelGrid.bounds[0]), + tuple(self.voxelGrid.bounds[1]), ) @property @@ -2368,8 +2366,8 @@ def intersect(self, other, triedReversed=False): return PolygonalRegion(polygon=self.polygons, z=other.z).intersect(other) if isinstance(other, PathRegion): - center_z = (other.AABB[2][1] + other.AABB[2][0]) / 2 - height = other.AABB[2][1] - other.AABB[2][0] + 1 + center_z = (other.AABB[0][2] + other.AABB[1][2]) / 2 + height = other.AABB[1][2] - other.AABB[0][2] + 1 return self.approxBoundFootprint(center_z, height).intersect(other) return super().intersect(other, triedReversed) @@ -2700,8 +2698,9 @@ def projectVector(self, point, onDirection): @cached_property def AABB(self): - return tuple( - zip(numpy.amin(self.vertices, axis=0), numpy.amax(self.vertices, axis=0)) + return ( + tuple(numpy.amin(self.vertices, axis=0)), + tuple(numpy.amax(self.vertices, axis=0)), ) def uniformPointInner(self): @@ -3014,7 +3013,7 @@ def projectVector(self, point, onDirection): @property def AABB(self): xmin, ymin, xmax, ymax = self.polygons.bounds - return ((xmin, ymin), (xmax, ymax), (self.z, self.z)) + return ((xmin, ymin, self.z), (xmax, ymax, self.z)) @distributionFunction def buffer(self, amount): @@ -3140,7 +3139,7 @@ def uniformPointInner(self): def AABB(self): x, y, _ = self.center r = self.radius - return ((x - r, y - r), (x + r, y + r), (self.z, self.z)) + return ((x - r, y - r, self.z), (x + r, y + r, self.z)) def __repr__(self): return f"CircularRegion({self.center!r}, {self.radius!r})" @@ -3328,7 +3327,7 @@ def AABB(self): x, y, z = zip(*self.corners) minx, maxx = findMinMax(x) miny, maxy = findMinMax(y) - return ((minx, miny), (maxx, maxy), (self.z, self.z)) + return ((minx, miny, self.z), (maxx, maxy, self.z)) def __repr__(self): return ( @@ -3639,7 +3638,7 @@ def length(self): @property def AABB(self): xmin, ymin, xmax, ymax = self.lineString.bounds - return ((xmin, ymin), (xmax, ymax), (0, 0)) + return ((xmin, ymin, 0), (xmax, ymax, 0)) def show(self, plt, style="r-", **kwargs): plotPolygon(self.lineString, plt, style=style, **kwargs) @@ -3743,6 +3742,10 @@ def intersects(self, other, triedReversed=False): return any(other.containsPoint(pt) for pt in self.points) def intersect(self, other, triedReversed=False): + # Try other way first before falling back to IntersectionRegion with sampler. + if triedReversed is False: + return other.intersect(self) + def sampler(intRegion): o = intRegion.regions[1] center, radius = o.circumcircle @@ -3793,8 +3796,9 @@ def projectVector(self, point, onDirection): @property def AABB(self): - return tuple( - zip(numpy.amin(self.points, axis=0), numpy.amax(self.points, axis=0)) + return ( + tuple(numpy.amin(self.points, axis=0)), + tuple(numpy.amax(self.points, axis=0)), ) def __eq__(self, other): diff --git a/src/scenic/core/workspaces.py b/src/scenic/core/workspaces.py index f302f70b3..8daec9aab 100644 --- a/src/scenic/core/workspaces.py +++ b/src/scenic/core/workspaces.py @@ -60,7 +60,7 @@ def show2D(self, plt): aabb = self.region.AABB except (NotImplementedError, TypeError): # unbounded Regions don't support this return - ((xmin, ymin), (xmax, ymax), _) = aabb + ((xmin, ymin, _), (xmax, ymax, _)) = aabb plt.xlim(xmin, xmax) plt.ylim(ymin, ymax) diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index 35b5077b0..b7293ab3c 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -75,7 +75,7 @@ def test_circular_region(): assert not circ3.intersects(circ) assert circ.distanceTo(Vector(4, -3)) == 0 assert circ.distanceTo(Vector(1, -7)) == pytest.approx(3) - assert circ.AABB == ((2, -5), (6, -1), (0, 0)) + assert circ.AABB == ((2, -5, 0), (6, -1, 0)) def test_circular_sampling(): @@ -108,7 +108,7 @@ def test_rectangular_region(): r3 = RectangularRegion(Vector(2.5, 4.5), 0, 1, 1) assert not rect.intersects(r3) assert rect.distanceTo((1 + 2 * math.sqrt(3), 4)) == pytest.approx(2) - (minx, miny), (maxx, maxy), _ = rect.AABB + (minx, miny, _), (maxx, maxy, _) = rect.AABB assert maxy == pytest.approx(3 + math.sqrt(3) / 2) assert miny == pytest.approx(1 - math.sqrt(3) / 2) assert maxx == pytest.approx(1.5 + math.sqrt(3)) @@ -136,7 +136,7 @@ def test_polyline_region(): assert pl.equallySpacedPoints(3) == list(pl.points) assert pl.pointsSeparatedBy(math.sqrt(2)) == list(pl.points[:-1]) assert pl.length == pytest.approx(2 * math.sqrt(2)) - assert pl.AABB == ((0, 0), (1, 2), (0, 0)) + assert pl.AABB == ((0, 0, 0), (1, 2, 0)) start = pl.start assert isinstance(start, OrientedPoint) assert start.position == (0, 2) @@ -205,7 +205,7 @@ def test_polygon_region(): assert poly.distanceTo((2, 1.1, 4)) == pytest.approx(4) assert poly.containsObject(Object._with(position=(2, 1.25), width=0.49, length=0.49)) assert not poly.containsObject(Object._with(position=(2, 1.25), width=1, length=0.49)) - assert poly.AABB == ((1, 1), (3, 2), (0, 0)) + assert poly.AABB == ((1, 1, 0), (3, 2, 0)) line = PolylineRegion([(1, 1), (2, 1.8)]) assert poly.intersects(line) assert line.intersects(poly) @@ -399,10 +399,10 @@ def test_path_region(): assert r2.distanceTo(Vector(0, 0, 0)) == pytest.approx(math.sqrt(18)) # Test AABB - assert r1.AABB == ((0, 1), (0, 1), (0, 0)) - assert r2.AABB == ((3, 4), (3, 4), (0, 3)) - assert r3.AABB == ((6, 7), (6, 7), (0, 3)) - assert r4.AABB == ((0, 7), (0, 7), (0, 3)) + assert r1.AABB == ((0, 0, 0), (1, 1, 0)) + assert r2.AABB == ((3, 3, 0), (4, 4, 3)) + assert r3.AABB == ((6, 6, 0), (7, 7, 3)) + assert r4.AABB == ((0, 0, 0), (7, 7, 3)) def test_mesh_polygon_intersection(): @@ -567,7 +567,7 @@ def test_pointset_region(): assert ps.distanceTo((3, 4)) == 0 assert ps.distanceTo((3, 5)) == pytest.approx(1) assert ps.distanceTo((2, 3)) == pytest.approx(math.sqrt(2)) - assert ps.AABB == ((1, 5), (2, 6), (0, 5)) + assert ps.AABB == ((1, 2, 0), (5, 6, 5)) def test_voxel_region(): @@ -603,7 +603,7 @@ def test_voxel_region(): sampled_pt = vr1.uniformPointInner() assert vr1.containsPoint(sampled_pt) - assert vr1.AABB == ((2.5, 5.5), (3.5, 6.5), (4.5, 7.5)) + assert vr1.AABB == ((2.5, 3.5, 4.5), (5.5, 6.5, 7.5)) vg2 = trimesh.voxel.VoxelGrid(encoding=numpy.asarray(encoding)) @@ -741,26 +741,33 @@ def test_orientation_inheritance(): assert c.intersect(r).orientation is v2 -# General test of region combinations +## Automated Region Tests REGIONS = { - MeshVolumeRegion: MeshVolumeRegion(trimesh.creation.box((0.75, 0.75, 0.75))), - MeshSurfaceRegion: MeshSurfaceRegion(trimesh.creation.box((0.5, 0.5, 0.5))), - BoxRegion: BoxRegion(), - SpheroidRegion: SpheroidRegion(), - PolygonalFootprintRegion: PolygonalRegion( - [(0, 0.5), (0, 1), (2, 1), (0, 0)] - ).footprint, - PathRegion: PathRegion(points=[(6, 6), (6, 7, 1), (7, 7, 2), [7, 6, 3]]), - PolygonalRegion: PolygonalRegion([(0, 0.5), (0, 1), (2, 1), (0, 0)]), - CircularRegion: CircularRegion(Vector(29, 34), 5), - SectorRegion: SectorRegion(Vector(29, 34), 5, 1, 0.5), - RectangularRegion: RectangularRegion(Vector(1, 2), math.radians(30), 4, 2), - PolylineRegion: PolylineRegion([(0, 2), (1, 1), (0, 0)]), - PointSetRegion: PointSetRegion("foo", [(1, 2), (3, 4), (5, 6)]), - ViewRegion: ViewRegion(50, (1, 1)), + AllRegion("all"), + EmptyRegion("none"), + MeshVolumeRegion(trimesh.creation.box((0.75, 0.75, 0.75))), + MeshSurfaceRegion(trimesh.creation.box((0.5, 0.5, 0.5))), + BoxRegion(), + SpheroidRegion(), + PolygonalRegion([(0, 0.5), (0, 1), (2, 1), (0, 0)]).footprint, + PathRegion(points=[(6, 6), (6, 7, 1), (7, 7, 2), [7, 6, 3]]), + PolygonalRegion([(0, 0.5), (0, 1), (2, 1), (0, 0)]), + CircularRegion(Vector(29, 34), 5), + SectorRegion(Vector(29, 34), 5, 1, 0.5), + RectangularRegion(Vector(1, 2), math.radians(30), 4, 2), + PolylineRegion([(0, 2), (1, 1), (0, 0)]), + PointSetRegion("foo", [(1, 2), (3, 4), (5, 6)]), + ViewRegion(50, (1, 1)), } + +def regions_id(val): + return type(val).__name__ + + +# General test of region combinations + INVALID_INTERSECTS = ( {MeshSurfaceRegion, PathRegion}, {MeshSurfaceRegion, PolygonalRegion}, @@ -776,21 +783,14 @@ def test_orientation_inheritance(): ) -def regions_id(val): - return val[0].__name__ - - @pytest.mark.slow -@pytest.mark.parametrize( - "A,B", itertools.combinations(REGIONS.items(), 2), ids=regions_id -) +@pytest.mark.parametrize("A,B", itertools.combinations(REGIONS, 2), ids=regions_id) def test_region_combinations(A, B): - type_a, region_a = A - type_b, region_b = B + region_a = A + region_b = B - ## Check type correctness ## - assert isinstance(region_a, type_a) - assert isinstance(region_b, type_b) + type_a = type(A) + type_b = type(B) ## Check all output combinations ## # intersects() @@ -819,6 +819,28 @@ def test_region_combinations(A, B): assert isinstance(difference_out, Region) +# Test Region AABB +@pytest.mark.slow +@pytest.mark.parametrize("region", REGIONS, ids=regions_id) +def test_region_AABB(region): + # Ensure region actually supports AABB + try: + region.AABB + except (NotImplementedError, TypeError): + return + + # Check general structure + assert isinstance(region.AABB, tuple) + assert all(isinstance(b, tuple) for b in region.AABB) + assert len(region.AABB) == 2 + assert all(len(b) == 3 for b in region.AABB) + + # Sample some points and check that they're all contained + for pt in sample_ignoring_rejections(region, 1000): + for i in range(len(pt)): + assert region.AABB[0][i] <= pt[i] <= region.AABB[1][i] + + ## Deprecation Tests @deprecationTest("3.3.0") def test_polygons_points(): From 50eed8ee7679a9625da7a29240656bebd78c8ef9 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:04:06 -0700 Subject: [PATCH 23/28] Requirement Parsing Fixes (#299) * Added deep boolean operator tests. * Moved requirement atomic checks to separate transformer. --- src/scenic/syntax/compiler.py | 30 ++++++++++++++---------------- tests/syntax/test_requirements.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index bc15f1fee..5328c0c6d 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -236,11 +236,20 @@ def makeSyntaxError(self, msg, node: ast.AST) -> ScenicParseError: } +class AtomicCheckTransformer(Transformer): + def visit_Call(self, node: ast.Call): + func = node.func + if isinstance(func, ast.Name) and func.id in TEMPORAL_PREFIX_OPS: + self.makeSyntaxError( + f'malformed use of the "{func.id}" temporal operator', node + ) + return self.generic_visit(node) + + class PropositionTransformer(Transformer): def __init__(self, filename="") -> None: super().__init__(filename) self.nextSyntaxId = 0 - self.inAtomic = False def transform( self, node: ast.AST, nextSyntaxId=0 @@ -262,12 +271,9 @@ def transform( return newNode, self.nextSyntaxId def generic_visit(self, node): - # Don't recurse inside atomics. - old_inAtomic = self.inAtomic - self.inAtomic = True - super_val = super().generic_visit(node) - self.inAtomic = old_inAtomic - return super_val + acv = AtomicCheckTransformer(self.filename) + acv.visit(node) + return node def _register_requirement_syntax(self, syntax): """register requirement syntax for later use @@ -346,7 +352,7 @@ def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST: def visit_UnaryOp(self, node): # rewrite `not` in requirements into a proposition factory - if not isinstance(node.op, ast.Not) or self.inAtomic: + if not isinstance(node.op, ast.Not): return self.generic_visit(node) lineNum = ast.Constant(node.lineno) @@ -367,14 +373,6 @@ def visit_UnaryOp(self, node): ) return ast.copy_location(newNode, node) - def visit_Call(self, node: ast.Call): - func = node.func - if isinstance(func, ast.Name) and func.id in TEMPORAL_PREFIX_OPS: - self.makeSyntaxError( - f'malformed use of the "{func.id}" temporal operator', node - ) - return self.generic_visit(node) - def visit_Always(self, node: s.Always): value = self.visit(node.value) if not self.is_proposition_factory(value): diff --git a/tests/syntax/test_requirements.py b/tests/syntax/test_requirements.py index 0c06dd832..0c7699fe0 100644 --- a/tests/syntax/test_requirements.py +++ b/tests/syntax/test_requirements.py @@ -508,3 +508,33 @@ def test_deep_not(): require all(not o.x > 0 for o in objs) """ ) + + +def test_deep_and(): + with pytest.raises(RejectionException): + sampleSceneFrom( + """ + objs = [new Object at 10@10, new Object at 20@20] + require all(o.x > 0 and o.x < 0 for o in objs) + """ + ) + + +def test_deep_or(): + with pytest.raises(RejectionException): + sampleSceneFrom( + """ + objs = [new Object at 10@10, new Object at 20@20] + require all(o.x < 0 or o.x < -1 for o in objs) + """ + ) + + +def test_temporal_in_atomic(): + with pytest.raises(ScenicSyntaxError): + sampleSceneFrom( + """ + objs = [new Object at 10@10, new Object at 20@20] + require all(eventually(o.x > 0) for o in objs) + """ + ) From e6b5f26c65815a9d1273b5ab854cbb1160fc95a8 Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:57:06 -0700 Subject: [PATCH 24/28] alwaysProvidesOrientation Patch and Test (#300) * 'alwaysProvidesOrientation Patch and Test * Modified warning message. --- src/scenic/syntax/veneer.py | 6 ++++++ tests/syntax/test_specifiers.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index 7b03cc799..4b550fbdd 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -249,6 +249,7 @@ import sys import traceback import typing +import warnings from scenic.core.distributions import ( Distribution, @@ -1521,6 +1522,11 @@ def alwaysProvidesOrientation(region): return sample.orientation is not None or sample is nowhere except RejectionException: return False + except Exception as e: + warnings.warn( + f"While sampling internally to determine if a random region provides an orientation, the following exception was raised: {repr(e)}" + ) + return False def OffsetBy(offset): diff --git a/tests/syntax/test_specifiers.py b/tests/syntax/test_specifiers.py index fe5e677f3..274e875e1 100644 --- a/tests/syntax/test_specifiers.py +++ b/tests/syntax/test_specifiers.py @@ -1198,3 +1198,19 @@ def test_color(): ego = new Object with color (1,1,1,1,1) """ sampleEgoFrom(program) + + +# alwaysProvidesOrientation +def test_alwaysProvidesOrientation_exception(): + with pytest.warns(UserWarning): + compileScenic( + """ + from scenic.core.distributions import distributionFunction + + @distributionFunction + def foo(bar): + assert False + + new Object in foo(Range(0,1)) + """ + ) From 0abbbcd09b12c403e903679f8f8a9f80c07282fb Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:09:39 -0700 Subject: [PATCH 25/28] Added no render option and fixed colors. (#302) --- src/scenic/core/object_types.py | 11 ++++++++++- src/scenic/simulators/webots/model.scenic | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index 8ecdff064..5230f0c83 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -1037,6 +1037,7 @@ class Object(OrientedPoint): "occluding": True, "showVisibleRegion": False, "color": None, + "render": True, "velocity": PropertyDefault((), {"dynamic"}, lambda self: Vector(0, 0, 0)), "speed": PropertyDefault((), {"dynamic"}, lambda self: 0), "angularVelocity": PropertyDefault((), {"dynamic"}, lambda self: Vector(0, 0, 0)), @@ -1550,6 +1551,9 @@ def show3D(self, viewer, highlight=False): if needsSampling(self): raise RuntimeError("tried to show() symbolic Object") + if not self.render: + return + # Render the object object_mesh = self.occupiedSpace.mesh.copy() @@ -1564,7 +1568,12 @@ def show3D(self, viewer, highlight=False): else: assert False - object_mesh.visual.face_colors = [255 * r, 255 * g, 255 * b, 255 * a] + object_mesh.visual.face_colors = [ + int(255 * r), + int(255 * g), + int(255 * b), + int(255 * a), + ] viewer.add_geometry(object_mesh) diff --git a/src/scenic/simulators/webots/model.scenic b/src/scenic/simulators/webots/model.scenic index 247f5a19f..0df1b70f7 100644 --- a/src/scenic/simulators/webots/model.scenic +++ b/src/scenic/simulators/webots/model.scenic @@ -303,7 +303,7 @@ class Hill(Terrain): height: 1 spread: 0.25 - color: (0,0,0,0) + render: False def heightAtOffset(self, offset): dx, dy, _ = offset From b0093d930992391eb0703b33aa15632982d1ca5d Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:21:04 -0700 Subject: [PATCH 26/28] Enhance CI Workflow for Scenic Simulators: Improved Volume Management, Reliability, and Cost Efficiency (#310) * Create and attach a new gp3 volume from the latest snapshot during EC2 instance setup. * Prevent broken pipe errors during CARLA connection. * Ensure EC2 instance stops even if tests fail, and perform volume cleanup to avoid AWS costs. * Increase CARLA connection timeouts to ensure tests pass reliably. --- .github/workflows/run-simulators.yml | 137 ++++++++++++++----------- tests/simulators/carla/test_actions.py | 14 +-- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/.github/workflows/run-simulators.yml b/.github/workflows/run-simulators.yml index 885e386a4..3b04f79df 100644 --- a/.github/workflows/run-simulators.yml +++ b/.github/workflows/run-simulators.yml @@ -10,13 +10,42 @@ jobs: runs-on: ubuntu-latest concurrency: group: sim + outputs: + volume_id: ${{ steps.create_volume_step.outputs.volume_id }} + env: + INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} steps: + - name: Create Volume from Latest Snapshot and Attach to Instance + id: create_volume_step + run: | + # Retrieve the latest snapshot ID + LATEST_SNAPSHOT_ID=$(aws ec2 describe-snapshots --owner-ids self --query 'Snapshots | sort_by(@, &StartTime) | [-1].SnapshotId' --output text) + echo "Checking availability for snapshot: $LATEST_SNAPSHOT_ID" + + # Wait for the snapshot to complete + aws ec2 wait snapshot-completed --snapshot-ids $LATEST_SNAPSHOT_ID + echo "Snapshot is ready." + + # Create a new volume from the latest snapshot + volume_id=$(aws ec2 create-volume --snapshot-id $LATEST_SNAPSHOT_ID --availability-zone us-west-1b --volume-type gp3 --size 400 --throughput 250 --query "VolumeId" --output text) + echo "Created volume with ID: $volume_id" + + # Set volume_id as output + echo "volume_id=$volume_id" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + # Wait until the volume is available + aws ec2 wait volume-available --volume-ids $volume_id + echo "Volume is now available" + + # Attach the volume to the instance + aws ec2 attach-volume --volume-id $volume_id --instance-id $INSTANCE_ID --device /dev/sda1 + echo "Volume $volume_id attached to instance $INSTANCE_ID as /dev/sda1" + - name: Start EC2 Instance - env: - INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} run: | # Get the instance state instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') @@ -27,7 +56,7 @@ jobs: sleep 10 instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') done - + # Check if instance state is "stopped" if [[ "$instance_state" == "stopped" ]]; then echo "Instance is stopped, starting it..." @@ -42,34 +71,17 @@ jobs: exit 1 fi - # wait for status checks to pass - TIMEOUT=300 # Timeout in seconds - START_TIME=$(date +%s) - END_TIME=$((START_TIME + TIMEOUT)) - while true; do - response=$(aws ec2 describe-instance-status --instance-ids $INSTANCE_ID) - system_status=$(echo "$response" | jq -r '.InstanceStatuses[0].SystemStatus.Status') - instance_status=$(echo "$response" | jq -r '.InstanceStatuses[0].InstanceStatus.Status') - - if [[ "$system_status" == "ok" && "$instance_status" == "ok" ]]; then - echo "Both SystemStatus and InstanceStatus are 'ok'" - exit 0 - fi - - CURRENT_TIME=$(date +%s) - if [[ "$CURRENT_TIME" -ge "$END_TIME" ]]; then - echo "Timeout: Both SystemStatus and InstanceStatus have not reached 'ok' state within $TIMEOUT seconds." - exit 1 - fi - - sleep 10 # Check status every 10 seconds - done + # Wait for instance status checks to pass + echo "Waiting for instance status checks to pass..." + aws ec2 wait instance-status-ok --instance-ids $INSTANCE_ID + echo "Instance is now ready for use." + check_simulator_version_updates: name: check_simulator_version_updates runs-on: ubuntu-latest needs: start_ec2_instance - steps: + steps: - name: Check for Simulator Version Updates env: PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} @@ -109,11 +121,11 @@ jobs: echo "NVIDIA Driver is not set" exit 1 fi - ' + ' - name: NVIDIA Driver is not set if: ${{ failure() }} run: | - echo "NVIDIA SMI is not working, please run the steps here on the instance:" + echo "NVIDIA SMI is not working, please run the steps here on the instance:" echo "https://scenic-lang.atlassian.net/wiki/spaces/KAN/pages/2785287/Setting+Up+AWS+VM?parentProduct=JSW&initialAllowedFeatures=byline-contributors.byline-extensions.page-comments.delete.page-reactions.inline-comments.non-licensed-share&themeState=dark%253Adark%2520light%253Alight%2520spacing%253Aspacing%2520colorMode%253Alight&locale=en-US#Install-NVIDIA-Drivers" run_carla_simulators: @@ -128,17 +140,17 @@ jobs: USER_NAME: ${{secrets.SSH_USERNAME}} run: | echo "$PRIVATE_KEY" > private_key && chmod 600 private_key - ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} ' + ssh -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -i private_key ${USER_NAME}@${HOSTNAME} ' cd /home/ubuntu/actions/Scenic && source venv/bin/activate && carla_versions=($(find /software -maxdepth 1 -type d -name 'carla*')) && for version in "${carla_versions[@]}"; do - echo "============================= CARLA $version =============================" + echo "============================= CARLA $version =============================" export CARLA_ROOT="$version" pytest tests/simulators/carla done ' - + run_webots_simulators: name: run_webots_simulators runs-on: ubuntu-latest @@ -164,39 +176,44 @@ jobs: done kill %1 ' - + stop_ec2_instance: name: stop_ec2_instance runs-on: ubuntu-latest - needs: [run_carla_simulators, run_webots_simulators] - steps: + needs: [start_ec2_instance, check_simulator_version_updates, check_nvidia_smi, run_carla_simulators, run_webots_simulators] + if: always() + env: + VOLUME_ID: ${{ needs.start_ec2_instance.outputs.volume_id }} + INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + steps: - name: Stop EC2 Instance - env: - INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} run: | - # Get the instance state + # Get the instance state and stop it if running instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') - - # If the machine is pending wait for it to fully start - while [ "$instance_state" == "pending" ]; do - echo "Instance is pending startup, waiting for it to fully start..." - sleep 10 - instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') - done - - # Check if instance state is "stopped" if [[ "$instance_state" == "running" ]]; then - echo "Instance is running, stopping it..." - aws ec2 stop-instances --instance-ids $INSTANCE_ID - elif [[ "$instance_state" == "stopping" ]]; then - echo "Instance is stopping..." + echo "Instance is running, stopping it..." + aws ec2 stop-instances --instance-ids $INSTANCE_ID + aws ec2 wait instance-stopped --instance-ids $INSTANCE_ID + echo "Instance has stopped." elif [[ "$instance_state" == "stopped" ]]; then - echo "Instance is already stopped..." - exit 0 + echo "Instance is already stopped." else - echo "Unknown instance state: $instance_state" - exit 1 + echo "Unexpected instance state: $instance_state" + exit 1 fi + + - name: Detach Volume + run: | + # Detach the volume + aws ec2 detach-volume --volume-id $VOLUME_ID + aws ec2 wait volume-available --volume-ids $VOLUME_ID + echo "Volume $VOLUME_ID detached." + + - name: Delete Volume + run: | + # Delete the volume after snapshot is complete + aws ec2 delete-volume --volume-id $VOLUME_ID + echo "Volume $VOLUME_ID deleted." diff --git a/tests/simulators/carla/test_actions.py b/tests/simulators/carla/test_actions.py index f0aede475..7914ad04a 100644 --- a/tests/simulators/carla/test_actions.py +++ b/tests/simulators/carla/test_actions.py @@ -43,19 +43,21 @@ def getCarlaSimulator(getAssetPath): f"bash {CARLA_ROOT}/CarlaUE4.sh -RenderOffScreen", shell=True ) - for _ in range(30): + for _ in range(180): if isCarlaServerRunning(): break time.sleep(1) + else: + pytest.fail("Unable to connect to CARLA.") # Extra 5 seconds to ensure server startup - time.sleep(5) + time.sleep(10) base = getAssetPath("maps/CARLA") def _getCarlaSimulator(town): path = os.path.join(base, f"{town}.xodr") - simulator = CarlaSimulator(map_path=path, carla_map=town) + simulator = CarlaSimulator(map_path=path, carla_map=town, timeout=180) return simulator, town, path yield _getCarlaSimulator @@ -76,7 +78,7 @@ def test_throttle(getCarlaSimulator): behavior DriveWithThrottle(): while True: take SetThrottleAction(1) - + ego = new Car at (369, -326), with behavior DriveWithThrottle record ego.speed as CarSpeed terminate after 5 steps @@ -109,8 +111,8 @@ def test_brake(getCarlaSimulator): do DriveWithThrottle() for 2 steps do Brake() for 6 steps - ego = new Car at (369, -326), - with blueprint 'vehicle.toyota.prius', + ego = new Car at (369, -326), + with blueprint 'vehicle.toyota.prius', with behavior DriveThenBrake record final ego.speed as CarSpeed terminate after 8 steps From 4ae8ee2f254e82675d2625e081cd157d19356e3d Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:18:25 -0800 Subject: [PATCH 27/28] Newtonian Simulator Fixes and Cleanup (#309) * Fixed scaling-warp issues. * Fixed wobble, cleaned up, and added debug render mode. * Added per car width/height scaling. * Moved scenicToScreenVal int cast. * Added clarification comment. --- src/scenic/simulators/newtonian/car.png | Bin 73799 -> 68790 bytes .../simulators/newtonian/driving_model.scenic | 4 +- src/scenic/simulators/newtonian/simulator.py | 83 +++++++++++++----- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/src/scenic/simulators/newtonian/car.png b/src/scenic/simulators/newtonian/car.png index 46f39d574c401515262c9cccf5025575a101cc92..a6f7d014d063c02dddab28c13fbeb222713dc621 100644 GIT binary patch literal 68790 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 AxDcKsCq;LkQtqN?FYz;`RWYX)FP-eSHT6;MK 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 D8t+mw literal 73799 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Μ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

@F$zXlNSJ?x9zYw1?LYPockbWwYmYB(6adk^I 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 X$aytzuW>YMeYC5iy93+q=L`P^EnoQa diff --git a/src/scenic/simulators/newtonian/driving_model.scenic b/src/scenic/simulators/newtonian/driving_model.scenic index 1c01ccab2..a976dbdd0 100644 --- a/src/scenic/simulators/newtonian/driving_model.scenic +++ b/src/scenic/simulators/newtonian/driving_model.scenic @@ -14,7 +14,9 @@ from scenic.domains.driving.model import * # includes basic actions and behavio from scenic.simulators.utils.colors import Color -simulator NewtonianSimulator(network, render=render) +param debugRender = False + +simulator NewtonianSimulator(network, render=render, debug_render=globalParameters.debugRender) class NewtonianActor(DrivingObject): throttle: 0 diff --git a/src/scenic/simulators/newtonian/simulator.py b/src/scenic/simulators/newtonian/simulator.py index fd38aa427..193103ab7 100644 --- a/src/scenic/simulators/newtonian/simulator.py +++ b/src/scenic/simulators/newtonian/simulator.py @@ -5,6 +5,7 @@ from math import copysign, degrees, radians, sin import os import pathlib +import statistics import time from PIL import Image @@ -58,15 +59,16 @@ class NewtonianSimulator(DrivingSimulator): when not otherwise specified is still 0.1 seconds. """ - def __init__(self, network=None, render=False, export_gif=False): + def __init__(self, network=None, render=False, debug_render=False, export_gif=False): super().__init__() self.export_gif = export_gif self.render = render + self.debug_render = debug_render self.network = network def createSimulation(self, scene, **kwargs): simulation = NewtonianSimulation( - scene, self.network, self.render, self.export_gif, **kwargs + scene, self.network, self.render, self.export_gif, self.debug_render, **kwargs ) if self.export_gif and self.render: simulation.generate_gif("simulation.gif") @@ -76,11 +78,14 @@ def createSimulation(self, scene, **kwargs): class NewtonianSimulation(DrivingSimulation): """Implementation of `Simulation` for the Newtonian simulator.""" - def __init__(self, scene, network, render, export_gif, timestep, **kwargs): + def __init__( + self, scene, network, render, export_gif, debug_render, timestep, **kwargs + ): self.export_gif = export_gif self.render = render self.network = network self.frames = [] + self.debug_render = debug_render if timestep is None: timestep = 0.1 @@ -102,10 +107,31 @@ def setup(self): ) self.screen.fill((255, 255, 255)) x, y, _ = self.objects[0].position - self.min_x, self.max_x = min_x - 50, max_x + 50 - self.min_y, self.max_y = min_y - 50, max_y + 50 + self.min_x, self.max_x = min_x - 40, max_x + 40 + self.min_y, self.max_y = min_y - 40, max_y + 40 self.size_x = self.max_x - self.min_x self.size_y = self.max_y - self.min_y + + # Generate a uniform screen scaling (applied to width and height) + # that includes all of both dimensions. + self.screenScaling = min(WIDTH / self.size_x, HEIGHT / self.size_y) + + # Calculate a screen translation that brings the mean vehicle + # position to the center of the screen. + + # N.B. screenTranslation is initialized to (0, 0) here intentionally. + # so that the actual screenTranslation can be set later based off what + # was computed with this null value. + self.screenTranslation = (0, 0) + + scaled_positions = map( + lambda x: self.scenicToScreenVal(x.position), self.objects + ) + mean_x, mean_y = map(statistics.mean, zip(*scaled_positions)) + + self.screenTranslation = (WIDTH / 2 - mean_x, HEIGHT / 2 - mean_y) + + # Create screen polygon to avoid rendering entirely invisible images self.screen_poly = shapely.geometry.Polygon( ( (self.min_x, self.min_y), @@ -117,9 +143,7 @@ def setup(self): img_path = os.path.join(current_dir, "car.png") self.car = pygame.image.load(img_path) - self.car_width = int(3.5 * WIDTH / self.size_x) - self.car_height = self.car_width - self.car = pygame.transform.scale(self.car, (self.car_width, self.car_height)) + self.parse_network() self.draw_objects() @@ -149,9 +173,14 @@ def addRegion(region, color, width=1): def scenicToScreenVal(self, pos): x, y = pos[:2] - x_prop = (x - self.min_x) / self.size_x - y_prop = (y - self.min_y) / self.size_y - return int(x_prop * WIDTH), HEIGHT - 1 - int(y_prop * HEIGHT) + + screen_x = (x - self.min_x) * self.screenScaling + screen_y = HEIGHT - 1 - (y - self.min_y) * self.screenScaling + + screen_x = screen_x + self.screenTranslation[0] + screen_y = screen_y + self.screenTranslation[1] + + return int(screen_x), int(screen_y) def createObjectInSimulator(self, obj): # Set actor's initial speed @@ -207,21 +236,14 @@ def draw_objects(self): for i, obj in enumerate(self.objects): color = (255, 0, 0) if i == 0 else (0, 0, 255) - h, w = obj.length, obj.width - pos_vec = Vector(-1.75, 1.75) - neg_vec = Vector(w / 2, h / 2) - heading_vec = Vector(0, 10).rotatedBy(obj.heading) - dx, dy = int(heading_vec.x), -int(heading_vec.y) - x, y = self.scenicToScreenVal(obj.position) - rect_x, rect_y = self.scenicToScreenVal(obj.position + pos_vec) + + if self.debug_render: + self.draw_rect(obj, color) + if hasattr(obj, "isCar") and obj.isCar: - self.rotated_car = pygame.transform.rotate( - self.car, math.degrees(obj.heading) - ) - self.screen.blit(self.rotated_car, (rect_x, rect_y)) + self.draw_car(obj) else: - corners = [self.scenicToScreenVal(corner) for corner in obj._corners2D] - pygame.draw.polygon(self.screen, color, corners) + self.draw_rect(obj, color) pygame.display.update() @@ -232,6 +254,19 @@ def draw_objects(self): time.sleep(self.timestep) + def draw_rect(self, obj, color): + corners = [self.scenicToScreenVal(corner) for corner in obj._corners2D] + pygame.draw.polygon(self.screen, color, corners) + + def draw_car(self, obj): + car_width = int(obj.width * self.screenScaling) + car_height = int(obj.height * self.screenScaling) + scaled_car = pygame.transform.scale(self.car, (car_width, car_height)) + rotated_car = pygame.transform.rotate(scaled_car, math.degrees(obj.heading)) + car_rect = rotated_car.get_rect() + car_rect.center = self.scenicToScreenVal(obj.position) + self.screen.blit(rotated_car, car_rect) + def generate_gif(self, filename="simulation.gif"): imgs = [Image.fromarray(frame) for frame in self.frames] imgs[0].save(filename, save_all=True, append_images=imgs[1:], duration=50, loop=0) From fe28e13b52c220a04e46fecc78370560470c4684 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Tue, 26 Nov 2024 11:13:49 -0800 Subject: [PATCH 28/28] Fix requirements inside loops and functions (#316) * fix require statements inside loops * fix require statements inside functions * fix RectangularRegion with random coerced parameter * fix require statements with random closure variables --- src/scenic/core/dynamics/scenarios.py | 7 +-- src/scenic/core/regions.py | 4 +- src/scenic/core/requirements.py | 79 +++++++++++++++++--------- src/scenic/syntax/compiler.py | 9 +-- tests/syntax/test_requirements.py | 81 +++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 35 deletions(-) diff --git a/src/scenic/core/dynamics/scenarios.py b/src/scenic/core/dynamics/scenarios.py index 77ec470e8..61a55e48c 100644 --- a/src/scenic/core/dynamics/scenarios.py +++ b/src/scenic/core/dynamics/scenarios.py @@ -58,7 +58,7 @@ def __init__(self, *args, **kwargs): self._objects = [] # ordered for reproducibility self._sampledObjects = self._objects self._externalParameters = [] - self._pendingRequirements = defaultdict(list) + self._pendingRequirements = [] self._requirements = [] # things needing to be sampled to evaluate the requirements self._requirementDeps = set() @@ -409,9 +409,8 @@ def _registerObject(self, obj): def _addRequirement(self, ty, reqID, req, line, name, prob): """Save a requirement defined at compile-time for later processing.""" - assert reqID not in self._pendingRequirements preq = PendingRequirement(ty, req, line, prob, name, self._ego) - self._pendingRequirements[reqID] = preq + self._pendingRequirements.append((reqID, preq)) def _addDynamicRequirement(self, ty, req, line, name): """Add a requirement defined during a dynamic simulation.""" @@ -429,7 +428,7 @@ def _compileRequirements(self): namespace = self._dummyNamespace if self._dummyNamespace else self.__dict__ requirementSyntax = self._requirementSyntax assert requirementSyntax is not None - for reqID, requirement in self._pendingRequirements.items(): + for reqID, requirement in self._pendingRequirements: syntax = requirementSyntax[reqID] if requirementSyntax else None # Catch the simple case where someone has most likely forgotten the "monitor" diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 07f4c58b7..4732fda1c 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3275,7 +3275,9 @@ def __init__(self, position, heading, width, length, name=None): self.circumcircle = (self.position, self.radius) super().__init__( - polygon=self._makePolygons(position, heading, width, length), + polygon=self._makePolygons( + self.position, self.heading, self.width, self.length + ), z=self.position.z, name=name, additionalDeps=deps, diff --git a/src/scenic/core/requirements.py b/src/scenic/core/requirements.py index f143ae552..ab6de91da 100644 --- a/src/scenic/core/requirements.py +++ b/src/scenic/core/requirements.py @@ -50,16 +50,21 @@ def __init__(self, ty, condition, line, prob, name, ego): # condition is an instance of Proposition. Flatten to get a list of atomic propositions. atoms = condition.atomics() - bindings = {} + self.globalBindings = {} # bindings to global/builtin names + self.closureBindings = {} # bindings to top-level closure variables + self.cells = [] # cells used in referenced closures atomGlobals = None for atom in atoms: - bindings.update(getAllGlobals(atom.closure)) + gbindings, cbindings, closures = getNameBindings(atom.closure) + self.globalBindings.update(gbindings) + self.closureBindings.update(cbindings) + for closure in closures: + self.cells.extend(closure.__closure__) globs = atom.closure.__globals__ if atomGlobals is not None: assert globs is atomGlobals else: atomGlobals = globs - self.bindings = bindings self.egoObject = ego def compile(self, namespace, scenario, syntax=None): @@ -68,21 +73,28 @@ def compile(self, namespace, scenario, syntax=None): While we're at it, determine whether the requirement implies any relations we can use for pruning, and gather all of its dependencies. """ - bindings, ego, line = self.bindings, self.egoObject, self.line + globalBindings, closureBindings = self.globalBindings, self.closureBindings + cells, ego, line = self.cells, self.egoObject, self.line condition, ty = self.condition, self.ty # Convert bound values to distributions as needed - for name, value in bindings.items(): - bindings[name] = toDistribution(value) + for name, value in globalBindings.items(): + globalBindings[name] = toDistribution(value) + for name, value in closureBindings.items(): + closureBindings[name] = toDistribution(value) + cells = tuple((cell, toDistribution(cell.cell_contents)) for cell in cells) + allBindings = dict(globalBindings) + allBindings.update(closureBindings) # Check whether requirement implies any relations used for pruning canPrune = condition.check_constrains_sampling() if canPrune: - relations.inferRelationsFrom(syntax, bindings, ego, line) + relations.inferRelationsFrom(syntax, allBindings, ego, line) # Gather dependencies of the requirement deps = set() - for value in bindings.values(): + cellVals = (value for cell, value in cells) + for value in itertools.chain(allBindings.values(), cellVals): if needsSampling(value): deps.add(value) if needsLazyEvaluation(value): @@ -93,7 +105,7 @@ def compile(self, namespace, scenario, syntax=None): # If this requirement contains the CanSee specifier, we will need to sample all objects # to meet the dependencies. - if "CanSee" in bindings: + if "CanSee" in globalBindings: deps.update(scenario.objects) if ego is not None: @@ -102,13 +114,18 @@ def compile(self, namespace, scenario, syntax=None): # Construct closure def closure(values, monitor=None): - # rebind any names referring to sampled objects + # rebind any names referring to sampled objects (for require statements, + # rebind all names, since we want their values at the time the requirement + # was created) # note: need to extract namespace here rather than close over value # from above because of https://github.com/uqfoundation/dill/issues/532 namespace = condition.atomics()[0].closure.__globals__ - for name, value in bindings.items(): - if value in values: + for name, value in globalBindings.items(): + if ty == RequirementType.require or value in values: namespace[name] = values[value] + for cell, value in cells: + cell.cell_contents = values[value] + # rebind ego object, which can be referred to implicitly boundEgo = None if ego is None else values[ego] # evaluate requirement condition, reporting errors on the correct line @@ -132,24 +149,34 @@ def closure(values, monitor=None): return CompiledRequirement(self, closure, deps, condition) -def getAllGlobals(req, restrictTo=None): +def getNameBindings(req, restrictTo=None): """Find all names the given lambda depends on, along with their current bindings.""" namespace = req.__globals__ if restrictTo is not None and restrictTo is not namespace: - return {} + return {}, {}, () externals = inspect.getclosurevars(req) - assert not externals.nonlocals # TODO handle these - globs = dict(externals.builtins) - for name, value in externals.globals.items(): - globs[name] = value - if inspect.isfunction(value): - subglobs = getAllGlobals(value, restrictTo=namespace) - for name, value in subglobs.items(): - if name in globs: - assert value is globs[name] - else: - globs[name] = value - return globs + globalBindings = externals.builtins + + closures = set() + if externals.nonlocals: + closures.add(req) + + def handleFunctions(bindings): + for value in bindings.values(): + if inspect.isfunction(value): + if value.__closure__ is not None: + closures.add(value) + subglobs, _, _ = getNameBindings(value, restrictTo=namespace) + for name, value in subglobs.items(): + if name in globalBindings: + assert value is globalBindings[name] + else: + globalBindings[name] = value + + globalBindings.update(externals.globals) + handleFunctions(externals.globals) + handleFunctions(externals.nonlocals) + return globalBindings, externals.nonlocals, closures class BoundRequirement: diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index 5328c0c6d..70f1b9f24 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1359,11 +1359,12 @@ def createRequirementLike( """Create a call to a function that implements requirement-like features, such as `record` and `terminate when`. Args: - functionName (str): Name of the requirement-like function to call. Its signature must be `(reqId: int, body: () -> bool, lineno: int, name: str | None)` + functionName (str): Name of the requirement-like function to call. Its signature + must be `(reqId: int, body: () -> bool, lineno: int, name: str | None)` body (ast.AST): AST node to evaluate for checking the condition lineno (int): Line number in the source code - name (Optional[str], optional): Optional name for requirements. Defaults to None. - prob (Optional[float], optional): Optional probability for requirements. Defaults to None. + name (Optional[str]): Optional name for requirements. Defaults to None. + prob (Optional[float]): Optional probability for requirements. Defaults to None. """ propTransformer = PropositionTransformer(self.filename) newBody, self.nextSyntaxId = propTransformer.transform(body, self.nextSyntaxId) @@ -1374,7 +1375,7 @@ def createRequirementLike( value=ast.Call( func=ast.Name(functionName, loadCtx), args=[ - ast.Constant(requirementId), # requirement IDre + ast.Constant(requirementId), # requirement ID newBody, # body ast.Constant(lineno), # line number ast.Constant(name), # requirement name diff --git a/tests/syntax/test_requirements.py b/tests/syntax/test_requirements.py index 0c7699fe0..730ac60c4 100644 --- a/tests/syntax/test_requirements.py +++ b/tests/syntax/test_requirements.py @@ -19,6 +19,87 @@ def test_requirement(): assert all(0 <= x <= 10 for x in xs) +def test_requirement_in_loop(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + for i in range(2): + require ego.position[i] >= 0 + """ + ) + poss = [sampleEgo(scenario, maxIterations=150).position for i in range(60)] + assert all(0 <= pos.x <= 10 and 0 <= pos.y <= 10 for pos in poss) + + +def test_requirement_in_function(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + def f(i): + require ego.position[i] >= 0 + for i in range(2): + f(i) + """ + ) + poss = [sampleEgo(scenario, maxIterations=150).position for i in range(60)] + assert all(0 <= pos.x <= 10 and 0 <= pos.y <= 10 for pos in poss) + + +def test_requirement_in_function_helper(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + m = 0 + def f(): + assert m == 0 + return ego.y + m + def g(): + require ego.x < f() + g() + m = -100 + """ + ) + poss = [sampleEgo(scenario, maxIterations=60).position for i in range(60)] + assert all(pos.x < pos.y for pos in poss) + + +def test_requirement_in_function_random_local(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ 0 + def f(): + local = Range(0, 1) + require ego.x < local + f() + """ + ) + xs = [sampleEgo(scenario, maxIterations=60).position.x for i in range(60)] + assert all(-10 <= x <= 1 for x in xs) + + +def test_requirement_in_function_random_cell(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ 0 + def f(i): + def g(): + return i + return g + g = f(Range(0, 1)) # global function with a cell containing a random value + def h(): + local = Uniform(True, False) + def inner(): # local function likewise + return local + require (g() >= 0) and ((ego.x < -5) if inner() else (ego.x > 5)) + h() + """ + ) + xs = [sampleEgo(scenario, maxIterations=150).position.x for i in range(60)] + assert all(x < -5 or x > 5 for x in xs) + assert any(x < -5 for x in xs) + assert any(x > 5 for x in xs) + + def test_soft_requirement(): scenario = compileScenic( """