Skip to content

Commit

Permalink
Merge pull request #183 from BerkeleyLearnVerify/IntersectsOperator
Browse files Browse the repository at this point in the history
Add Intersects Operator
  • Loading branch information
dfremont authored Nov 9, 2023
2 parents 1b878bc + 363df2d commit 6c66160
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 12 deletions.
2 changes: 1 addition & 1 deletion docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Glossary
footprint
The infinite extrusion of a 2D `region` in the positive and negative Z directions.
Testing containment of an `object` in a 2D region automatically uses its footprint, so that the object is considered contained if and only if its projection into the plane of the region is contained in the region.
Footprints are represented internally by instances of the `PolygonalFootprintRegion` class.
Footprints are represented internally by instances of the `PolygonalFootprintRegion` class, and can be accessed using the ``footprint`` attribute.

global parameters
Parameters of a scene like weather or time of day which are not associated with any object.
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/operators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ See the :ref:`Visibility System <visibility>` reference for a discussion of the
----------------------------------
Whether a position or `Object` lies in the `Region`; for the latter, the object must be completely contained in the region.

.. _({Object} | {region}) intersects ({Object} | {region}):

(*Object* | *region*) intersects (*Object* | *region*)
------------------------------------------------------
Whether an `Object`/`Region` intersects another `Object`/`Region`, i.e. whether any portion of the occupied spaces intersect.

When working with 2D regions, it can be useful to check intersection with the :term:`footprint` of a region, e.g. when checking whether a car intersects a given lane. In this case, one would write :scenic:`car intersects lane.footprint` instead of :scenic:`car intersects lane`. For more details, see :term:`footprint`.


Orientation Operators
=====================
Expand Down
6 changes: 4 additions & 2 deletions docs/syntax_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,11 @@ In the following tables, operators are grouped by the type of value they return.
* - Boolean Operators
- Meaning
* - :sampref:`({Point} | {OrientedPoint}) can see ({vector} | {Object})`
- Whether or not a position or `Object` is visible from a `Point` or `OrientedPoint`.
- Whether or not a position or `Object` is visible from a `Point` or `OrientedPoint`
* - :sampref:`({vector} | {Object}) in {region}`
- Whether a position or `Object` lies in the region
- Whether a position or `Object` lies in the region
* - :sampref:`({Object} | {region}) intersects ({Object} | {region})`
- Whether an `Object`/`Region` intersects an `Object`/`Region`.


.. list-table::
Expand Down
32 changes: 27 additions & 5 deletions src/scenic/core/object_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,22 +1117,44 @@ def distanceTo(self, point):

@cached_method
def intersects(self, other):
"""Whether or not this object intersects another object"""
# For objects that are boxes and flat, we can take a fast route
if self._isPlanarBox and other._isPlanarBox:
"""Whether or not this object intersects another object or region"""
## Type Checking ##
if not isinstance(other, (Object, Region)):
raise TypeError(
f"Cannot compute intersection of Scenic Object with {type(other)}."
)

## Heuristic Fast Paths ##
# For two objects that are boxes and flat, we can take a fast route
if self._isPlanarBox and (isinstance(other, Object) and other._isPlanarBox):
if abs(self.position.z - other.position.z) > (self.height + other.height) / 2:
return False

self_poly = self._boundingPolygon
other_poly = other._boundingPolygon
return self_poly.intersects(other_poly)

if isLazy(self.occupiedSpace) or isLazy(other.occupiedSpace):
# For an object that is a box and flat with a polygonal region, we can
# also take a fast route.
if self._isPlanarBox and (
isinstance(other, PolygonalRegion)
and abs(self.position.z - other.z) <= self.height / 2
):
return self._boundingPolygon.intersects(other.polygons)

## Default Case
# Extract other's occupied space if it's an object
if isinstance(other, Object):
other_occupied_space = other.occupiedSpace
else:
other_occupied_space = other

if isLazy(self.occupiedSpace) or isLazy(other_occupied_space):
raise RandomControlFlowError(
"Cannot compute intersection between Objects with non-fixed values."
)

return self.occupiedSpace.intersects(other.occupiedSpace)
return self.occupiedSpace.intersects(other_occupied_space)

@cached_property
def left(self):
Expand Down
10 changes: 10 additions & 0 deletions src/scenic/syntax/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,3 +1250,13 @@ def __init__(self, left: ast.AST, right: ast.AST, *args: any, **kwargs: any) ->
self.left = left
self.right = right
self._fields = ["left", "right"]


class IntersectsOp(AST):
__match_args__ = ("left", "right")

def __init__(self, left: ast.AST, right: ast.AST, *args: any, **kwargs: any) -> None:
super().__init__(*args, **kwargs)
self.left = left
self.right = right
self._fields = ["left", "right"]
10 changes: 10 additions & 0 deletions src/scenic/syntax/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1746,3 +1746,13 @@ def visit_CanSeeOp(self, node: s.CanSeeOp):
],
keywords=[],
)

def visit_IntersectsOp(self, node: s.IntersectsOp):
return ast.Call(
func=ast.Name(id="Intersects", ctx=loadCtx),
args=[
self.visit(node.left),
self.visit(node.right),
],
keywords=[],
)
3 changes: 3 additions & 0 deletions src/scenic/syntax/scenic.gram
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,7 @@ bitwise_or:
| scenic_visible_from
| scenic_not_visible_from
| scenic_can_see
| scenic_intersects
| a=bitwise_or '|' b=bitwise_xor { ast.BinOp(left=a, op=ast.BitOr(), right=b, LOCATIONS) }
| bitwise_xor

Expand All @@ -1582,6 +1583,8 @@ scenic_not_visible_from: a=bitwise_or "not" "visible" 'from' b=bitwise_xor { s.N

scenic_can_see: a=bitwise_or "can" "see" b=bitwise_xor { s.CanSeeOp(left=a, right=b, LOCATIONS) }

scenic_intersects: a=bitwise_or "intersects" b=bitwise_xor { s.IntersectsOp(left=a, right=b, LOCATIONS) }

bitwise_xor:
| scenic_offset_along
| a=bitwise_xor '^' b=bitwise_and { ast.BinOp(left=a, op=ast.BitXor(), right=b, LOCATIONS) }
Expand Down
10 changes: 10 additions & 0 deletions src/scenic/syntax/veneer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"RelativeTo",
"OffsetAlong",
"CanSee",
"Intersects",
"Until",
"Implies",
"VisibleFromOp",
Expand Down Expand Up @@ -1367,6 +1368,15 @@ def canSeeHelper(X, Y, objects):
return canSeeHelper(X, Y, objects)


@distributionFunction
def Intersects(X, Y):
"""The :scenic:`{X} intersects {Y}` operator."""
if isA(X, Object):
return X.intersects(Y)
else:
return Y.intersects(X)


### Specifiers


Expand Down
8 changes: 8 additions & 0 deletions tests/syntax/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,14 @@ def test_can_see_op(self):
case _:
assert False

def test_intersects_op(self):
node, _ = compileScenicAST(IntersectsOp(Name("X"), Name("Y")))
match node:
case Call(Name("Intersects"), [Name("X"), Name("Y")]):
assert True
case _:
assert False


# Test cases inherited from the old translator for checking edge cases

Expand Down
107 changes: 103 additions & 4 deletions tests/syntax/test_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def test_point_in_region_2d():
ptA = new Point at [email protected]
ptB = new Point at [email protected]
ptC = new Point at (11, 4.5, 1)
param p = tuple([[email protected] in reg, 9@7 in reg, (11, 4.5, -1) in reg, ptA in reg, ptB in reg, ptC in reg])
param p = ([email protected] in reg, 9@7 in reg, (11, 4.5, -1) in reg, ptA in reg, ptB in reg, ptC in reg)
"""
)
assert p == (True, False, True, True, False, True)
Expand All @@ -469,7 +469,7 @@ def test_object_in_region_2d():
ego = new Object at [email protected], with width 0.25, with length 0.25
other_1 = new Object at [email protected], with width 2.5
other_2 = new Object at (11.5, 5.5, 2), with width 0.25, with length 0.25
param p = tuple([ego in reg, other_1 in reg, other_2 in reg])
param p = (ego in reg, other_1 in reg, other_2 in reg)
"""
)
assert p == (True, False, True)
Expand All @@ -482,7 +482,7 @@ def test_point_in_region_3d():
reg = BoxRegion()
ptA = new Point at (0.25,0.25,0.25)
ptB = new Point at (1,1,1)
param p = tuple([(0,0,0) in reg, (0.49,0.49,0.49) in reg, (0.5,0.5,0.5) in reg, (0.51,0.51,0.51) in reg, ptA in reg, ptB in reg])
param p = ((0,0,0) in reg, (0.49,0.49,0.49) in reg, (0.5,0.5,0.5) in reg, (0.51,0.51,0.51) in reg, ptA in reg, ptB in reg)
"""
)
assert p == (True, True, True, False, True, False)
Expand All @@ -497,12 +497,111 @@ def test_object_in_region_3d():
obj_2 = new Object at (0.49, 0.49, 0.49), with allowCollisions True
obj_3 = new Object at (0.75, 0.75, 0.75), with allowCollisions True
obj_4 = new Object at (3,3,3), with allowCollisions True
param p = tuple([obj_1 in reg, obj_2 in reg, obj_3 in reg, obj_4 in reg])
param p = (obj_1 in reg, obj_2 in reg, obj_3 in reg, obj_4 in reg)
"""
)
assert p == (True, True, False, False)


# Intersects
def test_intersects_obj_obj():
p = sampleParamPFrom(
"""
obj1 = new Object at (-1,0,0.1), with allowCollisions True
obj2 = new Object at (1,0,0), with allowCollisions True
obj3 = new Object with width 10, with length 10, with height 10, with allowCollisions True
param p = (obj1 intersects obj2, obj1 intersects obj3, obj2 intersects obj3)
"""
)
assert p == (False, True, True)

# Case where neither corners or centers intersect, but
# Objects still intersect.
p = sampleParamPFrom(
"""
obj1 = new Object at (10,0,0), with width 30, with allowCollisions True
obj2 = new Object at (0,10,0), with length 30, with allowCollisions True
param p = (obj1 intersects obj2,
obj1.position in obj2.occupiedSpace, obj2.position in obj1.occupiedSpace,
any((c in obj2.occupiedSpace) for c in obj1.corners),
any((c in obj1.occupiedSpace) for c in obj2.corners))
"""
)
assert p == (True, False, False, False, False)


def test_intersects_region_region():
p = sampleParamPFrom(
"""
reg1 = BoxRegion(position=(-1,0,0.1))
reg2 = BoxRegion(position=(1,0,0))
reg3 = BoxRegion(dimensions=(10,10,10))
param p = (reg1 intersects reg2, reg1 intersects reg3, reg2 intersects reg3)
"""
)
assert p == (False, True, True)


def test_intersects_obj_region():
p = sampleParamPFrom(
"""
reg1 = BoxRegion(position=(-1,0,0.1))
obj2 = new Object at (1,0,0), with allowCollisions True
obj3 = new Object with width 10, with length 10, with height 10, with allowCollisions True
param p = (reg1 intersects obj2, obj2 intersects reg1,
reg1 intersects obj3, obj3 intersects reg1)
"""
)
assert p == (False, False, True, True)


def test_intersects_2d():
p = sampleParamPFrom(
"""
obj1 = new Object at (0.2,0,0), with allowCollisions True
obj2 = new Object at (-0.2,0,0), with allowCollisions True
reg = RectangularRegion((0,0,0), 0, 10, 10)
param p = (obj1 intersects obj2, obj1 intersects reg)
"""
)
assert p == (True, True)


def test_intersects_non_0_z():
p = sampleParamPFrom(
"""
obj1 = new Object at (0.2,0,1), with allowCollisions True
obj2 = new Object at (-0.2,0,1), with allowCollisions True
reg = RectangularRegion((0,0,1), 0, 10, 10)
param p = (obj1 intersects obj2, obj1 intersects reg)
"""
)
assert p == (True, True)


def test_intersects_overlap():
p = sampleParamPFrom(
"""
obj = new Object at (0,0,0), with allowCollisions True
reg = RectangularRegion((0.5,0,0), 0, 1, 1)
param p = obj intersects reg
"""
)
assert p == True


def test_intersects_diff_z():
p = sampleParamPFrom(
"""
obj1 = new Object at (0,0,0.1), with allowCollisions True
obj2 = new Object at (0,0,10), with allowCollisions True
reg = RectangularRegion((0,0,0), 0, 10, 10)
param p = (obj1 intersects reg, obj2 intersects reg, obj1 intersects obj2)
"""
)
assert p == (True, False, False)


## Heading operators


Expand Down
9 changes: 9 additions & 0 deletions tests/syntax/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2845,3 +2845,12 @@ def test_can_see(self):
assert True
case _:
assert False

def test_intersects(self):
mod = parse_string_helper("x intersects y ")
stmt = mod.body[0]
match stmt:
case Expr(IntersectsOp(Name("x"), Name("y"))):
assert True
case _:
assert False

0 comments on commit 6c66160

Please sign in to comment.