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}")
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))
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)
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))
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))
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)
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)
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)
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)
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())
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())