From 773822463a939e6b272acf9ddc58ca7e96c96a9b Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 10 Jan 2024 17:58:50 -0800 Subject: [PATCH] PR fixes. --- src/scenic/core/object_types.py | 49 ++++++++++++--------------------- src/scenic/core/pruning.py | 2 ++ src/scenic/core/regions.py | 39 ++++++++------------------ src/scenic/core/scenarios.py | 1 - tests/core/test_regions.py | 3 +- tests/syntax/test_properties.py | 1 - tests/syntax/test_pruning.py | 14 +++++----- 7 files changed, 40 insertions(+), 69 deletions(-) diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index 83f46db4a..b007a07b2 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -1257,12 +1257,6 @@ def boundingBox(self): def inradius(self): """A lower bound on the inradius of this object""" - # Define a helper function that computes the actual inradius - def inradiusActual(width, length, height, shape): - return MeshVolumeRegion( - mesh=shape.mesh, dimensions=(width, length, height) - ).inradius - # Define a helper function that computes the support of the inradius, # given the sub supports. def inradiusSupport(width_s, length_s, height_s, shape_s): @@ -1310,14 +1304,15 @@ def inradiusSupport(width_s, length_s, height_s, shape_s): return distance_range - # Return either the inradius or a FunctionDistribution using the above helpers - args = toDistribution((self.width, self.length, self.height, self.shape)) - if not isLazy(args): - return inradiusActual(*args) - else: - return FunctionDistribution( - args=args, kwargs={}, func=inradiusActual, support=inradiusSupport - ) + # Define a helper function that computes the actual inradius + @distributionFunction(support=inradiusSupport) + def inradiusActual(width, length, height, shape): + return MeshVolumeRegion( + mesh=shape.mesh, dimensions=(width, length, height) + ).inradius + + # Return the inradius (possibly a distribution) with proper support information + return inradiusActual(self.width, self.length, self.height, self.shape) @cached_property def planarInradius(self): @@ -1328,12 +1323,6 @@ def planarInradius(self): roll are both 0. """ - # Define a helper function that computes the actual planarInradius - def planarInradiusActual(width, length, shape): - return MeshVolumeRegion( - mesh=shape.mesh, dimensions=(width, length, 1) - ).boundingPolygon.inradius - # Define a helper function that computes the support of the inradius, # given the sub supports. def planarInradiusSupport(width_s, length_s, shape_s): @@ -1377,17 +1366,15 @@ def planarInradiusSupport(width_s, length_s, shape_s): return distance_range - # Return either the inradius or a FunctionDistribution using the above helpers - args = toDistribution((self.width, self.length, self.shape)) - if not isLazy(args): - return planarInradiusActual(*args) - else: - return FunctionDistribution( - args=args, - kwargs={}, - func=planarInradiusActual, - support=planarInradiusSupport, - ) + # Define a helper function that computes the actual planarInradius + @distributionFunction(support=planarInradiusSupport) + def planarInradiusActual(width, length, shape): + return MeshVolumeRegion( + mesh=shape.mesh, dimensions=(width, length, 1) + ).boundingPolygon.inradius + + # Return the planar inradius (possibly a distribution) with proper support information + return planarInradiusActual(self.width, self.length, self.shape) @cached_property def surface(self): diff --git a/src/scenic/core/pruning.py b/src/scenic/core/pruning.py index d954fe0ac..da98288b0 100644 --- a/src/scenic/core/pruning.py +++ b/src/scenic/core/pruning.py @@ -146,6 +146,8 @@ def matchPolygonalField(heading, position): ### Pruning procedures + + def prune(scenario, verbosity=1): """Prune a `Scenario`, removing infeasible parts of the space. diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 162f39527..c4f3a29e6 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -1418,7 +1418,6 @@ def intersect(self, other, triedReversed=False): # Other region is a mesh volume. We can extract the mesh to perform boolean operations on it other_mesh = other.mesh - # If dimensions are prov # Compute intersection using Trimesh try: new_mesh = self.mesh.intersection(other_mesh, engine=self.engine) @@ -2066,14 +2065,7 @@ class VoxelRegion(Region): """Region represented by a voxel grid in 3D space. Args: - encoding: A numpy array encoding the voxel grid. - dimensions: An optional 3-tuple, with the values representing width, length, height - respectively. The voxel region will be scaled to have these dimensions. Default - value (1,1,1). - position: A position, which determines where the center of the region will be. Default - value is the origin, (0,0,0). - voxelGrid: Optionally, just pass in the final trimesh voxelGrid to be used. Passing this - parameter will result in encoding, dimensions, and position being ignored. + voxelGrid: The Trimesh voxelGrid to be used. orientation: An optional vector field describing the preferred orientation at every point in the region. name: An optional name to help with debugging. @@ -2085,27 +2077,21 @@ def __init__(self, voxelGrid, orientation=None, name=None, lazy=False): # Initialize superclass super().__init__(name, orientation=orientation) - # Work around Trimesh caching bug - self._voxelGrid = trimesh.voxel.VoxelGrid( - voxelGrid.encoding, transform=voxelGrid.transform.copy() - ) - # Check that the encoding isn't empty. In that case, raise an error. - if self._voxelGrid.encoding.is_empty: + if voxelGrid.encoding.is_empty: raise ValueError("Tried to create an empty VoxelRegion.") - # Transform voxel grid points and extract scale - self.voxel_points = self._voxelGrid.points - self.scale = self._voxelGrid.scale + # Store voxel grid and extract points and scale + self.voxelGrid = trimesh.voxel.VoxelGrid( + voxelGrid.encoding, transform=voxelGrid.transform.copy() + ) + self.voxel_points = self.voxelGrid.points + self.scale = self.voxelGrid.scale # Initialize KD-Tree for containment checking if not lazy if not lazy: self.kdTree - @cached_property - def voxelGrid(self): - return self._voxelGrid - @cached_property def kdTree(self): return scipy.spatial.KDTree(self.voxel_points) @@ -2114,16 +2100,14 @@ def containsPoint(self, point): point = toVector(point) # Find closest voxel point - _, index = self.kdTree.query([point]) + _, index = self.kdTree.query(point) closest_point = self.voxel_points[index] # Check voxel containment voxel_low = closest_point - self.scale / 2 voxel_high = closest_point + self.scale / 2 - return numpy.all(voxel_low <= point, axis=1) & numpy.all( - point <= voxel_high, axis=1 - ) + return numpy.all(voxel_low <= point) & numpy.all(point <= voxel_high) def containsObject(self, obj): raise NotImplementedError @@ -2142,10 +2126,11 @@ def uniformPointInner(self): # equal to scale, centered at the origin. base_pt = numpy.random.random_sample(3) - 0.5 scaled_pt = base_pt * self.scale + + # Pick a random voxel point and add it to base_pt. voxel_base = self.voxel_points[random.randrange(len(self.voxel_points))] offset_pt = voxel_base + scaled_pt - # Then pick a random voxel point and add the base point to that point. return Vector(*offset_pt) def dilation(self, iterations, structure=None): diff --git a/src/scenic/core/scenarios.py b/src/scenic/core/scenarios.py index 4c5a3935e..b900b0bc5 100644 --- a/src/scenic/core/scenarios.py +++ b/src/scenic/core/scenarios.py @@ -469,7 +469,6 @@ def _generateInner(self, maxIterations, verbosity, feedback): numpy.random.set_state(np_state) if rejection is not None: - # breakpoint() optionallyDebugRejection() # obtained a valid sample; assemble a scene from it diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index 647014930..af4154d69 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -574,8 +574,7 @@ def test_voxel_region(): def test_mesh_voxelization(getAssetPath): plane_region = MeshVolumeRegion.fromFile(getAssetPath("meshes/classic_plane.obj.bz2")) - voxel_grid = plane_region.mesh.voxelized(max(plane_region.mesh.extents) / 100).fill() - vr = VoxelRegion(voxelGrid=voxel_grid) + vr = plane_region.voxelized(max(plane_region.mesh.extents) / 100) for sampled_pt in trimesh.sample.volume_mesh(plane_region.mesh, 100): assert vr.containsPoint(sampled_pt) diff --git a/tests/syntax/test_properties.py b/tests/syntax/test_properties.py index 51585ce74..1b8f29888 100644 --- a/tests/syntax/test_properties.py +++ b/tests/syntax/test_properties.py @@ -186,7 +186,6 @@ def test_object_inradius(): """ ) ego = sampleEgo(scenario) - assert isinstance(scenario.objects[0].inradius, Distribution) assert supportInterval(scenario.objects[0].inradius) == (0, 0) assert ego.inradius == 0 diff --git a/tests/syntax/test_pruning.py b/tests/syntax/test_pruning.py index 90f521645..daf2deb5b 100644 --- a/tests/syntax/test_pruning.py +++ b/tests/syntax/test_pruning.py @@ -62,7 +62,7 @@ class TestObject: baseOffset: (0.1, 0, self.height/2) workspace = Workspace(PolygonalRegion([0@0, 2@0, 2@2, 0@2])) - ego = new Object in workspace, with height Range(0.1,0.5) + ego = new TestObject on workspace, with height Range(0.1,0.5) """ ) # Sampling should fail ~30.56% of the time, so @@ -166,9 +166,9 @@ def test_visibility_pruning(): The following scenarios are equivalent except for how they specify that foo must be visible from ego. The size of the workspace and the visibleDistance of ego are chosen such that without pruning the chance of sampling a valid - scene over 100 tries is 1-(1-Decimal(3.14)/Decimal(10000000000**2))**100 = ~1e-18. + scene over 100 tries is 1-(1-Decimal(3.14)/Decimal(1e10**2))**100 = ~1e-18. Assuming the approximately buffered volume of the viewRegion has a 50% chance of - ejecting (i.e. it is twice as large as the true buffered viewRegion, which testing + rejecting (i.e. it is twice as large as the true buffered viewRegion, which testing indicates in this case has about a 10% increase in volume for this case), the chance of not finding a sample in 100 iterations is 1e-31. @@ -176,13 +176,13 @@ def test_visibility_pruning(): in the viewRegion instead of at any point where the object intersects the view region. Because of this, we want to see at least one sample where the position is outside the viewRegion but the object intersects the viewRegion. The chance of this happening - is 1-((4/3)*math.pi*(1)**3)/((4/3)*math.pi*(1.1)**3) = ~25%, so by repeating the process - 30 times we have a 1e-19 chance of not gettin a single point in this zone. + per sample is 1 - (1 / 1.1)**3 = ~25%, so by repeating the process 30 times we have + a 1e-19 chance of not getting a single point in this zone. """ # requireVisible scenario = compileScenic( """ - workspace = Workspace(RectangularRegion(0@0, 0, 10000000000, 10000000000)) + workspace = Workspace(RectangularRegion(0@0, 0, 1e10, 1e10)) ego = new Object at (0,0,0), with visibleDistance 1 foo = new Object in workspace, with requireVisible True, with shape SpheroidShape(dimensions=(0.2,0.2,0.2)) @@ -196,7 +196,7 @@ def test_visibility_pruning(): # visible scenario = compileScenic( """ - workspace = Workspace(RectangularRegion(0@0, 0, 10000000000, 10000000000)) + workspace = Workspace(RectangularRegion(0@0, 0, 1e10, 1e10)) ego = new Object at (0,0,0), with visibleDistance 1 foo = new Object in workspace, visible, with shape SpheroidShape(dimensions=(0.2,0.2,0.2))