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_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)
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_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_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_add_marbles_empty(self): """ Corner case: add 0 new Marbles. """ marble = generate_dummy_marble() model = NenwinModel([], set([marble])) new_marbles = [] model.add_marbles(new_marbles) expected = set([marble]) self.assertSetEqual(expected, model.marbles)
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_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_get_params(self): """ Nodes and Marbles should be registered as submodules of the model within the PyTorch framework, and hence all learnable parameters should be obtainable via model. """ marbles = (generate_dummy_marble(), ) nodes = (generate_dummy_node(), ) model = NenwinModel(nodes, marbles) # 8 parameters per particle: init_pos, init_vel, init_acc, mass, # and the 4 stiffness / attraction params result = tuple(model.parameters()) print(result) self.assertEqual(len(result), 16)
def test_get_params_add_marbles(self): """ Later added marbles should not be registered as submodules. """ marbles = (generate_dummy_marble(), ) nodes = (generate_dummy_node(), ) model = NenwinModel(nodes) model.add_marbles(marbles) # 8 parameters per particle: init_pos, init_vel, init_acc, mass, # and the 4 stiffness / attraction params result = tuple(model.parameters()) print(result) self.assertEqual(len(result), 8)
def test_add_marbles(self): """ Base case: add two marbles, should appear in marbles() getter. Old marbles should remain as well. """ marble = generate_dummy_marble() model = NenwinModel([], set([marble])) new_marbles = (generate_dummy_marble(), generate_dummy_marble()) model.add_marbles(new_marbles) expected = set([marble, *new_marbles]) self.assertSetEqual(expected, model.marbles) self.assertSetEqual(expected, model._NenwinModel__all_particles)
def test_find_min_weighted_distance_to(self): """ Base case: target particle not in Model. Weights differ. """ marble_positions = ( torch.tensor([0.0, 0]), torch.tensor([10.0, 10.0]) ) marble_velocities = ( torch.tensor([7.5, 7.5]), torch.tensor([5.0, 5.0]) ) marbles = [gen_marble_at(pos=pos, vel=vel) for pos, vel in zip(marble_positions, marble_velocities)] model = NenwinModel([], marbles) target = Marble(torch.tensor([15.0, 15.0]), ZERO, ZERO, 0, None, None) pos_weight = 1 vel_weight = 2 expected = velocity_weighted_distance(target, marbles[0], pos_weight, vel_weight) result = find_min_weighted_distance_to(target, model, pos_weight, vel_weight) torch.testing.assert_allclose(result, expected)
def gen_architecture_b( input_placer_type: type ) -> Tuple[NenwinModel, VelInputPlacer, Tuple[Node]]: """ Same as architecture A, but with four additional Nodes (normal Nodes, no MarbleEaterNodes) at: * (2.5, 2.5), (-2.5, 2.5), (2.5, -2.5), (-2.5, -2.5) So the normal Nodes surround the input-region, and the MarbleEaterNodes are to the left and right of this. Returns: * Model holding the architecture descibed above. * VelInputPlacer with the input region as described above. * Tuple of the two MarbleEaterNodes. """ eater_positions = [(-10, 0), (10, 0)] node_positions = [(0, -5), (-5, 0), (5, 0), (0, 5), (2.5, 2.5), (-2.5, 2.5), (2.5, -2.5), (-2.5, -2.5)] input_region_pos = np.array((-2.5, -1)) input_region_size = np.array((5, 2)) mass = 1 radius = 0.5 attraction_function = NewtonianGravity() nodes = gen_nodes(attraction_function, mass, node_positions) eater_nodes = gen_eater_nodes(attraction_function, mass, radius, eater_positions) model = NenwinModel(nodes + eater_nodes) input_placer = input_placer_type(input_region_pos, input_region_size) return model, input_placer, eater_nodes
def test_repr_2(self): """ Base case: no Nodes, no set initial Marbles given. """ expected = "NenwinModel(set(),set())" result = repr(NenwinModel([])) self.assertEqual(expected, result)
def visualization_demo(): attract_funct = NewtonianGravity() marbles = set() marble_1 = Marble(np.array([460, 460]), np.array([0, 0]), np.array([0, 0]), mass=20, attraction_function=attract_funct, datum=None, marble_stiffness=1, node_stiffness=0, marble_attraction=0, node_attraction=1) marble_2 = Marble(np.array([550, 550]), np.array([10, 10]), np.array([0, 0]), mass=40, attraction_function=attract_funct, datum=None, marble_stiffness=1, node_stiffness=0, marble_attraction=0, node_attraction=1) marble_3 = Marble(np.array([450, 500]), np.array([2, -2]), np.array([0, 0]), mass=40, attraction_function=attract_funct, datum=None, marble_stiffness=1, node_stiffness=0, marble_attraction=0, node_attraction=1) marbles = set((marble_1, marble_2, marble_3)) node_1 = Node(np.array([500, 500]), np.array([0, 0]), np.array([0, 0]), mass=200, attraction_function=attract_funct, marble_stiffness=1, node_stiffness=1, marble_attraction=1, node_attraction=1) node_2 = Node(np.array([400, 400]), np.array([0, 0]), np.array([0, 0]), mass=200, attraction_function=attract_funct, marble_stiffness=0.7, node_stiffness=1, marble_attraction=1, node_attraction=1) nodes = set((node_1, node_2)) model = NenwinModel(nodes, marbles) simulation = Simulation(model, None, None, MockPipe()) visualization = NenwinVisualization((1500, 1000), simulation, model) visualization.run(10, 0.01)
def gen_architecture_c( input_placer_type: type ) -> Tuple[NenwinModel, VelInputPlacer, Tuple[Node]]: """ Generate the following architecture: * The input region is at (0, 0) and has size (6, 1) (So it has vertices {(0, 0), (0, 1), (6, 0), (6, 1)}) * There are two MarbleEaterNodes, at (1, 4) and (5, 4) * There are five normal Nodes, at (1, 2), (2, 3), (3, 2), (4, 3) and (5, 2). Returns: * Model holding the architecture descibed above. * VelInputPlacer with the input region as described above. * Tuple of the two MarbleEaterNodes. """ eater_positions = [(1, 4), (5, 4)] node_positions = [(1, 2), (2, 3), (3, 2), (4, 3), (5, 2)] input_region_pos = np.array((0, 0)) input_region_size = np.array((6, 1)) mass = 1 radius = 0.5 attraction_function = NewtonianGravity() nodes = gen_nodes(attraction_function, mass, node_positions) eater_nodes = gen_eater_nodes(attraction_function, mass, radius, eater_positions) model = NenwinModel(nodes + eater_nodes) input_placer = input_placer_type(input_region_pos, input_region_size) return model, input_placer, eater_nodes
def test_find_most_promising_marble_to_2(self): """ Base case: target particle not in Model. Weights differ. """ marble_positions = ( (0, 0), (10.0, 10.0) ) marble_velocities = ( (7.5, 7.5), (5.0, 5.0) ) marbles = [Marble(torch.tensor(pos, dtype=torch.float), torch.tensor(vel, dtype=torch.float), ZERO, 0, None, None) for pos, vel in zip(marble_positions, marble_velocities)] model = NenwinModel([], marbles) target = Marble(torch.tensor([15.0, 15.0]), ZERO, ZERO, 0, None, None) expected = marbles[0] pos_weight = 1 vel_weight = 2 result = find_most_promising_marble_to(target, model, pos_weight, vel_weight) self.assertIs(result, expected)
def test_find_most_promising_marble_to_1(self): """ Base case: target particle not in Model. Both weights are 1. """ marble_positions = ( (0, 0), (10.2, 10.1), (5, 10), (12.9, 3.2), (9.9, -0.7) ) marble_velocities = ( (0, 0), (5, 5.1), (10, -100), (1.2, 1), (0, 0) ) marbles = [Marble(torch.tensor(pos, dtype=torch.float), torch.tensor(vel, dtype=torch.float), ZERO, 0, None, None) for pos, vel in zip(marble_positions, marble_velocities)] model = NenwinModel([], marbles) target = Marble(torch.tensor([15.0, 15.0]), ZERO, ZERO, 0, None, None) expected = marbles[1] self.assertIs(find_most_promising_marble_to( target, model, 1, 1), expected)
def test_nodes_getter(self): """ nodes() getter should return only and all non-Marble Nodes. """ marble = generate_dummy_marble() node = generate_dummy_node() model = NenwinModel(set([node]), set([marble])) self.assertSetEqual(set([node]), model.nodes)
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_no_initial_marbles(self): """ In vitro test: should not raise error if no Marbles provided. """ try: model = NenwinModel([]) except: self.fail("Initialization of NenwinModel without initial_marbles" + " should not fail.")
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_error_if_no_other_marbles(self): """ Cannot find closest Marble if no other Marbles exist. """ target = Marble(torch.tensor([12.87, 2.9]), ZERO, ZERO, 0, None, None) model = NenwinModel([], [target]) with self.assertRaises(RuntimeError): find_closest_marble_to(target, model)
def test_no_output(self): nodes = [gen_node_at(torch.tensor([10., 10]))] marbles = [gen_marble_at(ZERO)] model = NenwinModel(nodes, marbles) loss_fun = NenwinLossFunction(nodes, model, 0, 1) result = loss_fun._find_loss_case(0) expected = LossCases.no_prediction self.assertEqual(result, expected)
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_correct_output(self): node = gen_node_at(ZERO) marble = gen_marble_at(ZERO) model = NenwinModel([node], [marble]) node.eat(marble) loss_fun = NenwinLossFunction([node], model, 0, 1) target_index = 0 result = loss_fun._find_loss_case(target_index) expected = LossCases.correct_prediction self.assertEqual(result, expected)
def test_wrong_output(self): nodes = [gen_node_at(torch.tensor([10., 10])), gen_node_at(torch.tensor([11., 11]))] marbles = [gen_marble_at(ZERO), gen_marble_at(torch.tensor([11., 11]))] model = NenwinModel(nodes, marbles) nodes[1].eat(marbles[1]) loss_fun = NenwinLossFunction(nodes, model, 0, 1) target_index = 0 # Node at index 1 ate the Marble result = loss_fun._find_loss_case(target_index) expected = LossCases.wrong_prediction self.assertEqual(result, expected)
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)
def test_value_loss_correct_output(self): """ If the prediction is correct, the loss should be 0.0. """ node = gen_node_at(ZERO) marble = gen_marble_at(ZERO) model = NenwinModel([node], [marble]) node.eat(marble) loss_fun = NenwinLossFunction([node], model, 0, 1) target_index = 0 result = loss_fun(target_index) expected = 0.0 self.assertAlmostEqual(result, expected)