def test_copy_no_form(self): """ Test that the copy function raises an exception when a Gaussian does not have either of its form updated. """ gaussian_no_form = Gaussian(cov=1.0, mean=0.0, log_weight=0.0, var_names=["a"]) gaussian_no_form.covform = False with self.assertRaises(Exception): gaussian_no_form.copy()
def test_marginalise_with_conditional(self): """ Test that marginalising an unconditioned non-linear Gaussian results in a vacuous Gaussian. """ ab_vars = ["a", "b"] cd_vars = ["c", "d"] a_mat = np.array([[2, 0], [0, 1]]) def transform(x_val, _): return a_mat.dot(x_val) noise_cov = np.array([[1, 0], [0, 1]]) nlg_factor = NonLinearGaussian(conditioning_vars=ab_vars, conditional_vars=cd_vars, transform=transform, noise_cov=noise_cov) cov = np.array([[2, 1], [1, 3]]) mean = np.array([[2], [1]]) conditional_gaussian = Gaussian(cov=cov, mean=mean, log_weight=0.0, var_names=cd_vars) nlg_factor = nlg_factor.multiply(conditional_gaussian) actual_cd_marginal = nlg_factor.marginalize(vrs=cd_vars, keep=True) expected_cd_marginal = conditional_gaussian.copy() self.assertTrue(actual_cd_marginal.equals(expected_cd_marginal))
def test_observe(self): """ Test that the Gaussian reduce function returns the correct result. """ # Note these equations where written independently from the actuall implementation. # TODO: consider extending this test and hard-coding the expected parameters kmat = np.array([[6, 2, 1], [2, 8, 3], [1, 3, 9]]) hvec = np.array([[1], [2], [3]]) g_val = 1.5 z_observed = np.array([[6]]) expected_prec = np.array([[6, 2], [2, 8]]) expeted_h = np.array([[-5], [-16]]) expected_g = -142.5 expected_gaussian = Gaussian(prec=expected_prec, h_vec=expeted_h, g_val=expected_g, var_names=["x", "y"]) gaussian = Gaussian(prec=kmat, h_vec=hvec, g_val=g_val, var_names=["x", "y", "z"]) actual_gaussian = gaussian.reduce(vrs=["z"], values=z_observed) self.assertTrue(actual_gaussian.equals(expected_gaussian)) # Test that the result is still correct with a different parameter order. gaussian_copy = gaussian.copy() gaussian_copy._reorder_parameters(["x", "z", "y"]) actual_gaussian = gaussian_copy.reduce(vrs=["z"], values=z_observed) self.assertTrue(actual_gaussian.equals(expected_gaussian))
def test_copy_2d_canform(self): """ Test that the copy function returns a identical copy of a Gaussian in canonical form. """ gaussian = Gaussian(prec=[[7.0, 2.0], [2.0, 1.0]], h_vec=[4.0, 1.0], g_val=0.0, var_names=["a", "b"]) self.assertTrue(gaussian.equals(gaussian.copy()))
def test_copy_2d_covform(self): """ Test that the copy function returns a identical copy of a two dimensional Gaussian in covariance form. """ gaussian = Gaussian(cov=[[7.0, 2.0], [2.0, 1.0]], mean=[4.0, 1.0], log_weight=0.0, var_names=["a", "b"]) self.assertTrue(gaussian.equals(gaussian.copy()))
def test_form_conversion(self): """ Test that conversion from one form to the other and back results in the same Gaussian parameters. """ gaussian_ab = Gaussian(cov=[[7.0, 2.0], [2.0, 1.0]], mean=[4.0, 1.0], log_weight=0.0, var_names=["a", "b"]) gaussian_ab_copy = gaussian_ab.copy() gaussian_ab_copy._update_canform() gaussian_ab_copy.covform = False gaussian_ab_copy._update_covform() self.assertTrue(gaussian_ab.equals(gaussian_ab_copy))
def test_multiply_both_sides(self): """ Test that the multiply function results in the correct joint distribution when a factor with the conditional scope is received first and a conditioning scope factor is received afterwards. """ conditional_update_cov = np.array([[2, 1], [1, 4]]) conditional_update_mean = np.array([[5], [4]]) conditional_update_factor = Gaussian(cov=conditional_update_cov, mean=conditional_update_mean, log_weight=0.0, var_names=["c", "d"]) a_mat = np.array([[2, 0], [0, 1]]) def transform(x_val, _): return a_mat.dot(x_val) noise_cov = np.array([[1, 0], [0, 1]]) nlg_factor = NonLinearGaussian( conditioning_vars=["a", "b"], conditional_vars=["c", "d"], transform=transform, noise_cov=noise_cov, ) conditioning_cov = np.array([[3, 1], [1, 5]]) conditioning_mean = np.array([[2], [3]]) conditioning_gaussian = Gaussian(cov=conditioning_cov, mean=conditioning_mean, log_weight=0.0, var_names=["a", "b"]) # TODO: Consider replacing this with hardcoded specific params. joint_cov, joint_mean = con_params_to_joint(conditioning_cov, conditioning_mean, a_mat, noise_cov) expected_joint = Gaussian(cov=joint_cov, mean=joint_mean, log_weight=0.0, var_names=["a", "b", "c", "d"]) expected_joint = expected_joint.multiply( conditional_update_factor.copy()) nlg_factor = nlg_factor.multiply(conditional_update_factor) nlg_factor = nlg_factor.multiply(conditioning_gaussian) conditional_update_factor.show() self.assertTrue(expected_joint.equals(nlg_factor.joint_distribution))
def test_copy_1d_covform(self): """ Test that the copy function returns a identical copy of a one dimensional Gaussian in covariance form. """ gaussian = Gaussian(cov=7.0, mean=4.0, log_weight=0.0, var_names=["a"]) self.assertTrue(gaussian.equals(gaussian.copy()))
class TestClusterGraph(unittest.TestCase): """ A test class for the ClusterGraph class """ def setUp(self): """ Run before every test. """ self.cg_1, self.cg1_factors = get_cg1_and_factors() self.processed_cg1, _ = get_cg1_and_factors() self.processed_cg1.process_graph() vrs = ["a"] cov = [[10]] mean = [[0]] log_weight = 0.0 self.p_a = Gaussian(var_names=vrs, cov=cov, mean=mean, log_weight=log_weight) vrs = ["a", "b"] prec = [[0.4263, -0.4737], [-0.4737, 0.5263]] h_ved = [[0.0], [0.0]] g_val = -1.2398654762908699 self.p_b_g_a = Gaussian(var_names=vrs, prec=prec, h_vec=h_ved, g_val=g_val) vrs = ["a", "c"] prec = [[0.0099, -0.0330], [-0.0330, 0.1099]] h_ved = [[0.0], [0.0]] g_val = -2.0230757399660746 self.p_c_g_a = Gaussian(var_names=vrs, prec=prec, h_vec=h_ved, g_val=g_val) vrs = ["a", "b", "c"] cov = [[10.0066, 9.0065, 3.0047], [9.0065, 10.0064, 2.7044], [3.0047, 2.7044, 10.0014]] mean = [[0.0], [0.0], [0.0]] log_weight = 7.069106988666363e-08 self.p_a_b_c = Gaussian(var_names=vrs, cov=cov, mean=mean, log_weight=log_weight) def tearDown(self): """ Run after every test. """ unstub() def test__sort_almost_sorted_sorted(self): """ Test that the _sort_almost_sorted function returns a already sorted deque unchanged. """ expected_result = deque([8, 6, 4, 0]) input_deque = deque([8, 6, 4, 0]) actual_result = _sort_almost_sorted(input_deque, key=lambda x: x) self.assertEqual(expected_result, actual_result) def test__sort_almost_sorted_not_sorted(self): """ Test that the _sort_almost_sorted function sorts the deque correctly. """ expected_result = deque([8, 6, 5, 4]) input_deque = deque([5, 8, 6, 4]) actual_result = _sort_almost_sorted(input_deque, key=lambda x: x) self.assertEqual(expected_result, actual_result) def test__sort_almost_sorted_front_to_back(self): """ Test that the _sort_almost_sorted function sorts the deque correctly. """ expected_result = deque([8, 6, 4, 3]) input_deque = deque([3, 8, 6, 4]) actual_result = _sort_almost_sorted(input_deque, key=lambda x: x) self.assertEqual(expected_result, actual_result) # TODO: improve this function and remove too-many-locals below def test_correct_message_passing(self): # pylint: disable=too-many-locals """ Check that the correct messages are passed in the correct order. """ factor_a = Gaussian(var_names=["a"], cov=[[0.5]], mean=[0.0], log_weight=3.0) factor_ab = Gaussian(var_names=["a", "b"], cov=[[10, 9], [9, 10]], mean=[0, 0], log_weight=0.0) factor_abfa = factor_ab.absorb(factor_a) factor_ac = Gaussian(var_names=["a", "c"], cov=[[10, 3], [3, 10]], mean=[0, 0], log_weight=0.0) factor_bd = Gaussian(var_names=["b", "d"], cov=[[15, 4], [4, 15]], mean=[0, 0], log_weight=0.0) cluster_graph = ClusterGraph( [factor_a, factor_ab, factor_ac, factor_bd]) # expected messages # from cluster 0 (factor_abfa) to cluster 1 (factor_ac) msg_1_factor = factor_abfa.marginalize(vrs=["a"], keep=True) msg_1 = Message(msg_1_factor, "c0#a,b", "c1#a,c") # from cluster 0 (factor_abfa) to cluster 2 (factor_bd) msg_2_factor = factor_abfa.marginalize(vrs=["b"], keep=True) msg_2 = Message(msg_2_factor, "c0#a,b", "c2#b,d") # from cluster 1 (factor_ac) to cluster 0 (factor_abfa) msg_3_factor = factor_ac.marginalize(vrs=["a"], keep=True) msg_3 = Message(msg_3_factor, "c1#a,c", "c0#a,b") # from cluster 2 (factor_bd) to cluster 0 (factor_abfa) msg_4_factor = factor_bd.marginalize(vrs=["b"], keep=True) msg_4 = Message(msg_4_factor, "c2#b,d", "c0#a,b") expected_messages = [msg_1, msg_2, msg_3, msg_4] # Test that the factors of the cluster in the cluster graph are correct expected_cluster_factors = [ factor_abfa.copy(), factor_ac.copy(), factor_bd.copy() ] actual_cluster_factors = [c._factor for c in cluster_graph._clusters] def key_func(factor): return "".join(factor.var_names) actual_cluster_factors = sorted(actual_cluster_factors, key=key_func) expected_cluster_factors = sorted(expected_cluster_factors, key=key_func) for actual_f, expect_f in zip(actual_cluster_factors, expected_cluster_factors): self.assertEqual(actual_f, expect_f) # See note below for gmp in cluster_graph.graph_message_paths: receiver_cluster_id = gmp.receiver_cluster._cluster_id sender_cluster_id = gmp.sender_cluster._cluster_id message_vars = gmp.sender_cluster.get_sepset(receiver_cluster_id) dim = len(message_vars) almost_vacuous = Gaussian(var_names=message_vars, cov=np.eye(dim) * 1e10, mean=np.zeros([dim, 1]), log_weight=0.0) gmp.previously_sent_message = Message( sender_id=sender_cluster_id, receiver_id=receiver_cluster_id, factor=almost_vacuous) gmp.update_next_information_gain() cluster_graph.debug = True cluster_graph.process_graph(tol=0, max_iter=1) # Note # Now we want to ensure and check a certain message order. The problem is that if more than one KLD is inf, # there is no correct sorting order. This potentially points to a trade-off between easy 'distance from vacuous' # calculations at the start of message passing (and not ensuring that the most informative message is sent) and # maybe rather calculating a distance from almost vacuous and ensuring that the most informative messages are # sent first. Infinities might not be sortable, but that does not mean they are equal. actual_messages = cluster_graph.passed_messages self.assertEqual(len(expected_messages), len(actual_messages)) for actual_message, expected_message in zip(actual_messages, expected_messages): self.assertEqual(actual_message.sender_id, expected_message.sender_id) self.assertEqual(actual_message.receiver_id, expected_message.receiver_id) self.assertTrue( actual_message.equals(expected_message, rtol=1e-03, atol=1e-03)) def test_correct_posterior_marginal_weights(self): """ Check that the marginal weights are correct. """ # TODO: see why this fails with max_iter=1 self.cg_1.process_graph(tol=0, max_iter=2) # check posterior weight joint = self.cg1_factors[0] for factor in self.cg1_factors[1:]: joint = joint.absorb(factor) expected_log_weight = joint.get_log_weight() # the marginals are all marginals of the same distribution and should therefore have the same weight # (the integrand is the same, regardless of the order in which the variables are integrated out) actual_log_weights = [ cluster._factor.get_log_weight() for cluster in self.cg_1._clusters ] self.assertTrue(np.allclose(actual_log_weights, expected_log_weight)) @mock.patch.object(plt, "legend") def test_plot_next_messages_info_gain_legend(self, plt_mock): """ Test that the legend function is called when it should be. """ self.processed_cg1.plot_next_messages_info_gain(legend_on=True) plt_mock.assert_called() def test_init_fail_duplicate_cluster_ids(self): """ Test that the initializer fails when clusters have duplicate cluster_ids and returns the correct error message. """ with mock.patch( "veroku.cluster_graph.Cluster.cluster_id", new_callable=unittest.mock.PropertyMock) as mock_cluster_id: mock_cluster_id.return_value = "same_id" with self.assertRaises(ValueError) as error_context: ClusterGraph([ make_random_gaussian(["a", "b"]), make_random_gaussian(["b", "c"]), make_random_gaussian(["c", "d"]), ]) exception_msg = error_context.exception.args[0] self.assertTrue("non-unique" in exception_msg.lower()) expected_num_same_id_cluster = 3 actual_same_id_clusters = exception_msg.count("same_id") self.assertTrue(expected_num_same_id_cluster, actual_same_id_clusters) @mock.patch.object(plt, "plot") def test_plot_message_convergence(self, mock_plot): """ Test that the correct functions are called within the plot_message_convergence function. """ cg2_processed = get_cg2(seed=0, process=True) # TODO: Add check that log is called when log=True cg2_processed.plot_message_convergence(log=True) mock_plot.assert_called() @patch("builtins.print") def test__conditional_print_called(self, print_mock): """ Test that the _conditional_print function is called when verbose=True. """ self.cg_1.verbose = True self.cg_1._conditional_print("dummy") print_mock.assert_called() @patch("builtins.print") def test__conditional_print_not_called(self, print_mock): """ Test that the _conditional_print function is not called when verbose=False. """ self.cg_1.verbose = False self.cg_1._conditional_print("dummy") print_mock.assert_not_called() @patch("IPython.core.display.display") def test_show(self, display_mock): """ Test that the show method calls the display function. """ self.cg_1.show() display_mock.assert_called() @patch("graphviz.Source") def test_save_graph_image(self, source_mock): """ Test that save_graph_image saves a graph to the correct file. """ filename = "dummy_file" self.cg_1.save_graph_image(filename=filename) source_mock.assert_called_with(self.cg_1._graph, filename=filename, format="png") def test_correct_marginal_special_evidence(self): """ Test that the get_marginal function returns the correct marginal after a graph with special evidence has been processed. """ factors = [self.p_a, self.p_b_g_a, self.p_c_g_a] cga = ClusterGraph(factors, special_evidence={"a": 3.0}) cga.process_graph(max_iter=1) vrs = ["b"] cov = [[1.9]] mean = [[2.7002]] log_weight = -2.5202640960492313 expected_posterior_marginal = Gaussian(var_names=vrs, cov=cov, mean=mean, log_weight=log_weight) actual_posterior_marginal = cga.get_marginal(vrs=["b"]) actual_posterior_marginal._update_covform() self.assertTrue( expected_posterior_marginal.equals(actual_posterior_marginal)) def test_correct_marginal_fails_vars(self): """ Test that the get_marginal function fails when there is no factor containing the variables to keep. """ g_1 = make_random_gaussian(["a", "b"]) g_2 = make_random_gaussian(["b", "c"]) cga = ClusterGraph([g_1, g_2]) cga.process_graph() with self.assertRaises(ValueError): cga.get_marginal(["a", "c"]) def test_get_posterior_joint(self): """ Test that the get_posterior_joint returns the correct joint distribution after the graph has been processed. """ factors = [self.p_a, self.p_b_g_a, self.p_c_g_a] cga = ClusterGraph(factors) cga.process_graph() actual_posterior_joint = cga.get_posterior_joint() actual_posterior_joint._update_covform() expected_posterior_joint = self.p_a_b_c.copy() self.assertTrue( actual_posterior_joint.equals(expected_posterior_joint)) def test_process_graph_1_factor(self): """ Test that the get_posterior_joint returns the correct joint distribution (after the graph has been processed) for a graph constructed with a single factor. """ factors = [self.p_a_b_c] cga = ClusterGraph(factors) cga.process_graph() actual_posterior_joint = cga.get_posterior_joint() expected_posterior_joint = self.p_a_b_c.copy() self.assertTrue( actual_posterior_joint.equals(expected_posterior_joint)) def test_process_graph_1_factor_se(self): """ Test that the get_posterior_joint returns the correct joint distribution (after the graph has been processed) for a graph constructed with a single factor and special evidence. """ factors = [self.p_a_b_c] cga = ClusterGraph(factors, special_evidence={"a": 0.3}) cga.process_graph() actual_posterior_joint = cga.get_posterior_joint() expected_posterior_joint = self.p_a_b_c.observe(["a"], [0.3]) self.assertTrue( actual_posterior_joint.equals(expected_posterior_joint)) def test_make_animation_gif(self): """ Test that the make_message_passing_animation_gif creates a file with the correct name. """ # TODO: improve this test. factors = [self.p_a, self.p_b_g_a, self.p_c_g_a] cga = ClusterGraph(factors) cga.process_graph(make_animation_gif=True) filename = "my_graph_animation_now.gif" self.assertFalse(filename in os.listdir()) cga.make_message_passing_animation_gif(filename=filename) self.assertTrue(filename in os.listdir()) os.remove(filename)
def test_correct_message_passing(self): # pylint: disable=too-many-locals """ Check that the correct messages are passed in the correct order. """ factor_a = Gaussian(var_names=["a"], cov=[[0.5]], mean=[0.0], log_weight=3.0) factor_ab = Gaussian(var_names=["a", "b"], cov=[[10, 9], [9, 10]], mean=[0, 0], log_weight=0.0) factor_abfa = factor_ab.absorb(factor_a) factor_ac = Gaussian(var_names=["a", "c"], cov=[[10, 3], [3, 10]], mean=[0, 0], log_weight=0.0) factor_bd = Gaussian(var_names=["b", "d"], cov=[[15, 4], [4, 15]], mean=[0, 0], log_weight=0.0) cluster_graph = ClusterGraph( [factor_a, factor_ab, factor_ac, factor_bd]) # expected messages # from cluster 0 (factor_abfa) to cluster 1 (factor_ac) msg_1_factor = factor_abfa.marginalize(vrs=["a"], keep=True) msg_1 = Message(msg_1_factor, "c0#a,b", "c1#a,c") # from cluster 0 (factor_abfa) to cluster 2 (factor_bd) msg_2_factor = factor_abfa.marginalize(vrs=["b"], keep=True) msg_2 = Message(msg_2_factor, "c0#a,b", "c2#b,d") # from cluster 1 (factor_ac) to cluster 0 (factor_abfa) msg_3_factor = factor_ac.marginalize(vrs=["a"], keep=True) msg_3 = Message(msg_3_factor, "c1#a,c", "c0#a,b") # from cluster 2 (factor_bd) to cluster 0 (factor_abfa) msg_4_factor = factor_bd.marginalize(vrs=["b"], keep=True) msg_4 = Message(msg_4_factor, "c2#b,d", "c0#a,b") expected_messages = [msg_1, msg_2, msg_3, msg_4] # Test that the factors of the cluster in the cluster graph are correct expected_cluster_factors = [ factor_abfa.copy(), factor_ac.copy(), factor_bd.copy() ] actual_cluster_factors = [c._factor for c in cluster_graph._clusters] def key_func(factor): return "".join(factor.var_names) actual_cluster_factors = sorted(actual_cluster_factors, key=key_func) expected_cluster_factors = sorted(expected_cluster_factors, key=key_func) for actual_f, expect_f in zip(actual_cluster_factors, expected_cluster_factors): self.assertEqual(actual_f, expect_f) # See note below for gmp in cluster_graph.graph_message_paths: receiver_cluster_id = gmp.receiver_cluster._cluster_id sender_cluster_id = gmp.sender_cluster._cluster_id message_vars = gmp.sender_cluster.get_sepset(receiver_cluster_id) dim = len(message_vars) almost_vacuous = Gaussian(var_names=message_vars, cov=np.eye(dim) * 1e10, mean=np.zeros([dim, 1]), log_weight=0.0) gmp.previously_sent_message = Message( sender_id=sender_cluster_id, receiver_id=receiver_cluster_id, factor=almost_vacuous) gmp.update_next_information_gain() cluster_graph.debug = True cluster_graph.process_graph(tol=0, max_iter=1) # Note # Now we want to ensure and check a certain message order. The problem is that if more than one KLD is inf, # there is no correct sorting order. This potentially points to a trade-off between easy 'distance from vacuous' # calculations at the start of message passing (and not ensuring that the most informative message is sent) and # maybe rather calculating a distance from almost vacuous and ensuring that the most informative messages are # sent first. Infinities might not be sortable, but that does not mean they are equal. actual_messages = cluster_graph.passed_messages self.assertEqual(len(expected_messages), len(actual_messages)) for actual_message, expected_message in zip(actual_messages, expected_messages): self.assertEqual(actual_message.sender_id, expected_message.sender_id) self.assertEqual(actual_message.receiver_id, expected_message.receiver_id) self.assertTrue( actual_message.equals(expected_message, rtol=1e-03, atol=1e-03))