Module moog.physics.collisions

Collision forces.

The main class in this file is Collision. It simulates Newtonian collisions between two sprites. These collisions take into account the angles of the sprite edges/vertices at the point of contact to realistically simulate the collision. By default they respect Newtonian mechanics for rotation as well, taking into consideration the moments of inertia of the sprites. However, there is an option to ignore this and prohibit the collision from changing the angular velocity of the sprites.

WARNING: Sprites must be star-shaped with respect to their center of mass (i.e. the line fron the center of mass to any point in the sprite never exits the sprite) for guaranteed correct collisions. If they are not star-shaped, there can be rare events where two sprites collide at a corner and the collision is not simulated correctly.

Expand source code
"""Collision forces.

The main class in this file is Collision. It simulates Newtonian collisions
between two sprites. These collisions take into account the angles of the sprite
edges/vertices at the point of contact to realistically simulate the collision.
By default they respect Newtonian mechanics for rotation as well, taking into
consideration the moments of inertia of the sprites. However, there is an option
to ignore this and prohibit the collision from changing the angular velocity of
the sprites.

WARNING: Sprites must be star-shaped with respect to their center of mass (i.e.
the line fron the center of mass to any point in the sprite never exits the
sprite) for guaranteed correct collisions. If they are not star-shaped, there
can be rare events where two sprites collide at a corner and the collision is
not simulated correctly.
"""

# TODO(nwatters): Make collisions more robust when multiple collisions happen to
# a sprite in a single timestep. Consider using a backend like PyBullet or
# PyMunk.

# TODO(nwatters): Consider giving special treatment to circles for the collision
# point detection functions. Circular sprites are commonly used and have many
# vertices, which makes the _get_collision_vectors() function slow. Inferring
# collision point based on radius for circles might be worth making the code a
# little messier.

# TODO(nwatters): Handle the case where one sprite is dropped inside of another
# without having moved there by its physics. This can occur when the sprite is
# subject to some dynamics (e.g. via an action space or a portal) that directly
# change its position (instead of just changing its velocity). In this case the
# sprite should pop out of the overlap region. The current code does not handle
# this, but it is an important feature and would entail changing the
# _directed_collision_vectors() function.

from . import abstract_force
from matplotlib import path as mpl_path
from matplotlib import transforms as mpl_transforms
import numpy as np
from moog import sprite as sprite_lib

# Small float to ensure sprite position correction is conservative. Make sure
# this is not too small though, or ._make_disjoint() will be called
# unnecessarily often in Collision.step(). Something around 1e-2 seems to be
# fine.
_EPSILON = 1e-2


def _relative_motion_trajectory(path, path_sprite, anchor_sprite, delta_t):
    """Find trajectory of a path in the coordinate frame of an anchor sprite.

    Here path is rigidly tethered to path_sprite, i.e. it may consist of
    vertices of path_sprite. This function produces the trajectory of the path
    points in the inertial frame of anchor_sprite, i.e. the moving coordinate
    system in which anchor_sprite is static.

    Args:
        path: Instance of mpl_path.Path. path.vertices has shape [N, 2].
        path_sprite: Instance of ../sprite.Sprite.
        anchor_sprite: Instance of ../sprite.Sprite.
        delta_t: Scalar. Timestep over which to produce the trajectories.

    Returns:
        trajectory: Numpy array of shape [N, 2, 2]. Element i of trajectory
            is an array of shape [2, 2] in which the second element is the i'th
            path vertex and the first element is the previous position of that
            vertex in the relative coordinate transformation between
            anchor_sprite and path_sprite.
    """
    transform = (
        mpl_transforms.Affine2D().rotate_around(
            *path_sprite.position,
            theta=-1 * path_sprite.angle_vel * delta_t) +
        mpl_transforms.Affine2D().translate(
            *(-1 * path_sprite.velocity * delta_t)) +
        mpl_transforms.Affine2D().rotate_around(
            *anchor_sprite.position,
            theta=anchor_sprite.angle_vel * delta_t) +
        mpl_transforms.Affine2D().translate(
            *anchor_sprite.velocity * delta_t)
    )
    previous_path = transform.transform_path(path)
    trajectory = np.stack((previous_path.vertices, path.vertices), axis=1)

    return trajectory


def _directed_collision_vectors(sprite_0, sprite_1, delta_t):
    """Find single contact point and collision vectors.
    
    This function does the following:
        1. Finds all vertices of sprite_0 that lie inside of sprite_1.
        2. Finds the true continuous-time point of contact for each of these.
        3. Selects on that is most likely to be the true collisions point.
        4. Produces that collision point, the collision normal vector, and
            relative displacement of the sprites since the true collision time.

    Note that this is directed, i.e. only considers vertices of sprite_0 that
    lie in sprite_1. In reality a collision might occur by a vertex of sprite_1
    initially entering sprite_0. Hence we must call this function on
    *(sprite_1, sprite_0) as well. See _get_collision_vectors() for details.

    Args:
        sprite_0: Instance of ../sprite.Sprite.
        sprite_1: Instance of ../sprite.Sprite.
        delta_t: Scalar. Timestep of the physics.

    Returns:
        collision_point: None or numpy array of shape [2]. Position of the
            collision.
        collision_normal: None or numpy array of shape [2]. Zero-centered vector
            indicating the normal of the collision surface, i.e. the direction
            in which the collision force is applied. It is signed, so we let it
            point toward the sprite_1 side of the collision.
        since_collision: None or numpy array of shape [2]. Relative displacement
            of the two sprites since the collision. It is signed, so we let it
            point toward the sprite_0 side of the collision, i.e. representing
            how much sprite_0 has moved relative to sprite_0 since the collision
            event.
        perpendicular: A scaling of collision_normal, where the length is the
            margin of overlap of the sprites. This can be used to make the
            sprites disjoint.
    """
    # First find points of sprite_0 inside sprite_1
    vertices_0 = sprite_0.vertices
    contained_inds = np.argwhere(sprite_1.contains_points(vertices_0))[:, 0]
    if len(contained_inds) == 0:
        return None, None, None, None
    contained_vertices_0 = vertices_0[contained_inds]

    # Now find previous-to-current timestep trajectory of each contained vertex
    traj = _relative_motion_trajectory(
        mpl_path.Path(contained_vertices_0),
        sprite_0,
        sprite_1,
        delta_t,
    )

    # We will now find the crossing points between the previous-to-current
    # timestep relative trajectories of the vertices of sprite 0 and the edges 
    # of sprite 1. Instead of restricting ourselves to only the crossings of
    # those segments, we also extrapolate the relative trajectory of sprite 0
    # backwards in time to consider crossing points which may have occured
    # multiple timesteps ago. This is important to do because the relative
    # velocity of the sprites may have changed recently (e.g. due to some force
    # or action), so considering multi-timestep historical crossings avoids
    # instabilities. We can easily compute these historical crossings from the
    # segment crossing coefficients.
    vertices_1 = sprite_1.vertices
    vertices_1_next = np.concatenate((vertices_1[1:], vertices_1[:1]), axis=0)
    cross_a, cross_b = sprite_lib.segment_crossing_coefficients(
        start_0=traj[:, 0],
        end_0=traj[:, 1],
        start_1=vertices_1,
        end_1=vertices_1_next,
    )

    # To find current segment crossings we would further require cross_a > 0 and
    # cross_a < 1, but by allowing cross_a to be negative we let in historical
    # crossings and by allowing cross_a to be greater than 1 we let in future
    # crossings.
    crossings = (cross_b >= 0) * (cross_b <= 1)

    if not np.any(crossings):
        # This can happen because of _EPSILON_INTERPOLATION in sprite.py
        return None, None, None, None
    
    # We're going to use cross_a values to find the most eggregious crossing, so
    # set the non-crossing entries to -Inf. This is less cumbersome to implement
    # than setting to NaN and use np.nanargmin, because some slices in np.argmax
    # below might be all -Inf.
    cross_a[np.logical_not(crossings)] = -np.inf
    abs_cross_a = np.abs(1. - cross_a)

    # Get the crossing closest to the current vertex position for each vertex in
    # sprite_0. This will either be the most recent crossing or the most
    # imminent crossing.
    inds_crossings = np.argmin(abs_cross_a, axis=1)
    crossing_points = np.array([
        traj[i_0, 0] + cross_a[i_0, i_1] * (traj[i_0, 1] - traj[i_0, 0])
        for (i_0, i_1) in enumerate(inds_crossings)
    ])

    # Get segment from crossing point to trajectory end
    diffs_from_crossings = np.array([
        traj[i, 1] - crossing_points[i]
        for i in range(len(crossing_points))
    ])

    # Now we try to deduce which of the crossing points is the true collision
    # point. We do this by selecting the one for which the trajectory segment
    # endpoint is furthest from the crossing point, i.e. the point that is
    # deepest inside sprite_1.
    dists_vertices_crossings = np.linalg.norm(diffs_from_crossings, axis=1)
    dists_vertices_crossings[dists_vertices_crossings == np.inf] = 0
    crossing_points_ind = np.argmax(dists_vertices_crossings)
    sprite_1_ind = inds_crossings[crossing_points_ind]
    collision_point = crossing_points[crossing_points_ind]
    since_collision = diffs_from_crossings[crossing_points_ind]

    # If the true collision point is in the future, don't collide.
    if cross_a[crossing_points_ind, sprite_1_ind] > 1:
        # True collision point is in the future
        return collision_point, np.nan, since_collision, None

    # Finally, compute the collision vector normal to sprite_1
    delta_vertex_1 = (vertices_1_next[sprite_1_ind] -
                      vertices_1[sprite_1_ind])
    normal_vector = np.array([delta_vertex_1[1], -1 * delta_vertex_1[0]])
    collision_normal = normal_vector / np.linalg.norm(normal_vector)

    # Compute the perpendicular margin of overlap
    projection = delta_vertex_1 * (
        np.dot(since_collision, delta_vertex_1) /
        np.dot(delta_vertex_1, delta_vertex_1)
    )
    perpendicular = since_collision - projection

    return collision_point, collision_normal, since_collision, perpendicular


def _get_collision_vectors(sprite_0, sprite_1, delta_t):
    """Get contact point, collision normal, and displacement since collision.
    
    This function produces the collision point, collision normal, and relative
    displacement since the collision event for two sprites. It approximately
    infers these as  if the sprites had continuous time, i.e. the collision
    point is the true point of collision between the previous and current
    timestep.

    The collision point, normal, and displacement (plus things like the sprite
    centers of mass and moments of inertia) of are all that are needed from the
    sprite geometry for computing the physics. Once these are computed, we
    never have to look at the sprite vertices again.

    Args:
        sprite_0: Instance of ../sprite.Sprite.
        sprite_1: Instance of ../sprite.Sprite.
        delta_t: Scalar. Timestep of the physics.

    Returns:
        collision_point: None or numpy array of shape [2]. Position of the
            collision.
        collision_normal: None or numpy array of shape [2]. Zero-centered vector
            indicating the normal of the collision surface, i.e. the direction
            in which the collision force is applied. It is signed, so we let it
            point toward the sprite_0 side of the collision.
        since_collision: None or numpy array of shape [2]. Relative displacement
            of the two sprites since the collision. It is signed, so we let it
            point toward the sprite_1 side of the collision, i.e. representing
            how much sprite_0 has moved relative to sprite_1 since the collision
            event.
    """
    # First find the collision point on sprite_0 boundary
    collision_point_0, collision_normal_0, since_collision_0, perp_0 = (
        _directed_collision_vectors(sprite_1, sprite_0, delta_t))
    
    # Now find the collision point on sprite_1 boundary
    collision_point_1, collision_normal_1, since_collision_1, perp_1 = (
        _directed_collision_vectors(sprite_0, sprite_1, delta_t))

    # Make since_collision_i zero if no collision happened in the i direction
    if collision_point_0 is None:
        since_collision_0 = np.zeros(2)
    else:
        # Must negate collision_normal and since_collision for symmetry
        collision_normal_0 = -1. * collision_normal_0
        since_collision_0 = -1. * since_collision_0
    if since_collision_1 is None:
        since_collision_1 = np.zeros(2)

    # Pick which collision point to treat as the real collision point
    if np.linalg.norm(since_collision_0) > np.linalg.norm(since_collision_1):
        return collision_point_0, collision_normal_0, since_collision_0, perp_0
    else:
        return collision_point_1, collision_normal_1, since_collision_1, perp_1


def _collide_without_update_angle_vel(sprite_0,
                                      sprite_1,
                                      collision_point,
                                      collision_normal,
                                      elasticity=1.,
                                      symmetric=True):
    """Simulate collision in which sprite angular velocity cannot change.

    Since the sprite angular velocity is prohibited from changing, this
    collision is non-Newtonion, i.e. does not conserve angular momentum. However
    it does converse kinetic enery and momentum, hence models what the collision
    would be if the collision point were between the two sprites' centers of
    mass.

    Args:
        sprite_0: Instance of ../sprite.Sprite. If symmetric = False, this
            sprite is the one that is doing the colliding, i.e. this one's
            velocity changes.
        sprite_1: Instance of ../sprite.Sprite. If symmetric = False, this
            sprite is treated as having infinite mass (i.e. has fixed velocity).
        collision_point: Numpy array of shape [2]. Position of the collision.
        collision_normal: Numpy array of shape [2]. Unit normal of the
            collision, pointing towards sprote_0. Must have norm 1.
        elasticity: Float in [0, 1]. Elasticity 1 means the sprites bounce
            fully. Elasticity 0 means they stick together completely.
        symmetric: Bool. If True, collision is symmetric and both sprites'
            velocities are updated. If False, sprite_1 is treated as having
            infinite mass.
    """
    # Sanity check that collision_normal has norm 1
    collision_normal_norm = np.linalg.norm(collision_normal)
    if not np.isclose(collision_normal_norm, 1., atol=1e-4):
        raise ValueError(
            'collision_normal_norm is {}, which is not close to 1.'.format(
                collision_normal_norm))

    vel_0 = sprite_0.velocity
    vel_1 = sprite_1.velocity
    m_0 = sprite_0.mass
    m_1 = sprite_1.mass

    # Only consider the component of velocity parallel to normal
    vel_0_normal = np.dot(vel_0, collision_normal) * collision_normal
    vel_1_normal = np.dot(vel_1, collision_normal) * collision_normal

    # Find inertial reference frame, i.e. velocity of center of mass of the
    # entire system
    if symmetric:
        vel_cm_normal = (vel_0_normal * m_0 + vel_1_normal * m_1) / (m_0 + m_1)
    else:
        vel_cm_normal = vel_1_normal

    # Reflect each velocity across vel_cm, with elasticity
    delta_v_0 = (1 + elasticity) * (vel_cm_normal - vel_0_normal)
    delta_v_1 = (1 + elasticity) * (vel_cm_normal - vel_1_normal)

    # Update the sprite velocities
    sprite_0.velocity += delta_v_0
    sprite_1.velocity += delta_v_1


def _collide_with_update_angle_vel(sprite_0,
                                   sprite_1,
                                   collision_point,
                                   collision_normal,
                                   elasticity=1.,
                                   symmetric=True):
    """Simulate collision.

    This simulates Newtonian collisions between sprite_0 and sprite_1. It takes
    into account the geometry of the sprites, collision contact point, collision
    normal at the contact point, rotational inertia of the sprites, etc. Hence
    the collisions this simulates are very realistic.

    Deriving the equations governing the collision dynamics is non-trivial, so
    we sketch the approach here:
        
        First, for any collision between a rigid body with moment of unertia I,
        angular velocity w, mass M, and center-of-mass velocity v, we have
            I delta(w) = M delta(v) r sin(theta)
        where r is the distance between the collision point and the center of
        mass and theta is the angle between the collision normal and the center
        of mass. This equation is independent of the object with which the
        collision happens, and can be derived from conservation of momentum and
        conservation of angular momentum given some point mass being collided
        with.

        Now suppose we have two rigid bodies with subscripts 0 and 1. Then the
        above equation gives us
            I_0 delta(w_0) = M_0 delta(v_0) r_0 sin(theta_0)
            I_1 delta(w_1) = M_1 delta(v_1) r_1 sin(theta_1)
        Now conversation of global momentum gives us
            0 = sum_i[M_i delta(v_i)]
        And conservation of global kinetic energy gives
            0 = sum_i[M_i (v_i_after^2 - v_i_before^2) +
                      I_i (w_i_after^2 - w_i_before^2)]
              = sum_i[M_i delta(v_i) gamma(v_i) + I_i delta(w_i) gamma(w_i)]
        where gamma(x_i) = x_i_after + x_i_before.
        
        We must now solve these four equations for the four variable
            {v_i_after, w_i_after}_{i = 0, 1}
        Substituting gamma(x_i) = delta(x_i) + 2 x_i_before into the kinetic
        energy equation and eliminating all appearances of delta(v_1) and
        delta(w_1) using the previous equations, we end up with the solution in
        the code.

    Args:
        sprite_0: Instance of ../sprite.Sprite. If symmetric = False, this
            sprite is the one that is doing the colliding, i.e. this one's
            velocity changes.
        sprite_1: Instance of ../sprite.Sprite. If symmetric = False, this
            sprite is treated as having infinite mass and infinite moment of
            inertia (i.e. has fixed velocity and angular velocity).
        collision_point: Numpy array of shape [2]. Position of the collision.
        collision_normal: Numpy array of shape [2]. Unit normal of the
            collision, pointing towards sprote_0. Must have norm 1.
        elasticity: Float in [0, 1]. Elasticity 1 means the sprites bounce
            fully. Elasticity 0 means they stick together completely.
        symmetric: Bool. If True, collision is symmetric and both sprites'
            velocities are updated. If False, sprite_1 is treated as having
            infinite mass and infinite moment of inertia.
    """

    # Alias some scalars that we'll need
    m_0 = sprite_0.mass
    m_1 = sprite_1.mass
    w_0 = sprite_0.angle_vel
    w_1 = sprite_1.angle_vel
    i_0 = sprite_0.moment_of_inertia
    i_1 = sprite_1.moment_of_inertia

    # Extract only magnitude of velocity component parallel to normal
    v_0 = np.dot(sprite_0.velocity, collision_normal)
    v_1 = np.dot(sprite_1.velocity, collision_normal)

    # Extract sin(theta), where theta is angle between normal and contact point
    c_point_0 = collision_point - sprite_0.position
    c_point_1 = collision_point - sprite_1.position
    r_0 = np.linalg.norm(c_point_0)
    r_1 = np.linalg.norm(c_point_1)
    sin_theta_0 = np.cross(c_point_0, collision_normal) / r_0
    sin_theta_1 = np.cross(c_point_1, collision_normal) / r_1

    # Apply the collision equations. See docstring for a derivation sketch.
    s_0 = r_0 * sin_theta_0
    s_1 = r_1 * sin_theta_1

    a = m_0 + m_1 + m_0 * m_1 * ((s_0 * s_0 / i_0) + (s_1 * s_1 / i_1))
    b = (1 + elasticity) * (v_0 - v_1 + w_0 * s_0 - w_1 * s_1)
    if symmetric:
        delta_v_0 = -1 * m_1 * b / a
        delta_v_1 = m_0 * b / a
    else:
        delta_v_0 = -1 * m_1 * b / (a - m_0)
        delta_v_1 = 0.
    delta_w_0 = m_0 * delta_v_0 * s_0 / i_0
    delta_w_1 = m_1 * delta_v_1 * s_1 / i_1

    # Update sprite velocities and angular velocities
    sprite_0.velocity += delta_v_0 * collision_normal
    sprite_1.velocity += delta_v_1 * collision_normal
    sprite_0.angle_vel += delta_w_0
    sprite_1.angle_vel += delta_w_1


class Collision(abstract_force.AbstractForce):
    """Collision simulator.
    
    This class simulates Newtonian collisions between two rigid bodies, namely
    sprites in the environment. The physics assumes that the sprites have
    uniform density, as this is important for calculating their moments of
    inertia (see ../sprite for details).
    """

    def __init__(self, elasticity=1., symmetric=False, update_angle_vel=True,
                 max_recursion_depth=0):
        """Constructor.
        
        Args:
            elasticity: Float in [0, 1]. Elasticity 1 means the sprites bounce
                fully. Elasticity 0 means they stick together completely.
            symmetric: Bool. If True, collision is symmetric and both sprites
                are updated. If False, sprite_1 in .step() is treated as having
                infinite mass. This is useful for modeling bounces off of
                obstructors/walls that are fixed in the environment.
            update_angle_vel: Bool. If True, fully simulate the rotational
                mechanics and update the sprites angular velocity. If False,
                sprite angular velocities are not updated and the collision is
                treated as if the contact point was between the two sprites'
                centers of mass. Simulating the full rotational mechanics is
                slightly slower.
            max_recursion_depth: Int. Number of times self.step() may recurse.
                Usually, 0 (no recursion) is fine. However, larger values make
                the collisions slightly more accurate (at the expense of runtime
                efficiency) if multiple collisions are happening within a single
                step, e.g. colliding into a corner.
        """
        self._elasticity = elasticity
        self._symmetric = symmetric
        self._update_angle_vel = update_angle_vel
        self._max_recursion_depth = max_recursion_depth

    def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0):
        """Step the physics.
        
        Args:
            sprite_0: Instance of ../sprite.Sprite. If self._symmetric = False,
                this sprite is the one that is doing the colliding, i.e. this
                one's velocity changes.
            sprite_1: Instance of ../sprite.Sprite. If self._symmetric = False,
                this sprite is treated as having infinite mass.
            updates_per_env_step: Int. Number of times this force step is called
                for each step of the physics in the environment. This is used
                here for inferring the exact contact point in the collision.
            recursion_depth: Int. Number of times this function has recursed.
                Used internally only to catch and avoid max recursion depth
                errors.
        """
        if recursion_depth > self._max_recursion_depth:
            return

        if sprite_0 == sprite_1:
            return

        if not sprite_0.overlaps_sprite(sprite_1):
            return

        delta_t = 1. / updates_per_env_step

        # First get collision point, normal, and relative sprite displacement
        # since the collision
        collision_point, collision_normal, _, perpendicular = (
            _get_collision_vectors(sprite_0, sprite_1, delta_t))
        
        if collision_point is None:
            # Although the sprites do overlap, neither sprite contains any of
            # the other's vertices. This can happen when the sprites collide
            # exactly at two corners. This is annoying to handle because we must
            # infer which corner hit which face in between timesteps, but must
            # be done to ensure stability. See self._make_disjoint() for
            # details.
            self._make_disjoint(sprite_0, sprite_1)
        else:
            # Whether the collision point is in the future, in which case we
            # leave the sprites alone.
            future_collision_point = (
                np.isscalar(collision_normal) and np.isnan(collision_normal))

            if not future_collision_point:
                # There was a collision in the past, so we must simulate it
                # First, displace the sprites so they are no longer intersecting
                # Note that instead of perpendicular displacement we could use
                # since_collision. However, sometimes since_collision can have a
                # very acute angle and large magnitude due to angular velocity
                # or multiple collisions, so in prectice displacing by the
                # perpendicular is more stable.
                if self._symmetric:
                    sprite_0.position = (
                        sprite_0.position - (0.5 + _EPSILON) * perpendicular)
                    sprite_1.position = (
                        sprite_1.position + (0.5 + _EPSILON) * perpendicular)
                else:
                    sprite_0.position = (
                        sprite_0.position - (1. + _EPSILON) * perpendicular)

                # Second, change sprite velocities and angular velocities per
                # Newtonian physics
                if self._update_angle_vel:
                    _collide_with_update_angle_vel(
                        sprite_0,
                        sprite_1,
                        collision_point,
                        collision_normal,
                        elasticity=self._elasticity,
                        symmetric=self._symmetric,
                    )
                else:
                    _collide_without_update_angle_vel(
                        sprite_0,
                        sprite_1,
                        collision_point,
                        collision_normal,
                        elasticity=self._elasticity,
                        symmetric=self._symmetric,
                    )
            else:
                return
            
        # After perturbing the sprite positions and velocities, we recursively
        # step again in case that perturbation has now created another
        # collision.
        self.step(sprite_0, sprite_1, updates_per_env_step,
                  recursion_depth=recursion_depth + 1)

    def _make_disjoint(self, sprite_0, sprite_1):
        """Perturb the positions of sprite_0 and sprite_1 to make them disjoint.

        In theory, with infinitesimal delta_t, the collision detection would
        work without this correction since every collision occurs by a vertex of
        one sprite entering another sprite. However, with a non-zero delta_t, if
        a collision occurs almost exactly at two sharp corners of sprites, the
        vertex entrypoint could be missed due to the time discretization. So
        this correction will catch that case and perturb the sprites so they no
        longer intersect.

        Furthermore, sometimes if collisions with multiple sprites happen within
        one physics timestep, the collision misses one of them, so this
        perturbation comes in handy then to essentially push one of the
        collisions off until the next timestep.

        Finally, sometimes a sprite position may be macigally changed (e.g. by a
        discrete action space) to make it enter another sprite without moving
        there by its physics, so this correction is useful to separate the two
        overlapping sprites in that case as well.

        The way this function works is a bit ugly and not easy to describe. If
        you want to understand it, please draw out some examples on paper and
        see what the code does with them.

        Args:
            sprite_0: Instance of ../sprite.Sprite.
            sprite_1: Instance of ../sprite.Sprite.

        Returns:
            Boolean indicating whether the sprites were overlapping. This is
            useful for the calling code to know whether the sprie positions have
            been perturbed.
        """
        crossing_points, inds_crossings = sprite_lib.sprite_edge_crossings(
            sprite_0, sprite_1)

        if len(inds_crossings) <= 1:
            return True

        # correction_i is how much sprite_i must move (independent of sprite_j)
        # for the two to be disjoint.
        correction_0 = _position_correction(
            crossing_points,
            sprite_0,
            inds_crossings[:, 0],
            sprite_1,
            inds_crossings[:, 1])
        correction_1 = _position_correction(
            crossing_points,
            sprite_1,
            inds_crossings[:, 1],
            sprite_0,
            inds_crossings[:, 0])

        if np.linalg.norm(correction_0) > np.linalg.norm(correction_1):
            correction = -1 * (1 + _EPSILON) * correction_0
        else:
            correction = (1 + _EPSILON) * correction_0
        
        if not all(np.isfinite(correction)):  # no correction needed
            correction = np.zeros(2)
        
        if self._symmetric:
            sprite_0.position = sprite_0.position + 0.5 * correction
            sprite_1.position = sprite_1.position - 0.5 * correction
        else:
            sprite_0.position = sprite_0.position + correction
        
        return False


def _position_correction(crossing_points,
                         sprite_0,
                         crossing_inds_0,
                         sprite_1,
                         crossing_inds_1):
    """Finds a correction for the position to make the sprites disjoint.
    
    This finds how much the sprite_0 must move to alleviate the crossing points
    closest to its center of mass.

    Args:
        crossing_points: Numpy float array of size [K, 2].
        sprite_0: Instance of ../sprite.Sprite.
        crossing_inds_0: Numpy int array of size [K, 2].
        sprite_1: Instance of ../sprite.Sprite.
        crossing_inds_1: Numpy int array of size [K, 2].
    """
    vertices = sprite_0.vertices
    other_vertices = sprite_1.vertices

    # Sort crossing points by their proximity to the sprite_0's center of mass.
    dists_from_cm = np.linalg.norm(crossing_points - sprite_0.position, axis=1)
    sorted_inds = np.argsort(dists_from_cm)

    if sorted_inds[0] == sorted_inds[1]:
        # sprite_0 has no offending vertices, so sees no correction itself
        return np.array([np.inf, np.inf])

    # Get the two closest points. Ultimately we will find a perturbation
    # perpendicular to the segment between these two points. Specifically, we
    # will find the sprite_0 vertex closest to the other sprite_0's center of
    # mass along the direction perpendicular to the segment between these two
    # points. That vertex is the most eggregious offender and will tell use how
    # much we have to perturb.
    pt_0 = crossing_points[sorted_inds[0]]
    pt_1 = crossing_points[sorted_inds[1]]

    def _get_norm_v(point_0, point_1):
        # Get normalized vector perpendicular to [point_0, point_1] and pointing
        # towards sprite_1.position
        normalized_bdry = point_1 - point_0
        normalized_bdry /= np.linalg.norm(normalized_bdry)
        norm_v = normalized_bdry * -1 * np.sign(
            np.dot(sprite_1.position - point_0, normalized_bdry))
        return norm_v

    # Get the normal vector from the [pt_0, pt_1] segment in the direction of
    # cm_other.
    if crossing_inds_1[sorted_inds[0]] == crossing_inds_1[sorted_inds[0]]:
        # In this case using the edge of sprite_1 is more accurate
        pt_0_ind = crossing_inds_1[sorted_inds[0]]
        pt_1_ind = (pt_0_ind - 1) % len(other_vertices)
        norm_v = _get_norm_v(other_vertices[pt_1_ind], other_vertices[pt_0_ind])
    else:
        norm_v = _get_norm_v(pt_0, pt_1)

    # We're now going to traverse through some of sprite_0's vertices looking
    # for the one furthest in the norm_v direction. However, we don't want to
    # consider all of sprite_0's vertices (that could give wrong results for
    # some star-shaped sprites). We only want to consider those that lie between
    # the crossing points of interest. So we do this by finding which endpoint
    # of the first crossing point's sprite_0 edge lies in the norm_v direction
    # and traverse from there until we are in the negative norm_v direction.
    ind_forward = crossing_inds_0[sorted_inds[0]]
    ind_backward = (ind_forward - 1) % len(vertices)
    if np.dot(vertices[ind_forward] - pt_0, norm_v) > 0:
        # The forward endpoint of this sprite_0 edge is in the norm_v direction
        parity = 1
        current_ind = ind_forward
    elif np.dot(vertices[ind_backward] - pt_0, norm_v) > 0:
        # The backward endpoint of this sprite_0 edge is in the norm_v direction
        parity = -1
        current_ind = ind_backward
    else:
        # Neither vertex is in the norm_v direction, in which case we aren't
        # actually overlapping the other sprite_0 (probably just barely touching
        # it, perhaps caused by _EPSILON)
        return np.array([np.inf, np.inf])

    # Now iterate through vertices looking for the most eggregious offender
    # until we're no longer in the norm_v direction.
    worst_penalty = 0
    while np.dot(vertices[current_ind] - pt_0, norm_v) > 0:
        penalty = np.dot(vertices[current_ind] - pt_0, norm_v)
        if penalty > worst_penalty:
            worst_penalty = penalty
        current_ind = (current_ind + parity) % len(vertices)
    
    correction = worst_penalty * norm_v

    return correction

Classes

class Collision (elasticity=1.0, symmetric=False, update_angle_vel=True, max_recursion_depth=0)

Collision simulator.

This class simulates Newtonian collisions between two rigid bodies, namely sprites in the environment. The physics assumes that the sprites have uniform density, as this is important for calculating their moments of inertia (see ../sprite for details).

Constructor.

Args

elasticity
Float in [0, 1]. Elasticity 1 means the sprites bounce fully. Elasticity 0 means they stick together completely.
symmetric
Bool. If True, collision is symmetric and both sprites are updated. If False, sprite_1 in .step() is treated as having infinite mass. This is useful for modeling bounces off of obstructors/walls that are fixed in the environment.
update_angle_vel
Bool. If True, fully simulate the rotational mechanics and update the sprites angular velocity. If False, sprite angular velocities are not updated and the collision is treated as if the contact point was between the two sprites' centers of mass. Simulating the full rotational mechanics is slightly slower.
max_recursion_depth
Int. Number of times self.step() may recurse. Usually, 0 (no recursion) is fine. However, larger values make the collisions slightly more accurate (at the expense of runtime efficiency) if multiple collisions are happening within a single step, e.g. colliding into a corner.
Expand source code
class Collision(abstract_force.AbstractForce):
    """Collision simulator.
    
    This class simulates Newtonian collisions between two rigid bodies, namely
    sprites in the environment. The physics assumes that the sprites have
    uniform density, as this is important for calculating their moments of
    inertia (see ../sprite for details).
    """

    def __init__(self, elasticity=1., symmetric=False, update_angle_vel=True,
                 max_recursion_depth=0):
        """Constructor.
        
        Args:
            elasticity: Float in [0, 1]. Elasticity 1 means the sprites bounce
                fully. Elasticity 0 means they stick together completely.
            symmetric: Bool. If True, collision is symmetric and both sprites
                are updated. If False, sprite_1 in .step() is treated as having
                infinite mass. This is useful for modeling bounces off of
                obstructors/walls that are fixed in the environment.
            update_angle_vel: Bool. If True, fully simulate the rotational
                mechanics and update the sprites angular velocity. If False,
                sprite angular velocities are not updated and the collision is
                treated as if the contact point was between the two sprites'
                centers of mass. Simulating the full rotational mechanics is
                slightly slower.
            max_recursion_depth: Int. Number of times self.step() may recurse.
                Usually, 0 (no recursion) is fine. However, larger values make
                the collisions slightly more accurate (at the expense of runtime
                efficiency) if multiple collisions are happening within a single
                step, e.g. colliding into a corner.
        """
        self._elasticity = elasticity
        self._symmetric = symmetric
        self._update_angle_vel = update_angle_vel
        self._max_recursion_depth = max_recursion_depth

    def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0):
        """Step the physics.
        
        Args:
            sprite_0: Instance of ../sprite.Sprite. If self._symmetric = False,
                this sprite is the one that is doing the colliding, i.e. this
                one's velocity changes.
            sprite_1: Instance of ../sprite.Sprite. If self._symmetric = False,
                this sprite is treated as having infinite mass.
            updates_per_env_step: Int. Number of times this force step is called
                for each step of the physics in the environment. This is used
                here for inferring the exact contact point in the collision.
            recursion_depth: Int. Number of times this function has recursed.
                Used internally only to catch and avoid max recursion depth
                errors.
        """
        if recursion_depth > self._max_recursion_depth:
            return

        if sprite_0 == sprite_1:
            return

        if not sprite_0.overlaps_sprite(sprite_1):
            return

        delta_t = 1. / updates_per_env_step

        # First get collision point, normal, and relative sprite displacement
        # since the collision
        collision_point, collision_normal, _, perpendicular = (
            _get_collision_vectors(sprite_0, sprite_1, delta_t))
        
        if collision_point is None:
            # Although the sprites do overlap, neither sprite contains any of
            # the other's vertices. This can happen when the sprites collide
            # exactly at two corners. This is annoying to handle because we must
            # infer which corner hit which face in between timesteps, but must
            # be done to ensure stability. See self._make_disjoint() for
            # details.
            self._make_disjoint(sprite_0, sprite_1)
        else:
            # Whether the collision point is in the future, in which case we
            # leave the sprites alone.
            future_collision_point = (
                np.isscalar(collision_normal) and np.isnan(collision_normal))

            if not future_collision_point:
                # There was a collision in the past, so we must simulate it
                # First, displace the sprites so they are no longer intersecting
                # Note that instead of perpendicular displacement we could use
                # since_collision. However, sometimes since_collision can have a
                # very acute angle and large magnitude due to angular velocity
                # or multiple collisions, so in prectice displacing by the
                # perpendicular is more stable.
                if self._symmetric:
                    sprite_0.position = (
                        sprite_0.position - (0.5 + _EPSILON) * perpendicular)
                    sprite_1.position = (
                        sprite_1.position + (0.5 + _EPSILON) * perpendicular)
                else:
                    sprite_0.position = (
                        sprite_0.position - (1. + _EPSILON) * perpendicular)

                # Second, change sprite velocities and angular velocities per
                # Newtonian physics
                if self._update_angle_vel:
                    _collide_with_update_angle_vel(
                        sprite_0,
                        sprite_1,
                        collision_point,
                        collision_normal,
                        elasticity=self._elasticity,
                        symmetric=self._symmetric,
                    )
                else:
                    _collide_without_update_angle_vel(
                        sprite_0,
                        sprite_1,
                        collision_point,
                        collision_normal,
                        elasticity=self._elasticity,
                        symmetric=self._symmetric,
                    )
            else:
                return
            
        # After perturbing the sprite positions and velocities, we recursively
        # step again in case that perturbation has now created another
        # collision.
        self.step(sprite_0, sprite_1, updates_per_env_step,
                  recursion_depth=recursion_depth + 1)

    def _make_disjoint(self, sprite_0, sprite_1):
        """Perturb the positions of sprite_0 and sprite_1 to make them disjoint.

        In theory, with infinitesimal delta_t, the collision detection would
        work without this correction since every collision occurs by a vertex of
        one sprite entering another sprite. However, with a non-zero delta_t, if
        a collision occurs almost exactly at two sharp corners of sprites, the
        vertex entrypoint could be missed due to the time discretization. So
        this correction will catch that case and perturb the sprites so they no
        longer intersect.

        Furthermore, sometimes if collisions with multiple sprites happen within
        one physics timestep, the collision misses one of them, so this
        perturbation comes in handy then to essentially push one of the
        collisions off until the next timestep.

        Finally, sometimes a sprite position may be macigally changed (e.g. by a
        discrete action space) to make it enter another sprite without moving
        there by its physics, so this correction is useful to separate the two
        overlapping sprites in that case as well.

        The way this function works is a bit ugly and not easy to describe. If
        you want to understand it, please draw out some examples on paper and
        see what the code does with them.

        Args:
            sprite_0: Instance of ../sprite.Sprite.
            sprite_1: Instance of ../sprite.Sprite.

        Returns:
            Boolean indicating whether the sprites were overlapping. This is
            useful for the calling code to know whether the sprie positions have
            been perturbed.
        """
        crossing_points, inds_crossings = sprite_lib.sprite_edge_crossings(
            sprite_0, sprite_1)

        if len(inds_crossings) <= 1:
            return True

        # correction_i is how much sprite_i must move (independent of sprite_j)
        # for the two to be disjoint.
        correction_0 = _position_correction(
            crossing_points,
            sprite_0,
            inds_crossings[:, 0],
            sprite_1,
            inds_crossings[:, 1])
        correction_1 = _position_correction(
            crossing_points,
            sprite_1,
            inds_crossings[:, 1],
            sprite_0,
            inds_crossings[:, 0])

        if np.linalg.norm(correction_0) > np.linalg.norm(correction_1):
            correction = -1 * (1 + _EPSILON) * correction_0
        else:
            correction = (1 + _EPSILON) * correction_0
        
        if not all(np.isfinite(correction)):  # no correction needed
            correction = np.zeros(2)
        
        if self._symmetric:
            sprite_0.position = sprite_0.position + 0.5 * correction
            sprite_1.position = sprite_1.position - 0.5 * correction
        else:
            sprite_0.position = sprite_0.position + correction
        
        return False

Ancestors

Methods

def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0)

Step the physics.

Args

sprite_0
Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is the one that is doing the colliding, i.e. this one's velocity changes.
sprite_1
Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is treated as having infinite mass.
updates_per_env_step
Int. Number of times this force step is called for each step of the physics in the environment. This is used here for inferring the exact contact point in the collision.
recursion_depth
Int. Number of times this function has recursed. Used internally only to catch and avoid max recursion depth errors.
Expand source code
def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0):
    """Step the physics.
    
    Args:
        sprite_0: Instance of ../sprite.Sprite. If self._symmetric = False,
            this sprite is the one that is doing the colliding, i.e. this
            one's velocity changes.
        sprite_1: Instance of ../sprite.Sprite. If self._symmetric = False,
            this sprite is treated as having infinite mass.
        updates_per_env_step: Int. Number of times this force step is called
            for each step of the physics in the environment. This is used
            here for inferring the exact contact point in the collision.
        recursion_depth: Int. Number of times this function has recursed.
            Used internally only to catch and avoid max recursion depth
            errors.
    """
    if recursion_depth > self._max_recursion_depth:
        return

    if sprite_0 == sprite_1:
        return

    if not sprite_0.overlaps_sprite(sprite_1):
        return

    delta_t = 1. / updates_per_env_step

    # First get collision point, normal, and relative sprite displacement
    # since the collision
    collision_point, collision_normal, _, perpendicular = (
        _get_collision_vectors(sprite_0, sprite_1, delta_t))
    
    if collision_point is None:
        # Although the sprites do overlap, neither sprite contains any of
        # the other's vertices. This can happen when the sprites collide
        # exactly at two corners. This is annoying to handle because we must
        # infer which corner hit which face in between timesteps, but must
        # be done to ensure stability. See self._make_disjoint() for
        # details.
        self._make_disjoint(sprite_0, sprite_1)
    else:
        # Whether the collision point is in the future, in which case we
        # leave the sprites alone.
        future_collision_point = (
            np.isscalar(collision_normal) and np.isnan(collision_normal))

        if not future_collision_point:
            # There was a collision in the past, so we must simulate it
            # First, displace the sprites so they are no longer intersecting
            # Note that instead of perpendicular displacement we could use
            # since_collision. However, sometimes since_collision can have a
            # very acute angle and large magnitude due to angular velocity
            # or multiple collisions, so in prectice displacing by the
            # perpendicular is more stable.
            if self._symmetric:
                sprite_0.position = (
                    sprite_0.position - (0.5 + _EPSILON) * perpendicular)
                sprite_1.position = (
                    sprite_1.position + (0.5 + _EPSILON) * perpendicular)
            else:
                sprite_0.position = (
                    sprite_0.position - (1. + _EPSILON) * perpendicular)

            # Second, change sprite velocities and angular velocities per
            # Newtonian physics
            if self._update_angle_vel:
                _collide_with_update_angle_vel(
                    sprite_0,
                    sprite_1,
                    collision_point,
                    collision_normal,
                    elasticity=self._elasticity,
                    symmetric=self._symmetric,
                )
            else:
                _collide_without_update_angle_vel(
                    sprite_0,
                    sprite_1,
                    collision_point,
                    collision_normal,
                    elasticity=self._elasticity,
                    symmetric=self._symmetric,
                )
        else:
            return
        
    # After perturbing the sprite positions and velocities, we recursively
    # step again in case that perturbation has now created another
    # collision.
    self.step(sprite_0, sprite_1, updates_per_env_step,
              recursion_depth=recursion_depth + 1)

Inherited members