diff --git a/configuration/config.py b/configuration/config.py index b5b2238..ddfec5f 100644 --- a/configuration/config.py +++ b/configuration/config.py @@ -32,6 +32,7 @@ class Config: The rest of attributes are simply arguments defined in parser.py and described there """ # parsed arguments that have default value defined in parser.py + # each one should be defined here (as None), so the IDE knows the class has these attributes n = None q = None avg_deg = None @@ -62,6 +63,8 @@ class Config: num_parties = None config_file = None + # alternative electoral systems can be passed only in the configuration file + alternative_systems = None # derivatives of parsed arguments and others initialize_states = staticmethod(ng.default_initial_state) @@ -87,9 +90,6 @@ class Config: not_zealot_state = None all_states = None # the order matters in the mutation function! zealot first - # alternative electoral systems can be passed only in the configuration file - alternative_systems = None - @staticmethod def wrap_configuration(voting_function, **kwargs): """ @@ -248,6 +248,9 @@ def f(n, g): if self.mass_media is None: self.mass_media = 1.0 / self.num_parties # the symmetric case with no propaganda + elif self.mass_media < 0 or self.mass_media > 1: + raise ValueError(f'The mass_media parameter should be in the range [0,1], ' + f'mass_media={self.mass_media} was provided.') # Initialization in the consensus state if self.consensus: @@ -338,6 +341,7 @@ def f(n, g): alt['total_seats'] = alt['seats'][0] else: alt['total_seats'] = self.total_seats + alt['seats'] = None self.voting_systems[alt['name']] = self.wrap_configuration( es.single_district_voting, states=self.all_states, total_seats=alt['total_seats'], diff --git a/configuration/tests/__init__.py b/configuration/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configuration/tests/configuration_tests.py b/configuration/tests/configuration_tests.py new file mode 100644 index 0000000..16443ff --- /dev/null +++ b/configuration/tests/configuration_tests.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +import unittest +import json +from argparse import Namespace + +from configuration.config import Config, num_to_chars, generate_state_labels +from configuration.parser import parser +from electoral_sys.seat_assignment import seat_assignment_rules +from simulation.base import majority_propagation +from net_generation.base import consensus_initial_state + + +class DummyParser(Namespace): + + def __init__(self, **kwargs): + self.alternative_systems = None + self.avg_deg = 12.0 + self.config_file = None + self.consensus = False + self.district_coords = None + self.district_sizes = None + self.epsilon = 0.01 + self.euclidean = False + self.mass_media = None + self.mc_steps = 50 + self.n = 1875 + self.n_zealots = 1 + self.num_parties = 2 + self.planar_c = None + self.propagation = 'standard' + self.q = 25 + self.random_dist = False + self.ratio = 0.02 + self.reset = False + self.sample_size = 500 + self.seat_rule = 'simple' + self.seats = [1] + self.therm_time = 300000 + self.threshold = 0.0 + self.where_zealots = 'random' + self.zealots_district = None + super().__init__(**kwargs) + + +class SillyAttribute: + + def __getattribute__(self, item): + return "something that definitely is not a 'dest' name of any parameter in the argument parser" + + +class ArgumentDict(dict): + """ + A class to pass as the 2nd argument for Config instead of parser._option_string_actions, + after getting an item and then an attribute, like arg_dict[argument].dest, it should return something + that is not present in the configuration file, not to raise an error. + """ + def __getitem__(self, key): + return SillyAttribute() + + +class MockConfigFile: + + def __init__(self, **kwargs): + self.name = 'mock config file' + self.config_file = dict({ + 'avg_deg': 5, + 'num_parties': 4, + 'propagation': 'majority', + 'seat_rule': 'hare', + 'therm_time': 15654, + }, **kwargs) + + def read(self): + return json.dumps(self.config_file) + + +class TestConfiguration(unittest.TestCase): + + def assertHasAttr(self, obj, intended_attr): + """ + Helper function to assert if the object 'obj' has the attribute 'intended_attr' with a nice message. + """ + test_bool = hasattr(obj, intended_attr) + self.assertTrue(test_bool, msg=f"'{obj}' is lacking an attribute '{intended_attr}'") + + def test_num_to_chars(self): + res = num_to_chars(27) + self.assertEqual(res, 'ab') + + def test_num_to_chars_error1(self): + def inner(): + num_to_chars(-10) + self.assertRaises(ValueError, inner) + + def test_num_to_chars_error2(self): + def inner(): + num_to_chars(10.0) + self.assertRaises(ValueError, inner) + + def test_generate_state_labels(self): + res = generate_state_labels(53) + self.assertListEqual(res, + ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah', + 'ai', 'aj', 'ak', 'al', 'am', 'an', 'ao', 'ap', 'aq', 'ar', 'as', 'at', 'au', 'av', 'aw', + 'ax', 'ay', 'az', 'ba']) + + def test_config_basic_class_attributes(self): + # might be silly to test whether some attributes exist, but it might help to think through future changes + # and all the parser actions should be defined as class attributes of Config, so instances know they have them + self.maxDiff = None + self.assertIn('country-wide_system', Config.voting_systems.keys()) + self.assertIn('main_district_system', Config.voting_systems.keys()) + for action in parser._actions: + if action.dest != 'help': + self.assertHasAttr(Config, action.dest) + + self.assertHasAttr(Config, 'zealot_state') + self.assertHasAttr(Config, 'not_zealot_state') + self.assertHasAttr(Config, 'all_states') + + self.assertTrue(callable(Config.initialize_states)) + self.assertTrue(callable(Config.propagate)) + self.assertTrue(callable(Config.mutate)) + + def test_config_basic_attributes_values(self): + self.maxDiff = None + config = Config(DummyParser(), ArgumentDict()) + + test_parser = DummyParser(district_sizes=[75 for _ in range(25)], mass_media=0.5) + + for action in parser._actions: + if action.dest != 'help': + self.assertEqual(config.__getattribute__(action.dest), test_parser.__getattribute__(action.dest), + msg=f'Attribute {action.dest} different than expected') + + self.assertEqual(config.zealot_state, 'a') + self.assertEqual(config.not_zealot_state, 'b') + self.assertEqual(config.all_states, ['a', 'b']) + + self.assertTrue(callable(config.initialize_states)) + self.assertTrue(callable(config.propagate)) + self.assertTrue(callable(config.mutate)) + + def test_config_attributes_values_alternative_sys(self): + self.maxDiff = None + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2, 'seats': [2]}, + {'name': 'two', 'type': 'merge', 'dist_merging': [1], 'threshold': 0.3}, + {'name': 'three', 'type': 'merge', 'seat_rule': 'droop', 'dist_merging': [1] * 20 + [2] * 5, 'seats': [3]}, + ] + config = Config(input_parser, ArgumentDict()) + + self.assertListEqual(list(config.voting_systems.keys()), + ['country-wide_system', 'main_district_system', 'one', 'two', 'three']) + self.assertDictEqual(config.alternative_systems[0], + {'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2, + 'seat_alloc_function': seat_assignment_rules['hare'], 'seats': [2], + 'seats_per_district': [2] * 25, 'total_seats': 50}) + self.assertDictEqual(config.alternative_systems[1], + {'name': 'two', 'type': 'merge', 'seat_rule': config.seat_rule, + 'seat_alloc_function': config.seat_alloc_function, 'threshold': 0.3, 'q': 1, + 'total_seats': config.total_seats, 'seats': None, 'dist_merging': [1]}) + self.assertDictEqual(config.alternative_systems[2], + {'name': 'three', 'type': 'merge', 'seat_rule': 'droop', + 'seat_alloc_function': seat_assignment_rules['droop'], 'threshold': config.threshold, + 'q': 2, 'total_seats': 75, 'dist_merging': [1] * 20 + [2] * 5, 'seats': [3], + 'seats_per_district': [3] * 25}) + + def test_config_attributes_values_alternative_sys_error_no_name(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2}, + ] + self.assertRaises(KeyError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_no_type(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'seat_rule': 'hare'}, + ] + self.assertRaises(KeyError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_name_duplicate(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2}, + {'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2}, + ] + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_threshold(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': -0.2}, + ] + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_seat_rule(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'basic', 'seat_rule': 'wrong_seat_rule'}, + ] + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_seats(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'basic', 'seats': []}, + ] + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_no_dist_merging(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'merge'}, + ] + self.assertRaises(KeyError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_wrong_dist_merging(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'merge', 'dist_merging': 1}, + ] + self.assertRaises(KeyError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_wrong_dist_merging2(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'merge', 'dist_merging': []}, + ] + self.assertRaises(KeyError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_alternative_sys_error_too_short_dist_merging(self): + input_parser = DummyParser() + input_parser.alternative_systems = [ + {'name': 'one', 'type': 'merge', 'dist_merging': [1, 2, 3]}, + ] + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_basic_attributes_values_config_file(self): + input_parser = DummyParser() + input_parser.config_file = MockConfigFile() + config = Config(input_parser, ArgumentDict()) + self.assertEqual(config.config_file, 'mock config file') + self.assertEqual(config.avg_deg, 5) + self.assertEqual(config.num_parties, 4) + self.assertEqual(config.propagation, 'majority') + self.assertEqual(config.propagate, majority_propagation) + self.assertEqual(config.seat_rule, 'hare') + self.assertEqual(config.therm_time, 15654) + + def test_config_basic_attributes_values_config_file_threshold_error(self): + input_parser = DummyParser() + input_parser.config_file = MockConfigFile(threshold=1.1) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_threshold_too_low(self): + input_parser = DummyParser(threshold=-1) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_threshold_too_high(self): + input_parser = DummyParser(threshold=1.5) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_district_sizes_too_few(self): + input_parser = DummyParser(district_sizes=[1800, 75]) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_district_sizes_too_many(self): + input_parser = DummyParser(district_sizes=[25]*75) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_district_sizes_wrong_sum(self): + input_parser = DummyParser(district_sizes=[10]*25) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_n_q_wrong(self): + input_parser = DummyParser(n=1876) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_district_coords_too_few(self): + input_parser = DummyParser(district_coords=[[1, 2], [3, 4]]) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_district_coords_too_many(self): + input_parser = DummyParser(district_coords=[[1, 2]]*26) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_parties_too_few(self): + input_parser = DummyParser(num_parties=1) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_consensus(self): + input_parser = DummyParser(consensus=True) + config = Config(input_parser, ArgumentDict()) + self.assertEqual(config.initialize_states, consensus_initial_state) + + def test_config_attributes_values_zealots_degree(self): + input_parser = DummyParser(where_zealots='degree', zealots_district=223) + config = Config(input_parser, ArgumentDict()) + self.assertDictEqual(config.zealots_config, {'degree_driven': True, 'one_district': False, 'district': None}) + + def test_config_attributes_values_zealots_dist(self): + input_parser = DummyParser(where_zealots='district', zealots_district=12) + config = Config(input_parser, ArgumentDict()) + self.assertDictEqual(config.zealots_config, {'degree_driven': False, 'one_district': True, 'district': 12}) + + def test_config_attributes_values_zealots_random(self): + input_parser = DummyParser(where_zealots='random') + config = Config(input_parser, ArgumentDict()) + self.assertDictEqual(config.zealots_config, {'degree_driven': False, 'one_district': False, 'district': None}) + + def test_config_attributes_values_mass_media_too_low(self): + input_parser = DummyParser(mass_media=-0.1) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_mass_media_too_high(self): + input_parser = DummyParser(mass_media=1.1) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_seats_empty(self): + input_parser = DummyParser(seats=[]) + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + def test_config_attributes_values_wrong_seat_rule(self): + input_parser = DummyParser(seat_rule='wrong seat rule 123') + self.assertRaises(ValueError, Config, input_parser, ArgumentDict()) + + +if __name__ == '__main__': + unittest.main() diff --git a/main.py b/main.py index f4f0186..d147019 100644 --- a/main.py +++ b/main.py @@ -29,7 +29,7 @@ def run_experiment(n=None, epsilon=None, sample_size=None, therm_time=None, n_ze ratio=config.ratio, planar_const=config.planar_c, euclidean=config.euclidean, state_generator=config.initialize_states, random_dist=config.random_dist, initial_state=config.not_zealot_state, all_states=config.all_states) - init_g = add_zealots(init_g, n_zealots, zealot_state=config.zealot_state, **config.zealots_config) + init_g = add_zealots(init_g, n_zealots, config.zealot_state, **config.zealots_config) if not silent: link_fraction, link_ratio = compute_edge_ratio(init_g) @@ -55,7 +55,7 @@ def run_experiment(n=None, epsilon=None, sample_size=None, therm_time=None, n_ze g.vs()["state"] = config.initialize_states(n, all_states=config.all_states, state=config.not_zealot_state) # we have to reset zealots, otherwise they would have states different than 'zealot_state' g.vs()["zealot"] = np.zeros(n) - g = add_zealots(g, n_zealots, zealot_state=config.zealot_state, **config.zealots_config) + g = add_zealots(g, n_zealots, config.zealot_state, **config.zealots_config) g = run_thermalization_simple(config, g, epsilon, therm_time, n=n) g = run_simulation(config, g, epsilon, n * config.mc_steps, n=n) diff --git a/net_generation/base.py b/net_generation/base.py index b61cac8..b5e2885 100644 --- a/net_generation/base.py +++ b/net_generation/base.py @@ -134,16 +134,16 @@ def init_graph(n, block_sizes, avg_deg, block_coords=None, ratio=None, planar_co # # ########################################################### -def add_zealots(g, m, one_district=False, district=None, degree_driven=False, zealot_state='a'): +def add_zealots(g, m, zealot_state, one_district=False, district=None, degree_driven=False): """ Function creating zealots in the network. Overwrite as you wish. :param g: ig.Graph() object :param m: number of zealots + :param zealot_state: state to assign for zealots :param one_district: boolean, whether to add them to one district or randomly :param district: if one_district==True, which district to choose? If 'None', district is chosen randomly :param degree_driven: if True choose nodes proportionally to the degree - :param zealot_state: state to assign for zealots :return: ig.Graph() object """ if one_district: @@ -156,6 +156,7 @@ def add_zealots(g, m, one_district=False, district=None, degree_driven=False, ze ids = np.random.choice(g.vcount(), size=m, replace=False, p=deg_prob) else: ids = np.random.choice(g.vcount(), size=m, replace=False) + if len(ids): # apparently igraph can understand only python integers as node ids, # list comprehension below is not the most optimal way to obtain a list of python integers, @@ -164,4 +165,5 @@ def add_zealots(g, m, one_district=False, district=None, degree_driven=False, ze zealots = [int(_id) for _id in ids] g.vs[zealots]['zealot'] = 1 g.vs[zealots]['state'] = zealot_state + return g diff --git a/net_generation/tests/base_net_generation_tests.py b/net_generation/tests/base_net_generation_tests.py index e3c2ddb..018825d 100644 --- a/net_generation/tests/base_net_generation_tests.py +++ b/net_generation/tests/base_net_generation_tests.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- import unittest import numpy as np +import igraph as ig +from collections import Counter -from net_generation.base import default_initial_state, init_graph, planted_affinity, planar_affinity +from net_generation.base import default_initial_state, consensus_initial_state, add_zealots +from net_generation.base import init_graph, planted_affinity, planar_affinity class TestNetworkGeneration(unittest.TestCase): @@ -12,6 +15,14 @@ def test_default_initial_state(self): self.assertEqual(len(res), 2000) self.assertSetEqual(set(res), {'a', 'b', 'c'}) + def test_consensus_initial_state(self): + res = consensus_initial_state(1000, ('a', 'b', 'c'), state='b') + self.assertEqual(res, ['b' for _ in range(1000)]) + + def test_consensus_initial_state_no_state(self): + res = consensus_initial_state(9999, ('a', 'b', 'c', 'd')) + self.assertEqual(res, ['d' for _ in range(9999)]) + def test_planted_affinity(self): n = 100 q = 2 @@ -67,6 +78,38 @@ def test_func(): all_states=['a', 'b']) self.assertRaises(TypeError, test_func) + def test_init_graph_node_attributes(self): + n = 1300 + avg_deg = 10.0 + init_g = init_graph(n, [600, 400, 300], avg_deg, ratio=0.01, all_states=['a', 'b', 'd']) + self.assertIsInstance(init_g, ig.Graph) + self.assertSetEqual(set(init_g.vs['state']), {'a', 'b', 'd'}) + self.assertSetEqual(set(init_g.vs['zealot']), {0}) + self.assertListEqual(init_g.vs[0:600]['district'], [0 for _ in range(600)]) + self.assertListEqual(init_g.vs[600:1000]['district'], [1 for _ in range(400)]) + self.assertListEqual(init_g.vs[1000:1300]['district'], [2 for _ in range(300)]) + + def test_add_zealots_random(self): + g = ig.Graph(20) + g.vs['state'] = 'c' + g.vs['zealot'] = 0 + g = add_zealots(g, 15, 'a') + self.assertDictEqual(Counter(g.vs['state']), {'a': 15, 'c': 5}) + self.assertDictEqual(Counter(g.vs['zealot']), {1: 15, 0: 5}) + + def test_add_zealots_one_dist(self): + g = ig.Graph(30) + g.vs['state'] = 'b' + g.vs['zealot'] = 0 + g.vs[0:6]['district'] = 0 + g.vs[6:11]['district'] = 1 + g.vs[11:30]['district'] = 2 + g = add_zealots(g, 3, 'c', one_district=True, district=1) + self.assertDictEqual(Counter(g.vs[6:11]['state']), {'b': 2, 'c': 3}) + self.assertDictEqual(Counter(g.vs[6:11]['zealot']), {1: 3, 0: 2}) + self.assertDictEqual(Counter(g.vs['state']), {'b': 27, 'c': 3}) + self.assertDictEqual(Counter(g.vs['zealot']), {1: 3, 0: 27}) + if __name__ == '__main__': unittest.main() diff --git a/scripts/animation.py b/scripts/animation.py index 614bb9d..c9b27f6 100644 --- a/scripts/animation.py +++ b/scripts/animation.py @@ -37,7 +37,7 @@ def make_animation(config): ratio=config.ratio, planar_const=config.planar_c, euclidean=config.euclidean, state_generator=config.initialize_states, random_dist=config.random_dist, initial_state=config.not_zealot_state, all_states=config.all_states) - graph = add_zealots(graph, config.n_zealots, zealot_state=config.zealot_state, **config.zealots_config) + graph = add_zealots(graph, config.n_zealots, config.zealot_state, **config.zealots_config) link_fraction, link_ratio = compute_edge_ratio(graph) log.info(f'There is {str(round(100.0 * link_fraction, 1))}% of inter-district connections') diff --git a/simulation/base.py b/simulation/base.py index 2bdba15..72fc1a8 100644 --- a/simulation/base.py +++ b/simulation/base.py @@ -22,7 +22,7 @@ def default_mutation(node, all_states, p): noise is applied. :param node: the node for which a new state is generated because of noise :param all_states: possible states of nodes - :param p: probability of switching to state 'a' - mass media effect + :param p: probability of switching to state 'a' (i.e. to the first state in 'all_states') - mass media effect :result: the new mutated state for the node """ k = len(all_states) - 1 @@ -63,7 +63,7 @@ def majority_propagation(node, g, inverse=False): """ neighbours = g.neighbors(node) if neighbours: - c = Counter([g.vs[n]["state"] for n in neighbours]) + c = Counter(g.vs[neighbours]["state"]) counts = c.most_common() if inverse: max_count = counts[-1][1] diff --git a/simulation/tests/base_simulation_tests.py b/simulation/tests/base_simulation_tests.py index 3c61983..5881640 100644 --- a/simulation/tests/base_simulation_tests.py +++ b/simulation/tests/base_simulation_tests.py @@ -1,27 +1,202 @@ # -*- coding: utf-8 -*- import unittest import igraph as ig +from decimal import Decimal +from collections import Counter +from unittest.mock import patch -from simulation.base import default_mutation +from simulation.base import default_mutation, default_propagation, majority_propagation +from simulation.base import run_simulation, run_thermalization, run_thermalization_simple + + +class TestGraphNine(ig.Graph): + """ + this test graph is design to have 9 nodes, so initialize it like 'TestGraphNine(9)' + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.vs['state'] = 'b' + self.vs[2]['state'] = 'a' + self.vs[6]['state'] = 'c' + self.vs[7]['state'] = 'c' + self.vs[8]['state'] = 'c' + self.vs[0]['state'] = 'c' + self.vs['zealot'] = 0 + self.add_edge(1, 2) + self.add_edge(2, 3) + self.add_edge(2, 4) + self.add_edge(2, 5) + self.add_edge(4, 7) + self.add_edge(5, 6) + self.add_edge(5, 8) + self.add_edge(6, 8) + + +class Configuration: + """ + dummy configuration with propagate and mutate methods and thermalization + """ + all_states = None + mass_media = None + total_seats = None + seat_alloc_function = None + + @staticmethod + def propagate(node, g): + return 'abcdef' + + @staticmethod + def mutate(node, all_states, p): + # to check if all_states and mass_media are being passed correctly in run_simulation + return all_states + 'ghijk' + p + + +def eq_seat_assign(total_seats, vote_fractions=None, votes=None, total_votes=None): + return {party: int(total_seats / len(vote_fractions.keys())) for party in votes.keys()} class TestSimulationBase(unittest.TestCase): def test_default_mutation_one(self): graph = ig.Graph.Full(3) - res = default_mutation(graph.vs(1), ('a', 'b', 'c'), 1.0/3) + res = default_mutation(graph.vs[1], ('a', 'b', 'c'), 1.0/3) self.assertIn(res, ('a', 'b', 'c')) def test_default_mutation_two(self): graph = ig.Graph.Full(3) - res = default_mutation(graph.vs(1), ('a', 'b', 'c'), 1.0) + res = default_mutation(graph.vs[1], ('a', 'b', 'c'), 1.0) self.assertEqual(res, 'a') def test_default_mutation_three(self): graph = ig.Graph.Full(3) - res = default_mutation(graph.vs(1), ('a', 'b', 'c'), 0.0) + res = default_mutation(graph.vs[1], ('a', 'b', 'c'), 0.0) self.assertIn(res, ('b', 'c')) + def test_default_propagation(self): + g = TestGraphNine(9) + state = default_propagation(g.vs[2], g) # all neighbours are 'b' + self.assertEqual(state, 'b') + state = default_propagation(g.vs[3], g) # the only neighbour is 'a' + self.assertEqual(state, 'a') + + def test_default_propagation_no_neighbours(self): + g = ig.Graph(3) + g.vs['state'] = ['a', 'b', 'c'] + state = default_propagation(g.vs[1], g) + self.assertEqual(state, 'b') + + def test_majority_propagation(self): + g = TestGraphNine(9) + state = majority_propagation(g.vs[2], g) # all neighbours are 'b' + self.assertEqual(state, 'b') + state = majority_propagation(g.vs[3], g) # the only neighbour is 'a' + self.assertEqual(state, 'a') + state = majority_propagation(g.vs[5], g) # two neighbours are 'c' and one is 'a' + self.assertEqual(state, 'c') + state = majority_propagation(g.vs[8], g) # one neighbour is 'c' and one is 'b' + self.assertIn(state, ['b', 'c']) + + def test_majority_propagation_inverse(self): + g = TestGraphNine(9) + state = majority_propagation(g.vs[2], g, inverse=True) # all neighbours are 'b' + self.assertEqual(state, 'b') + state = majority_propagation(g.vs[3], g, inverse=True) # the only neighbour is 'a' + self.assertEqual(state, 'a') + state = majority_propagation(g.vs[5], g, inverse=True) # two neighbours are 'c' and one is 'a' + self.assertEqual(state, 'a') + state = majority_propagation(g.vs[8], g, inverse=True) # one neighbour is 'c' and one is 'b' + self.assertIn(state, ['b', 'c']) + + def test_majority_propagation_no_neighbours(self): + g = ig.Graph(3) + g.vs['state'] = ['a', 'b', 'c'] + state = majority_propagation(g.vs[1], g) + self.assertEqual(state, 'b') + + def test_run_simulation_propagate(self): + noise_rate = 0 + g = TestGraphNine(9) + config = Configuration() + g = run_simulation(config, g, noise_rate, 1, n=9) + self.assertIn('abcdef', g.vs['state']) + self.assertEqual(Counter(g.vs['state'])['abcdef'], 1) + + def test_run_simulation_propagate_no_n(self): + noise_rate = 0 + g = TestGraphNine(9) + config = Configuration() + g = run_simulation(config, g, noise_rate, 1) + self.assertIn('abcdef', g.vs['state']) + self.assertEqual(Counter(g.vs['state'])['abcdef'], 1) + + def test_run_simulation_mutate(self): + noise_rate = 1 + g = TestGraphNine(9) + config = Configuration() + config.all_states = 'qqq' + config.mass_media = 'ccc' + g = run_simulation(config, g, noise_rate, 1, n=9) + self.assertIn('qqqghijkccc', g.vs['state']) + self.assertEqual(Counter(g.vs['state'])['qqqghijkccc'], 1) + + def test_run_simulation_steps_count(self): + global steps_count + steps_count = 0 + + def one_step(*args, **kwargs): + global steps_count + steps_count += 1 + return 'a' + + noise_rate = 0.5 + g = TestGraphNine(9) + config = Configuration() + config.mutate = one_step + config.propagate = one_step + + g = run_simulation(config, g, noise_rate, 124, n=9) + self.assertEqual(steps_count, 124) + + def test_run_simulation_zealots(self): + noise_rate = 0.5 + g = TestGraphNine(9) + g.vs['zealot'] = 1 + config = Configuration() + g = run_simulation(config, g, noise_rate, 100, n=9) + self.assertListEqual(g.vs['state'], ['c', 'b', 'a', 'b', 'b', 'b', 'c', 'c', 'c']) + + @patch('simulation.base.run_simulation') + def test_run_thermalization_simple(self, mocked_run): + noise_rate = 0.2 + g = TestGraphNine(9) + config = Configuration() + run_thermalization_simple(config, g, noise_rate, 1, n=9) + mocked_run.assert_called_once_with(config, g, noise_rate, 1, n=9) + + @patch('simulation.base.run_simulation') + def test_run_thermalization(self, mocked_run): + # this test relies on electoral_sys.electoral_system.single_district_voting(), + # because it can not be mocked without doing black magic, because it's decorated + noise_rate = 0.2 + g = TestGraphNine(9) + g.add_vertex() + g.vs[0:5]['state'] = 'a' + g.vs[5:10]['state'] = 'b' + + mocked_run.return_value = g + + config = Configuration() + config.all_states = ['a', 'b'] + config.total_seats = 15 + config.seat_alloc_function = eq_seat_assign + + res_g, trajectory = run_thermalization(config, g, noise_rate, 5000, n=9) + mocked_run.assert_called_with(config, g, noise_rate, 1000, n=9) + self.assertEqual(mocked_run.call_count, 5) + self.assertEqual(res_g, g) + self.assertDictEqual(trajectory, {'a': [Decimal('0.5')] * 6, + 'b': [Decimal('0.5')] * 6}) + if __name__ == '__main__': unittest.main()