keenon / nimblephysics

Nimble: Physics Engine for Biomechanics and Deep Learning
http://www.nimblephysics.org
Other
403 stars 44 forks source link

Incorret simulation results for object collision #143

Open scheacle opened 1 year ago

scheacle commented 1 year ago

Incorret simulation results for object collision

The forward simulation gives incorrect results for object collision. The total energy of the whole system doubles after collision, a phenomenon that violates the law of conservation of energy. The collision is fully elastic, and there is no external force applied to the system, so the total energy should not change after collision.

Detailed description

When an object is colliding with multiple objects simultaneously, the simulation results of Nimble physics engine doubles the total energy of the system. As a minimal example to reproduce the issue, consider a simple scene with two balls colliding with each other while also colliding with a wall:

Initially, the two balls are located at x1 = 0.78 and x2 = 0.99, and are moving with initial speed v1 = 2 and v2 = 0, respectively, along the positive direction of x-axis. Ball 1 is only 0.01 away from ball 2, and ball 2 is also 0.01 away from a wall located at xw = 1.1. Since the ball 1 is moving to the right while ball 2 is standing still, ball 1 would collide with ball 2. After collision, ball 1 would become still with v1 = 0 while ball 2 would move to the right with speed v2 = 2. Almost immediately following its collision with ball 1, ball 2 would collide with the wall and bounce back to collide with ball 1 again.

Ideally, the total energy of the two balls should not change after the all the collisions, as the collision is fully elastic. However, the nimble engine outputs that ball 1 and ball 2 would move with v1 = v2 = -2 in the end, meaning that the system's total energy is doubled. The simulation results are shown in the following figure (the time step of each frame is 0.01):

step0-3 step4-7

Summary of the simulation output by the Nimble physics engine in each time step:

  1. At time step 0, the two balls move with v1 = 2 and v2 = 0, respectively.
  2. At time step 1, the two balls collide with each other.
  3. At time step 2, the engine detects collision in the last time step and decides that the two balls now move with v1 = 0 and v2 = 2, respectively.
  4. At time step 3, ball 2 bumps into the ball. Also, ball 1 is colliding with ball 2 at the same time, as the distance between them is negative (-0.01 actually).
  5. At time step 4, the engine detects collision in the last time step and decides that the two balls now move with v1 = -2 and v2 = -2, respectively. This is where the incorrect result comes in, as the total energy of the system is doubled.
  6. From time step 5 to 7, the two balls keep moving to the left with speed 2.

Besides, when the initial velocity of ball 2 is set to v2 = 3, the energy-doubling phenomenon disappears. Although the correctness of the final output results in this case is not verified, at least the total energy of the system is not doubled. The detailed execution results can be found in the source code below.

How to reproduce

Prerequisites

Source code

import nimblephysics as nimble
import torch

world = nimble.simulation.World()
world.setGravity([0, 0, 0]) # Disable gravity
world.setTimeStep(1e-2)

radius = 0.1  # The radius of the two balls.
wall_pos = 1  # The position of the wall on the x-axis. A ball will collide with the wall
              # when its x-position value reaches `wall_pos`, i.e., 1 in this case.
thickness = 1 # The thickness of the wall (for visualization purpose).
width = 6     # The width of the wall on the yz axes (for visualization purpose).

# Create a wall object
wall = nimble.dynamics.Skeleton()
wall_joint, wall_body = wall.createWeldJointAndBodyNodePair()
wall_offset = nimble.math.Isometry3()

position = [wall_pos + radius + thickness / 2, 0, 0]
shape_size = [thickness, width, width]

wall_offset.set_translation(position)
wall_joint.setTransformFromParentBodyNode(wall_offset)

wall_shape = wall_body.createShapeNode(nimble.dynamics.BoxShape(shape_size))
wall_body.setRestitutionCoeff(1.0) # 1.0 means no energy loss during collision.
wall_shape.createCollisionAspect()
world.addSkeleton(wall)

def create_ball():
    ball = nimble.dynamics.Skeleton()
    sphere_joint, sphere_body = ball.createTranslationalJoint2DAndBodyNodePair()

    offset = nimble.math.Isometry3()
    offset.set_translation([0., 0., 0.])
    sphere_joint.setTransformFromParentBodyNode(offset)

    sphereShape = sphere_body.createShapeNode(nimble.dynamics.SphereShape(radius))
    sphereShape.createCollisionAspect()
    sphere_body.setFrictionCoeff(0.0)    # Disable friction
    sphere_body.setRestitutionCoeff(1.0) # RestitutionCoeff as 1.0 means no energy loss during collision.
    sphere_body.setMass(1)

    return ball

# Create two balls with the same parameters
b1 = create_ball()
b2 = create_ball()
world.addSkeleton(b1)
world.addSkeleton(b2)

def simulate(init_state, n_steps):
    state = init_state
    act = torch.zeros(4) # No action force applied. The balls would do uniform linear motion.
    trace = [init_state] # The states of the balls during the whole simulation process.
    for _ in range(n_steps): # Simulate for `n_steps` time steps.
        state = nimble.timestep(world, state, act)
        trace.append(state)
    return trace

ball1_init_pos, ball2_init_pos = 0.78, 0.99

#=========================
# When the initial velocity of the two balls is set to 2 and 0, respectively,
# the final velocities of them become -2 and -2 after collision, meaning that
# the total energy of the two balls is doubled.
# Such phenomenon obviously violates the energy-conservation law.
ball1_init_vel, ball2_init_vel = 2.0, 0.0
#========================
# When the initial velocity of ball 2 is tweaked to 3,
# the final velocity of ball 1 and 2 would be -5 and 0, respectively, after collision.
# Although such output is still questionable, at least the energy-conservation law is
# respected in this case.
# Uncomment the following line of code to see the effects:
#
# ball2_init_vel = 3.0

# Run simulation for 10 time steps and collect the states of the two balls during simulation.
trace = simulate(
    # The positions and velocities of the two balls are set to 0 on the y-axis so that the
    # balls would only move along the x-axis.
    torch.tensor([
        ball1_init_pos, 0., ball2_init_pos, 0., ball1_init_vel, 0., ball2_init_vel, 0.]),
    10)

for i, t in enumerate(trace):
    print(f"Time step {i}, "
          f"x1 = {t[0].item():.2f}, "
          f"x2 = {t[2].item():.2f}, "
          f"v1 = {t[4].item():.0f}, "
          f"v2 = {t[6].item():.0f}")

Output

Time step 0, x1 = 0.78, x2 = 0.99, v1 = 2, v2 = 0
Time step 1, x1 = 0.80, x2 = 0.99, v1 = 2, v2 = 0
Time step 2, x1 = 0.82, x2 = 0.99, v1 = 0, v2 = 2
Time step 3, x1 = 0.82, x2 = 1.01, v1 = 0, v2 = 2
Time step 4, x1 = 0.82, x2 = 1.03, v1 = -2, v2 = -2
Time step 5, x1 = 0.80, x2 = 1.01, v1 = -2, v2 = -2
Time step 6, x1 = 0.78, x2 = 0.99, v1 = -2, v2 = -2
Time step 7, x1 = 0.76, x2 = 0.97, v1 = -2, v2 = -2
Time step 8, x1 = 0.74, x2 = 0.95, v1 = -2, v2 = -2
Time step 9, x1 = 0.72, x2 = 0.93, v1 = -2, v2 = -2
Time step 10, x1 = 0.70, x2 = 0.91, v1 = -2, v2 = -2

Output after uncommenting line 81 and setting v2 = 3

Time step 0, x1 = 0.78, x2 = 0.99, v1 = 2, v2 = 3
Time step 1, x1 = 0.80, x2 = 1.02, v1 = 2, v2 = 3
Time step 2, x1 = 0.82, x2 = 1.05, v1 = 2, v2 = -3
Time step 3, x1 = 0.84, x2 = 1.02, v1 = 2, v2 = -3
Time step 4, x1 = 0.86, x2 = 0.99, v1 = -5, v2 = -0
Time step 5, x1 = 0.81, x2 = 0.99, v1 = -5, v2 = -0
Time step 6, x1 = 0.76, x2 = 0.99, v1 = -5, v2 = -0
Time step 7, x1 = 0.71, x2 = 0.99, v1 = -5, v2 = -0
Time step 8, x1 = 0.66, x2 = 0.99, v1 = -5, v2 = -0
Time step 9, x1 = 0.61, x2 = 0.99, v1 = -5, v2 = -0
Time step 10, x1 = 0.56, x2 = 0.99, v1 = -5, v2 = -0