Exemplo n.º 1
0
    def test_grads_no_error_correct_pred(self):
        """
        Case in which the prediction is correct.
        All learnable parameters should have a gradient value
        that does not cause errors with an optimizer.

        Passes if no errors occurs.
        """
        node = gen_node_at(ZERO)
        marble = gen_marble_at(ZERO)
        model = NenwinModel([node], [marble])
        model.make_timestep(1.0)
        optim = torch.optim.Adam(model.parameters())
        optim.zero_grad()

        loss_fun = NenwinLossFunction([node], model, 0, 1)

        target_index = 0
        result = loss_fun(target_index)

        try:
            result.backward()
            optim.step()
        except RuntimeError as e:
            self.fail(f"Error occurred during backprop/optim step: {e}")
Exemplo n.º 2
0
    def test_reset(self):
        # Node and Marble have some arbitrary nonzero initial motion vars.
        node = Node(torch.tensor([1.]), torch.tensor([2.]), torch.tensor([3.]),
                    4, NewtonianGravity(), 1, 1, 1, 1)
        marble = Marble(torch.tensor([1.1]), torch.tensor([2.2]),
                        torch.tensor([3.3]), 4.4, NewtonianGravity(), None)
        model = NenwinModel([node], [marble])
        model.make_timestep(10)
        model.make_timestep(10)
        model.make_timestep(10)

        # Verify that the motion variables have changed
        self.assertFalse(check_close(node.pos, node.init_pos))
        self.assertFalse(check_close(marble.vel, marble.init_vel))
        self.assertFalse(check_close(marble.acc, marble.init_acc))

        model.reset()

        # Now they should be the original values again.
        self.assertTrue(check_close(node.pos, node.init_pos))
        self.assertTrue(check_close(node.vel, node.init_vel))
        self.assertTrue(check_close(node.acc, node.init_acc))
        self.assertTrue(check_close(marble.pos, marble.init_pos))
        self.assertTrue(check_close(marble.vel, marble.init_vel))
        self.assertTrue(check_close(marble.acc, marble.init_acc))
Exemplo n.º 3
0
    def test_eaten_marbles_disappear(self):

        # Both are generated at the smame pos, so immediately eaten.
        node = MarbleEaterNode(ZERO, ZERO, ZERO, 10, NewtonianGravity(), 1, 1,
                               1, 0, 10)
        marble = Marble(ZERO, ZERO, ZERO, 10, NewtonianGravity(), datum=None)
        model = NenwinModel([node], [marble])
        model.make_timestep(1.0)

        self.assertSetEqual(model.marbles, set([]))
        self.assertNotIn(marble, model._NenwinModel__all_particles)
Exemplo n.º 4
0
    def test_make_timestep_1(self):
        """
        Base base: single moving Marble, with constant velocity.
        """
        marble = Marble(ZERO, np.array([10]), ZERO, 1, None, None, 0, 0, 0, 0)
        model = NenwinModel([], [marble])

        model.make_timestep(time_passed=1)
        expected_new_pos = torch.tensor([10], dtype=torch.float)

        self.assertTrue(torch.allclose(marble.pos, expected_new_pos))
Exemplo n.º 5
0
    def test_make_timestep_2(self):
        """
        Base case: initial stationairy Marble, gets attracted and accelerated.
        """
        node = Node(pos=ZERO,
                    vel=ZERO,
                    acc=ZERO,
                    mass=1,
                    attraction_function=ATTRACT_FUNCT,
                    marble_stiffness=1,
                    node_stiffness=1,
                    marble_attraction=1,
                    node_attraction=0)
        marble = Marble(pos=np.array([5]),
                        vel=ZERO,
                        acc=ZERO,
                        mass=1,
                        attraction_function=ATTRACT_FUNCT,
                        datum=None)

        model = NenwinModel([node], [marble])
        time_passed = 1

        expected_pos, expected_vel = runge_kutta_4_step(marble.pos,
                                                        marble.vel,
                                                        -ATTRACT_FUNCT.value,
                                                        duration=time_passed)
        model.make_timestep(time_passed)

        self.assertTrue(torch.allclose(marble.pos, expected_pos, atol=0.01))
        self.assertTrue(torch.allclose(marble.vel, expected_vel, atol=0.01))
        self.assertTrue(
            torch.isclose(marble.acc,
                          torch.tensor(-ATTRACT_FUNCT.value),
                          atol=0.01))

        self.assertTrue(torch.allclose(node.pos, ZERO))
        self.assertTrue(torch.allclose(node.vel, ZERO))
        self.assertTrue(torch.allclose(node.acc, ZERO))
Exemplo n.º 6
0
    def test_grads_value_correct_pred(self):
        """
        Case in which the prediction is correct.
        All learnable parameters should have a gradient value
        that does not affect the values of the weights.
        """
        node = gen_node_at(ZERO)
        marble = gen_marble_at(torch.tensor([1.1, 1.1]))
        model = NenwinModel([node], [marble])
        for _ in range(1000):
            model.make_timestep(0.1)

        assert node.num_marbles_eaten == 1, "Testcase badly desgined."

        loss_fun = NenwinLossFunction([node], model, 0, 1)

        self.target_index = 0  # Node at index 1 ate the Marble
        result = loss_fun(self.target_index)
        result.backward(retain_graph=True)

        self.assertIsNone(marble.init_pos.grad)
        self.assertIsNone(marble.init_vel.grad)
        self.assertIsNone(marble.mass.grad)
Exemplo n.º 7
0
class ModelBackpropTestCase(unittest.TestCase):
    """
    Testcase tests if backpropagation correctly behaves for particle
    interaction within a model.
    """
    def setUp(self):
        self.node = Node(pos=ZERO,
                         vel=ZERO,
                         acc=ZERO,
                         mass=1,
                         attraction_function=NewtonianGravity(),
                         marble_stiffness=1,
                         node_stiffness=1,
                         marble_attraction=1,
                         node_attraction=0)
        self.marble = Marble(pos=np.array([5]),
                             vel=ZERO,
                             acc=ZERO,
                             mass=1,
                             attraction_function=ATTRACT_FUNCT,
                             datum=None)

        self.model = NenwinModel([self.node], [self.marble])

    def test_particle_gradients(self):
        """
        Base case: a Marble being attracted by a stationary Node,
        when using Marble's variables to compute loss,
        also Node.pos should receive gradients 
        when computing backprop on the loss.
        """
        self.model.make_timestep(1.0)
        self.model.make_timestep(1.0)
        loss = 2 * self.marble.pos
        loss.backward()

        self.assertIsNotNone(self.node.init_pos.grad)

    def test_particle_gradients_moving_node(self):
        """
        Base case: Marble being attracted by a moving Node,
        when using Marble's variables to compute loss,
        also Node.pos should receive gradients 
        when computing backprop on the loss.
        """
        self.node = Node(pos=ZERO,
                         vel=torch.tensor([0.1]),
                         acc=ZERO,
                         mass=1,
                         attraction_function=NewtonianGravity(),
                         marble_stiffness=1,
                         node_stiffness=1,
                         marble_attraction=1,
                         node_attraction=0)
        self.model = NenwinModel([self.node], [self.marble])
        self.model.make_timestep(1.0)
        self.model.make_timestep(1.0)
        self.model.make_timestep(1.0)

        loss = 2 * self.marble.acc
        loss.backward()

        self.assertIsNotNone(self.marble.init_pos.grad)
        self.assertIsNotNone(self.node.init_pos.grad)
        self.assertIsNotNone(self.node.init_vel.grad)
        self.assertIsNotNone(self.node._PhysicalParticle__mass.grad)
Exemplo n.º 8
0
class LossFunctionMultiplePredAllWrongTestCase(unittest.TestCase):
    """
    Testcases for NenwinLossFunction.__call__() 
    when multiple output-MarbleEaterNodes have eaten a Marble.
    The losses should count up.
    """

    def setUp(self):
        """

        Sketch:

         |
        ↑| M_1       M_2        M_3
        y|  ↓         ↓          ↓
         |            
        0| N_1       N_2        N_0
         |             
         |
         +-------------------------------
           -10         0         10   x→


        Marbles M_1 and M_2 are moving towards
        MarbleEaterNodes N_1 and N_2 respectively.
        They arrive at the same moment. 
        Only N_0 is the target output, 
        which will not receive any Marble.
        M_3 remains stationary.
        """
        self.pos_weight = 0.5
        self.vel_weight = 0.5

        self.nodes = (gen_node_at(torch.tensor([10.0, 0])),
                      gen_node_at(torch.tensor([-10.0, 0])),
                      gen_node_at(torch.tensor([0.0, 0]))
                      )
        self.marble_1 = gen_marble_at(torch.tensor([-10.0, 10.0]),
                                    vel=torch.tensor([0, -3.0]),
                                    datum="M_1")
        self.marble_2 = gen_marble_at(torch.tensor([0.0, 10.0]),
                                    vel=torch.tensor([0, -3.0]),
                                    datum="M_2")
        self.marble_3 = gen_marble_at(torch.tensor([10.0, 10.0]),
                                    vel=torch.tensor([0, 0.0]),
                                    datum="M_3")
        marbles = [self.marble_1, self.marble_2, self.marble_3]
        self.model = NenwinModel(self.nodes, marbles)

        for _ in range(50):  # Should be enough for the Marble to be eaten
            self.model.make_timestep(0.1)

        assert len(self.model.marbles) == 1, "Testcase badly designed."
        assert self.nodes[0].num_marbles_eaten == 0, "Testcase badly designed."
        

        self.loss_fun = NenwinLossFunction(self.nodes, self.model,
                                           vel_weight=self.vel_weight,
                                           pos_weight=self.pos_weight)

        self.target_index = 0
        self.wrong_node_index = 1
        self.loss = self.loss_fun(self.target_index)

    def test_value_loss_some_marble_left(self):
        target_node = self.nodes[0]

        # Penalty for the closest Marble that did not arrive at the target
        expected = velocity_weighted_distance(target_node, self.marble_3,
                                              pos_weight=self.pos_weight,
                                              vel_weight=self.vel_weight)
        # Penalties for wrong outputs
        for marble, node in zip([self.marble_1, self.marble_2], self.nodes[1:]):
            expected += -1/velocity_weighted_distance(node, marble,
                                                    pos_weight=self.pos_weight,
                                                    vel_weight=self.vel_weight)
        torch.testing.assert_allclose(self.loss, expected)
Exemplo n.º 9
0
class LossFunctionMultiplePredInclCorrectTestCase(unittest.TestCase):
    """
    Testcases for NenwinLossFunction.__call__() 
    when multiple output-MarbleEaterNodes have eaten a Marble.
    The losses should count up.
    """

    def setUp(self):
        """

        Sketch:

         |
        ↑| M_1                  M_3
        y|  ↓                    ↓
         |            
        0| N_1        ←M_2      N_0
         |             
         |
         +-------------------------------
           -10         0         10   x→


        Marbles M_1 and M_2 are moving towards N_1,
        but should arrive at N_0.
        M_3 arrives correctly at N_0, at the same moment.

        """
        self.pos_weight = 0.5
        self.vel_weight = 0.5

        self.nodes = (gen_node_at(torch.tensor([10.0, 0])),
                      gen_node_at(torch.tensor([-10.0, 0])))
        self.marble_1 = gen_marble_at(ZERO,
                                    vel=torch.tensor([-3.0, 0]),
                                    datum="M_1")
        self.marble_2 = gen_marble_at(torch.tensor([-10.0, 10.0]),
                                    vel=torch.tensor([0, -3.0]),
                                    datum="M_2")
        self.marble_3 = gen_marble_at(torch.tensor([10.0, 10.0]),
                                    vel=torch.tensor([0, -3.0]),
                                    datum="M_3")
        marbles = [self.marble_1, self.marble_2, self.marble_3]
        self.model = NenwinModel(self.nodes, marbles)

        for _ in range(50):  # Should be enough for the Marble to be eaten
            self.model.make_timestep(0.1)

        assert len(self.model.marbles) == 0, "Testcase badly designed."
        

        self.loss_fun = NenwinLossFunction(self.nodes, self.model,
                                           vel_weight=self.vel_weight,
                                           pos_weight=self.pos_weight)

        self.target_index = 0
        self.wrong_node_index = 1
        self.loss = self.loss_fun(self.target_index)


    def test_value_loss_no_marble_left(self):
        """
        For the two wrong Marbles (M_1 and M_2), 
        their loss should equal the velocity-weighted distance
        of the target Node to the nearest Marble,
        plus the *negative reciprocal* of the distance of the wrong node
        (self.nodes[1]) to the Marble.

        For the correcly arrived Marble, the loss should equal 0.
        
        Case where no non-eaten Marble is available 
        at time of loss computation. 
        Not that this should matter, as a correct Marble was eaten!
        """
        wrong_node = self.nodes[self.wrong_node_index]

        expected = 0.0
        for marble in [self.marble_1, self.marble_2]:
            expected -= 1/velocity_weighted_distance(wrong_node, marble,
                                                    pos_weight=self.pos_weight,
                                                    vel_weight=self.vel_weight)
        torch.testing.assert_allclose(self.loss, expected)
Exemplo n.º 10
0
class LossFunctionCallWrongPredTestCase(unittest.TestCase):
    """
    Testcases for NenwinLossFunction.__call__() for when another than the target
    output-MarbleEaterNodes has eaten a Marble.
    """

    def setUp(self):
        """

        Sketch:

         |
        ^|
        y|
         |
        0| N_1        <M        N_0
         |
         |
         +-------------------------------
           -10         0         10   x>


        Marble M starts with moving towards N_1, but should arrive at N_0.

        """
        self.pos_weight = 0.5
        self.vel_weight = 0.5

        self.nodes = (gen_node_at(torch.tensor([10.0, 0])),
                      gen_node_at(torch.tensor([-10.0, 0])))
        self.marble = gen_marble_at(ZERO,
                                    vel=torch.tensor([-3.0, 0]),
                                    datum="original")
        self.model = NenwinModel(self.nodes, [self.marble])

        for _ in range(50):  # Should be enough for the Marble to be eaten
            self.model.make_timestep(0.1)

        assert len(self.model.marbles) == 0, "Testcase badly designed."

        self.loss_fun = NenwinLossFunction(self.nodes, self.model,
                                           vel_weight=self.vel_weight,
                                           pos_weight=self.pos_weight)

        self.target_index = 0
        self.wrong_node_index = 1
        self.loss = self.loss_fun(self.target_index)

    def test_value_loss_wrong_pred_no_marble_left(self):
        """
        The loss should equal the velocity-weighted distance
        of the target Node to the nearest Marble,
        plus the *negative reciprocal* of the distance of the wrong node
        (self.nodes[1]) to the Marble.

        Case where no non-eaten Marble is available.
        """

        target_node = self.nodes[self.target_index]
        wrong_node = self.nodes[self.wrong_node_index]
        expected = velocity_weighted_distance(target_node, self.marble,
                                              pos_weight=self.pos_weight,
                                              vel_weight=self.vel_weight)
        expected -= 1/velocity_weighted_distance(wrong_node, self.marble,
                                                 pos_weight=self.pos_weight,
                                                 vel_weight=self.vel_weight)
        torch.testing.assert_allclose(self.loss, expected)

    def test_value_loss_wrong_pred_some_marble_left(self):
        """
        The loss should equal the velocity-weighted distance
        of the target Node to the nearest Marble,
        plus the *negative reciprocal* of the distance of the wrong node
        (self.nodes[1]) to the Marble.

        Case where another non-eaten Marble is still available
        at time of loss computation.
        """
        second_marble = gen_marble_at(ZERO, datum="second")
        self.model.add_marbles([second_marble])
        self.loss = self.loss_fun(self.target_index)

        target_node = self.nodes[self.target_index]
        wrong_node = self.nodes[self.wrong_node_index]
        expected = velocity_weighted_distance(target_node, second_marble,
                                              pos_weight=self.pos_weight,
                                              vel_weight=self.vel_weight)
        expected -= 1/velocity_weighted_distance(wrong_node, self.marble,
                                                 pos_weight=self.pos_weight,
                                                 vel_weight=self.vel_weight)
        torch.testing.assert_allclose(self.loss, expected)

    def test_grads_no_error_wrong_pred(self):
        """
        Case in which no prediction output is given.
        All learnable parameters should have a gradient value
        that does not cause errors with an optimizer.

        Case where another non-eaten Marble is still available
        at time of loss computation.
        """
        second_marble = gen_marble_at(ZERO)
        self.model.add_marbles([second_marble])

        optim = torch.optim.Adam(self.model.parameters())
        optim.zero_grad()

        try:
            self.loss.backward()
            optim.step()
        except RuntimeError as e:
            self.fail(f"Error occurred during backprop/optim step: {e}")

    def test_grads_value_wrong_pred(self):
        """
        Case in which no prediction output is given.
        The learnable parameters of the Marble should have a gradient value
        that does affect the values of the weights.
        The loss should be lower for the second run.
        """
        second_marble = gen_marble_at(ZERO)
        self.model.add_marbles([second_marble])

        optim = torch.optim.Adam(self.model.parameters())
        optim.zero_grad()

        self.loss.backward()

        self.assertIsNotNone(self.marble.init_pos.grad)
        self.assertIsNotNone(self.marble.init_vel.grad)
        self.assertIsNotNone(self.marble.mass.grad)

        optim.step()

        # Now verify the loss improved.
        self.model.reset()
        self.model.make_timestep(0.1)
        self.model.add_marbles([second_marble])
        new_loss = self.loss_fun(self.target_index)

        self.assertLess(new_loss.item(), self.loss.item())
Exemplo n.º 11
0
class LossFunctionCallNoPredTestCase(unittest.TestCase):
    """
    Testcases for NenwinLossFunction.__call__() in case none of the
    output-MarbleEaterNodes has eaten any Marble.
    """

    def setUp(self):

        self.pos_weight = 0.5
        self.vel_weight = 0.5

        self.node = gen_node_at(ZERO)
        self.marble = gen_marble_at(torch.tensor([10., 10.]))
        self.model = NenwinModel([self.node], [self.marble])
        self.model.make_timestep(0.1)  # too short to cross the distance

        self.loss_fun = NenwinLossFunction([self.node], self.model,
                                           vel_weight=self.vel_weight,
                                           pos_weight=self.pos_weight)

        assert self.node.num_marbles_eaten == 0, "Testcase badly desgined."

        self.target_index = 0
        self.loss = self.loss_fun(self.target_index)

    def test_value_loss_no_output(self):
        """
        If no Marble has been eaten, the loss
        should equal the velocity-weighted distance
        of the target Node to the nearest Marble.
        """
        expected = velocity_weighted_distance(self.node, self.marble,
                                              pos_weight=self.pos_weight,
                                              vel_weight=self.vel_weight)
        torch.testing.assert_allclose(self.loss, expected)

    def test_grads_no_error_no_output(self):
        """
        Case in which no prediction output is given.
        All learnable parameters should have a gradient value
        that does not cause errors with an optimizer.

        Passes if no errors occurs.
        """
        optim = torch.optim.Adam(self.model.parameters())
        optim.zero_grad()

        try:
            self.loss.backward()
            optim.step()
        except RuntimeError as e:
            self.fail(f"Error occurred during backprop/optim step: {e}")

    def test_grads_value_no_output(self):
        """
        Case in which no prediction output is given.
        The learnable parameters of the Marble should have a gradient value
        that does affect the values of the weights.
        The loss should be lower for the second run.
        """
        optim = torch.optim.Adam(self.model.parameters())
        optim.zero_grad()

        self.loss.backward()

        self.assertIsNotNone(self.marble.init_pos.grad)
        self.assertIsNotNone(self.marble.init_vel.grad)
        self.assertIsNotNone(self.marble.mass.grad)

        optim.step()

        # Now verify the loss improved.
        self.model.reset()
        self.model.make_timestep(0.1)
        new_loss = self.loss_fun(self.target_index)

        self.assertLess(new_loss.item(), self.loss.item())