Module tests.moog.physics.test_tether_physics

Tests for moog/physics/tether_physics.py.

To run this test, navigate to this directory and run

$ pytest test_tether_physics.py --capture=tee-sys

Note: The –capture=tee-sys routes print statements to stdout, which is useful for debugging.

Alternatively, to run this test and any others, navigate to any parent directory and simply run

$ pytest --capture=tee-sys

This will run all test_* files in children directories.

Expand source code
"""Tests for moog/physics/tether_physics.py.

To run this test, navigate to this directory and run
```bash
$ pytest test_tether_physics.py --capture=tee-sys
```

Note: The --capture=tee-sys routes print statements to stdout, which is useful
for debugging.

Alternatively, to run this test and any others, navigate to any parent directory
and simply run
```bash
$ pytest --capture=tee-sys
```
This will run all test_* files in children directories.
"""

import sys
sys.path.insert(0, '...')  # Allow imports from moog codebase

import collections
import matplotlib
from matplotlib import pyplot as plt
import numpy as np
import pytest

from moog import physics as physics_lib
from moog import shapes
from moog import sprite
from moog.physics import collisions
from moog.observers import pil_renderer

# Absolute error tolerance for testing scalars (positions, velocities, etc.)
_ATOL = 0.001


class MatplotlibUI():
    """Matplotlib UI.
    
    This can be used to visualize test conditions.
    """

    def __init__(self, image_size=128, anti_aliasing=3):
        """Constructor."""
        plt.ion()
        self._fig, self._ax = plt.subplots()
        self._ax.axis('off')
        self._imshow = self._ax.imshow(
            np.zeros((image_size, image_size, 3)), interpolation='none')
        self._renderer = pil_renderer.PILRenderer(
            image_size=(image_size, image_size), anti_aliasing=anti_aliasing)
        self._ax.set_xticks([])
        self._ax.set_yticks([])
        plt.show(block=False)
        plt.pause(0.1)

    def _render(self, state):
        """Renderer a state (ordereddict of iterables of sprites)."""
        self._imshow.set_data(self._renderer(state))
        plt.draw()
        plt.pause(0.1)

    def _simulate_video(self, physics, state, steps):
        """Simulate and display video."""
        physics.step(state)
        for i, s in enumerate(state['sprites']):
            print('')
            print(f'Sprite {i}')
            print(f'position: [{s.position[0]:.4f}, {s.position[1]:.4f}]')
            print(f'velocity: [{s.velocity[0]:.4f}, {s.velocity[1]:.4f}]')
            print(f'angle_vel: {s.angle_vel:.4f}')

        for _ in range(steps - 1):
            physics.step(state)
            self._render(state)
        
        # Print the true positions, velocities, and angular velocities of the
        # sprites, rounded to 4 decimal places.
        for i, s in enumerate(state['sprites']):
            print('')
            print(f'Sprite {i}')
            print(f'position: [{s.position[0]:.4f}, {s.position[1]:.4f}]')
            print(f'velocity: [{s.velocity[0]:.4f}, {s.velocity[1]:.4f}]')
            print(f'angle_vel: {s.angle_vel:.4f}')


def get_state():
    """Get initial state."""
    sprite_0 = sprite.Sprite(
        x=0.5, y=0.7, scale=0.1, shape='triangle',
        x_vel=0.04, y_vel=-0.02, c0=255, angle=2.)
    sprite_1 = sprite.Sprite(
        x=0.2, y=0.6, scale=0.1, shape='triangle',
        x_vel=0., y_vel=0., c1=255, angle=1.)
    sprite_2 = sprite.Sprite(
        x=0.6, y=0.3, scale=0.1, shape='triangle',
        x_vel=0., y_vel=0., c2=255)
    sprites = [sprite_0, sprite_1, sprite_2]

    walls = shapes.border_walls(
        visible_thickness=0.05, c0=128, c1=128, c2=128)

    state = collections.OrderedDict([('walls', walls), ('sprites', sprites)])
    
    return state


class TestTether():
    """Test Tether."""

    def _run_test(self, tether, step_1_state, final_state, plot=False):
        """Run test.
        
        Set plot = True to display videos of the test conditions.
        
        Args:
            tether: Tether force.
            step_1_state: Iterable of lists, one for each sprite. Each element
                is a list of [position, velocity, angle_vel] for the sprite
                after the first physics step.
            final_state: Same as step_1_state, except for the final step.
            plot: Bool. Whether to display video or run test.
        """
        state = get_state()

        collision = physics_lib.Collision(
            elasticity=0., symmetric=False, update_angle_vel=True)
        physics = physics_lib.Physics(
            (collision, 'sprites', 'walls'),
            corrective_physics=[tether],
            updates_per_env_step=10,
        )

        steps = 45
        if plot:
            MatplotlibUI()._simulate_video(physics, state, steps=steps)
        else:
            physics.step(state)

            for s, pred in zip(state['sprites'], step_1_state):
                assert np.allclose(s.position, pred[0], atol=_ATOL)
                assert np.allclose(s.velocity, pred[1], atol=_ATOL)
                assert np.allclose(s.angle_vel, pred[2], atol=_ATOL)

            for _ in range(steps - 1):
                physics.step(state)
            
            for s, pred in zip(state['sprites'], final_state):
                assert np.allclose(s.position, pred[0], atol=_ATOL)
                assert np.allclose(s.velocity, pred[1], atol=_ATOL)
                assert np.allclose(s.angle_vel, pred[2], atol=_ATOL)

    def testTetherNoAngleUpdate(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', update_angle_vel=False)

        step_1_state = [
            [[0.5133, 0.6933], [0.0133, -0.0067], 0.],
            [[0.2133, 0.5933], [0.0133, -0.0067], 0.],
            [[0.6133, 0.2933], [0.0133, -0.0067], 0.],
        ]

        final_state = [
            [[0.7710, 0.4900], [-0.0005, 0.], 0.],
            [[0.4710, 0.3900], [-0.0005, 0.], 0.],
            [[0.8671, 0.0939], [-0.0005, 0.], 0.],
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

    def testTetherAngleUpdate(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', update_angle_vel=True)

        step_1_state = [
            [[0.5206, 0.6900], [0.0205, -0.0103], -0.0447],
            [[0.2165, 0.6035], [0.0166, 0.0033], -0.0447],
            [[0.6027, 0.2860], [0.0025, -0.0140], -0.0447],
        ]

        final_state = [
            [[0.8341, 0.3401], [-0.0028, -0.0046], -0.0229],
            [[0.7139, 0.6271], [0.0037, -0.0018], -0.0229],
            [[0.4545, 0.2062], [-0.0059, 0.0041], -0.0229],
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

    def testTetherAnchor(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', anchor=np.array([0.2, 0.2]))

        step_1_state = [
            [[0.5190, 0.6881], [0.0188, -0.0122], -0.0385],
            [[0.2154, 0.5997], [0.0154, -0.0006], -0.0385],
            [[0.6036, 0.2845], [0.0033, -0.0155], -0.0385],
        ]

        final_state = [
            [[0.6927, 0.5118], [0., 0.], 0.],
            [[0.3798, 0.5573], [0., 0.], 0.],
            [[0.6025, 0.1233], [0., 0.], 0.]
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

Functions

def get_state()

Get initial state.

Expand source code
def get_state():
    """Get initial state."""
    sprite_0 = sprite.Sprite(
        x=0.5, y=0.7, scale=0.1, shape='triangle',
        x_vel=0.04, y_vel=-0.02, c0=255, angle=2.)
    sprite_1 = sprite.Sprite(
        x=0.2, y=0.6, scale=0.1, shape='triangle',
        x_vel=0., y_vel=0., c1=255, angle=1.)
    sprite_2 = sprite.Sprite(
        x=0.6, y=0.3, scale=0.1, shape='triangle',
        x_vel=0., y_vel=0., c2=255)
    sprites = [sprite_0, sprite_1, sprite_2]

    walls = shapes.border_walls(
        visible_thickness=0.05, c0=128, c1=128, c2=128)

    state = collections.OrderedDict([('walls', walls), ('sprites', sprites)])
    
    return state

Classes

class MatplotlibUI (image_size=128, anti_aliasing=3)

Matplotlib UI.

This can be used to visualize test conditions.

Constructor.

Expand source code
class MatplotlibUI():
    """Matplotlib UI.
    
    This can be used to visualize test conditions.
    """

    def __init__(self, image_size=128, anti_aliasing=3):
        """Constructor."""
        plt.ion()
        self._fig, self._ax = plt.subplots()
        self._ax.axis('off')
        self._imshow = self._ax.imshow(
            np.zeros((image_size, image_size, 3)), interpolation='none')
        self._renderer = pil_renderer.PILRenderer(
            image_size=(image_size, image_size), anti_aliasing=anti_aliasing)
        self._ax.set_xticks([])
        self._ax.set_yticks([])
        plt.show(block=False)
        plt.pause(0.1)

    def _render(self, state):
        """Renderer a state (ordereddict of iterables of sprites)."""
        self._imshow.set_data(self._renderer(state))
        plt.draw()
        plt.pause(0.1)

    def _simulate_video(self, physics, state, steps):
        """Simulate and display video."""
        physics.step(state)
        for i, s in enumerate(state['sprites']):
            print('')
            print(f'Sprite {i}')
            print(f'position: [{s.position[0]:.4f}, {s.position[1]:.4f}]')
            print(f'velocity: [{s.velocity[0]:.4f}, {s.velocity[1]:.4f}]')
            print(f'angle_vel: {s.angle_vel:.4f}')

        for _ in range(steps - 1):
            physics.step(state)
            self._render(state)
        
        # Print the true positions, velocities, and angular velocities of the
        # sprites, rounded to 4 decimal places.
        for i, s in enumerate(state['sprites']):
            print('')
            print(f'Sprite {i}')
            print(f'position: [{s.position[0]:.4f}, {s.position[1]:.4f}]')
            print(f'velocity: [{s.velocity[0]:.4f}, {s.velocity[1]:.4f}]')
            print(f'angle_vel: {s.angle_vel:.4f}')
class TestTether

Test Tether.

Expand source code
class TestTether():
    """Test Tether."""

    def _run_test(self, tether, step_1_state, final_state, plot=False):
        """Run test.
        
        Set plot = True to display videos of the test conditions.
        
        Args:
            tether: Tether force.
            step_1_state: Iterable of lists, one for each sprite. Each element
                is a list of [position, velocity, angle_vel] for the sprite
                after the first physics step.
            final_state: Same as step_1_state, except for the final step.
            plot: Bool. Whether to display video or run test.
        """
        state = get_state()

        collision = physics_lib.Collision(
            elasticity=0., symmetric=False, update_angle_vel=True)
        physics = physics_lib.Physics(
            (collision, 'sprites', 'walls'),
            corrective_physics=[tether],
            updates_per_env_step=10,
        )

        steps = 45
        if plot:
            MatplotlibUI()._simulate_video(physics, state, steps=steps)
        else:
            physics.step(state)

            for s, pred in zip(state['sprites'], step_1_state):
                assert np.allclose(s.position, pred[0], atol=_ATOL)
                assert np.allclose(s.velocity, pred[1], atol=_ATOL)
                assert np.allclose(s.angle_vel, pred[2], atol=_ATOL)

            for _ in range(steps - 1):
                physics.step(state)
            
            for s, pred in zip(state['sprites'], final_state):
                assert np.allclose(s.position, pred[0], atol=_ATOL)
                assert np.allclose(s.velocity, pred[1], atol=_ATOL)
                assert np.allclose(s.angle_vel, pred[2], atol=_ATOL)

    def testTetherNoAngleUpdate(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', update_angle_vel=False)

        step_1_state = [
            [[0.5133, 0.6933], [0.0133, -0.0067], 0.],
            [[0.2133, 0.5933], [0.0133, -0.0067], 0.],
            [[0.6133, 0.2933], [0.0133, -0.0067], 0.],
        ]

        final_state = [
            [[0.7710, 0.4900], [-0.0005, 0.], 0.],
            [[0.4710, 0.3900], [-0.0005, 0.], 0.],
            [[0.8671, 0.0939], [-0.0005, 0.], 0.],
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

    def testTetherAngleUpdate(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', update_angle_vel=True)

        step_1_state = [
            [[0.5206, 0.6900], [0.0205, -0.0103], -0.0447],
            [[0.2165, 0.6035], [0.0166, 0.0033], -0.0447],
            [[0.6027, 0.2860], [0.0025, -0.0140], -0.0447],
        ]

        final_state = [
            [[0.8341, 0.3401], [-0.0028, -0.0046], -0.0229],
            [[0.7139, 0.6271], [0.0037, -0.0018], -0.0229],
            [[0.4545, 0.2062], [-0.0059, 0.0041], -0.0229],
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

    def testTetherAnchor(self, plot=False):
        """Tether.
        
        Set plot = True to display videos of the test conditions.
        """
        tether = physics_lib.Tether('sprites', anchor=np.array([0.2, 0.2]))

        step_1_state = [
            [[0.5190, 0.6881], [0.0188, -0.0122], -0.0385],
            [[0.2154, 0.5997], [0.0154, -0.0006], -0.0385],
            [[0.6036, 0.2845], [0.0033, -0.0155], -0.0385],
        ]

        final_state = [
            [[0.6927, 0.5118], [0., 0.], 0.],
            [[0.3798, 0.5573], [0., 0.], 0.],
            [[0.6025, 0.1233], [0., 0.], 0.]
        ]

        self._run_test(tether, step_1_state, final_state, plot=plot)

Methods

def testTetherAnchor(self, plot=False)

Tether.

Set plot = True to display videos of the test conditions.

Expand source code
def testTetherAnchor(self, plot=False):
    """Tether.
    
    Set plot = True to display videos of the test conditions.
    """
    tether = physics_lib.Tether('sprites', anchor=np.array([0.2, 0.2]))

    step_1_state = [
        [[0.5190, 0.6881], [0.0188, -0.0122], -0.0385],
        [[0.2154, 0.5997], [0.0154, -0.0006], -0.0385],
        [[0.6036, 0.2845], [0.0033, -0.0155], -0.0385],
    ]

    final_state = [
        [[0.6927, 0.5118], [0., 0.], 0.],
        [[0.3798, 0.5573], [0., 0.], 0.],
        [[0.6025, 0.1233], [0., 0.], 0.]
    ]

    self._run_test(tether, step_1_state, final_state, plot=plot)
def testTetherAngleUpdate(self, plot=False)

Tether.

Set plot = True to display videos of the test conditions.

Expand source code
def testTetherAngleUpdate(self, plot=False):
    """Tether.
    
    Set plot = True to display videos of the test conditions.
    """
    tether = physics_lib.Tether('sprites', update_angle_vel=True)

    step_1_state = [
        [[0.5206, 0.6900], [0.0205, -0.0103], -0.0447],
        [[0.2165, 0.6035], [0.0166, 0.0033], -0.0447],
        [[0.6027, 0.2860], [0.0025, -0.0140], -0.0447],
    ]

    final_state = [
        [[0.8341, 0.3401], [-0.0028, -0.0046], -0.0229],
        [[0.7139, 0.6271], [0.0037, -0.0018], -0.0229],
        [[0.4545, 0.2062], [-0.0059, 0.0041], -0.0229],
    ]

    self._run_test(tether, step_1_state, final_state, plot=plot)
def testTetherNoAngleUpdate(self, plot=False)

Tether.

Set plot = True to display videos of the test conditions.

Expand source code
def testTetherNoAngleUpdate(self, plot=False):
    """Tether.
    
    Set plot = True to display videos of the test conditions.
    """
    tether = physics_lib.Tether('sprites', update_angle_vel=False)

    step_1_state = [
        [[0.5133, 0.6933], [0.0133, -0.0067], 0.],
        [[0.2133, 0.5933], [0.0133, -0.0067], 0.],
        [[0.6133, 0.2933], [0.0133, -0.0067], 0.],
    ]

    final_state = [
        [[0.7710, 0.4900], [-0.0005, 0.], 0.],
        [[0.4710, 0.3900], [-0.0005, 0.], 0.],
        [[0.8671, 0.0939], [-0.0005, 0.], 0.],
    ]

    self._run_test(tether, step_1_state, final_state, plot=plot)