def test_overwrite_attribute(): population = Population(circus=tc, size=10, ids_gen=SequencialGenerator(prefix="u_", max_length=1)) ages = [10, 20, 40, 10, 100, 98, 12, 39, 76, 23] age_attr = population.create_attribute("age", init_values=ages) # before modification ages = age_attr.get_values(["u_0", "u_4", "u_9"]).tolist() assert ages == [10, 100, 23] story_data = pd.DataFrame({ # id of the populations to update "A_ID": ["u_4", "u_0"], # new values to copy "new_ages": [34, 30]}, # index of the story data has, in general, nothing to do with the # updated population index=["cust_1", "cust_2"] ) update = age_attr.ops.update( member_id_field="A_ID", copy_from_field="new_ages" ) _, logs = update(story_data) assert logs == {} # before modification ages = age_attr.get_values(["u_0", "u_4", "u_9"]).tolist() assert ages == [30, 34, 23]
def test_creating_an_empty_population_and_adding_attributes_later_should_be_possible( ): # empty population a = Population(circus=None, size=0) assert a.ids.shape[0] == 0 # empty attributes a.create_attribute("att1") a.create_attribute("att2") dynamically_created = pd.DataFrame( { "att1": [1, 2, 3], "att2": [11, 12, 13], }, index=["ac1", "ac2", "ac3"]) a.update(dynamically_created) assert a.ids.tolist() == ["ac1", "ac2", "ac3"] assert a.get_attribute_values("att1", ["ac1", "ac2", "ac3"]).tolist() == [1, 2, 3] assert a.get_attribute_values( "att2", ["ac1", "ac2", "ac3"]).tolist() == [11, 12, 13]
def test_initializing_attribute_from_relationship_must_have_a_value_for_all(): population = Population(circus=tc, size=5, ids_gen=SequencialGenerator( prefix="abc", max_length=1)) oneto1 = population.create_relationship("rel") oneto1.add_relations(from_ids=["abc0", "abc1", "abc2", "abc3", "abc4"], to_ids=["ta", "tb", "tc", "td", "te"]) attr = Attribute(population, init_relationship="rel") expected = pd.DataFrame({"value": ["ta", "tb", "tc", "td", "te"]}, index=["abc0", "abc1", "abc2", "abc3", "abc4"]) assert attr._table.sort_index().equals(expected)
def test_population_constructor_should_refuse_duplicated_ids(): with pytest.raises(ValueError): Population( circus=None, size=10, # these ids have duplicated values, that is not good ids=[1, 2, 3, 4, 5, 6, 7, 8, 9, 9])
def test_scenario_transiting_to_state_with_1_back_to_default_prob_should_go_back_to_normal( ): """ similar test to above, though this time we are using back_to_normal_prob = 1 => all populations should be back to "normal" state at the end of the execution """ population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # this one is slightly tricky: populations active_ids_gens = ConstantsMockGenerator(values=[np.nan] * 5 + population.ids[:5].tolist()) excited_state_gens = ConstantsMockGenerator(values=[np.nan] * 5 + ["excited"] * 5) excited_call_activity = ConstantGenerator(value=10) # this time we're forcing to stay in the transited state back_to_normal_prob = ConstantGenerator(value=1) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }, # forcing the timer of all populations to be initialized to 0 timer_gen=ConstantDependentGenerator(value=0)) story.set_operations( # first 5 population are "active" active_ids_gens.ops.generate(named_as="active_ids"), excited_state_gens.ops.generate(named_as="new_state"), # forcing a transition to "excited" state of the 5 populations story.ops.transit_to_state(member_id_field="active_ids", state_field="new_state")) # before any execution, the state should be default for all assert ["default"] * 10 == story.timer["state"].tolist() logs = story.execute() # no logs are expected as output assert logs == {} # this time, all populations should have transited back to "normal" at the end print(story.timer["state"].tolist()) assert ["default"] * 10 == story.timer["state"].tolist()
def test_updating_non_existing_population_ids_should_add_them(): population = Population(circus=tc, size=5, ids_gen=SequencialGenerator( prefix="abc", max_length=1)) tested = Attribute(population, init_values=[10, 20, 30, 40, 50]) tested.update(pd.Series([22, 1000, 44], index=["abc1", "not_yet_there", "abc3"])) assert tested.get_values(["not_yet_there", "abc0", "abc3", "abc4"]).tolist() == [1000, 10, 44, 50]
def test_added_and_read_values_in_attribute_should_be_equal(): population = Population(circus=tc, size=5, ids_gen=SequencialGenerator(prefix="abc", max_length=1)) tested = Attribute(population, init_values=[10, 20, 30, 40, 50]) tested.add(["abc1", "abc3"], [22, 44]) assert tested.get_values(["abc0", "abc1", "abc2", "abc3", "abc4"]).tolist() == [10, 20 + 22, 30, 40 + 44, 50]
def test_adding_several_times_to_the_same_from_should_pile_up(): population = Population(circus=tc, size=5, ids_gen=SequencialGenerator(prefix="abc", max_length=1)) tested = Attribute(population, init_values=[10, 20, 30, 40, 50]) tested.add(["abc1", "abc3", "abc1"], [22, 44, 10]) assert tested.get_values(["abc0", "abc1", "abc2", "abc3", "abc4"]).tolist() == [10, 20 + 22 + 10, 30, 40 + 44, 50]
def test_story_autoreset_false_and_dropping_rows_should_reset_all_timers(): # in case an story is configured not to perform an auto-reset, after one # execution: # - all executed rows should now be at -1 (dropped or not) # - all non executed rows should have gone down one tick population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen, auto_reset_timer=False) # empty operation list as initialization # simulating an operation that drop the last 2 rows story.set_operations(MockDropOp(0, 2)) # we have no auto-reset => all timers should intially be at -1 all_minus_1 = pd.Series([-1] * 10, index=population.ids) assert story.timer["remaining"].equals(all_minus_1) # executing once => should do nothing, and leave all timers at -1 story.execute() assert story.timer["remaining"].equals(all_minus_1) # triggering explicitaly the story => timers should have the hard-coded # values from the mock generator story.reset_timers() assert story.timer["remaining"].equals(init_timers) # after one execution, no population id has been selected and all counters # are decreased by 1 story.execute() assert story.timer["remaining"].equals(init_timers - 1) # this time, the last 5 should have executed, but we should not have # any timer reste => they should go to -1. # The other 5 should now be at 0, ready to execute at next step story.execute() expected_timers = pd.Series([0] * 5 + [-1] * 5, index=population.ids) assert story.timer["remaining"].equals(expected_timers) # executing once more: the previously at -1 should still be there, and the # just executed at this stage should be there too story.execute() expected_timers = pd.Series([-1] * 10, index=population.ids) assert story.timer["remaining"].equals(expected_timers)
def test_bugfix_collisions_force_act_next(): # Previously, resetting the timer of reset populations was cancelling the reset. # # We typically want to reset the timer when we have change the activity # state => we want to generate new timer values that reflect the new state. # # But force_act_next should still have priority on that: if somewhere else # we force some populations to act at the next clock step (e.g. to re-try # buying an ER or so), the fact that their activity level changed should # not cancel the retry. population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen) timer_values = story.timer["remaining"].copy() forced = pd.Index(["ac_1", "ac_3", "ac_7", "ac_8", "ac_9"]) not_forced = pd.Index(["ac_0", "ac_2", "ac_4", "ac_4", "ac_6"]) # force_act_next should only impact those ids story.force_act_next(forced) assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0] assert story.timer.loc[not_forced]["remaining"].equals( timer_values[not_forced]) # resetting the timers should not change the timers of the populations that # are being forced story.reset_timers() assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0] # Ticking the timers should not change the timers of the populations that # are being forced. # This is important for population forcing themselves to act at the next # clock # step (typical scenario for retry) => the fact of thick the clock at the # end of the execution should not impact them. story.timer_tick(population.ids) assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0] assert story.timer.loc[not_forced]["remaining"].equals( timer_values[not_forced] - 1)
def test_updated_and_read_values_in_attribute_should_be_equal(): population = Population(circus=tc, size=5, ids_gen=SequencialGenerator( prefix="abc", max_length=1)) tested = Attribute(population, init_values=[10, 20, 30, 40, 50]) tested.update(pd.Series([22, 44], index=["abc1", "abc3"])) # value of a should untouched assert tested.get_values(["abc0"]).tolist() == [10] # arbitrary order should not be impacted assert tested.get_values(["abc0", "abc3", "abc1"]).tolist() == [10, 44, 22]
def test_set_and_read_values_in_attribute_should_be_equal(): population = Population(circus=None, size=5, ids_gen=SequencialGenerator(prefix="abc", max_length=1)) tested = Attribute(population, init_values=[10, 20, 30, 40, 50]) assert tested.get_values(["abc0"]).tolist() == [10] assert tested.get_values(["abc0", "abc3", "abc1"]).tolist() == [10, 40, 20] # getting no id should return empty list assert tested.get_values([]).tolist() == []
def test_get_activity_should_be_default_by_default(): population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) story = Story(name="tested", initiating_population=population, member_id_field="") # by default, each population should be in the default state with activity 1 assert [1.] * 10 == story.get_param("activity", population.ids).tolist() assert story.get_possible_states() == ["default"]
def test_empty_story_should_do_nothing_and_not_crash(): customers = Population(circus=None, size=1000, ids_gen=SequencialGenerator(prefix="a")) empty_story = Story(name="purchase", initiating_population=customers, member_id_field="AGENT") logs = empty_story.execute() # no logs should be produced assert logs == {}
def test_insert_op_population_value_for_existing_populations_should_update_all_values( ): # same as test above but triggered as an Operation on story data # copy of dummy population that will be updated tested_population = Population(circus=None, size=10, ids_gen=SequencialGenerator(max_length=1, prefix="a_")) ages = [10, 20, 40, 10, 100, 98, 12, 39, 76, 23] tested_population.create_attribute("age", init_values=ages) city = ["a", "b", "b", "a", "d", "e", "r", "a", "z", "c"] tested_population.create_attribute("city", init_values=city) story_data = pd.DataFrame( { "the_new_age": [139, 123, 1, 2], "location": ["city_7", "city_9", "city_11", "city_10"], "updated_populations": ["a_7", "a_9", "a_11", "a_10"] }, index=["d_1", "d_2", "d_4", "d_3"]) update_op = tested_population.ops.update(id_field="updated_populations", copy_attributes_from_fields={ "age": "the_new_age", "city": "location" }) story_data_2, logs = update_op(story_data) # there should be no impact on the story data assert story_data_2.shape == (4, 3) assert sorted(story_data_2.columns.tolist()) == [ "location", "the_new_age", "updated_populations" ] # we should have 2 new populations assert tested_population.ids.shape[0] == 12 updated_age = tested_population.get_attribute_values( "age", ["a_0", "a_7", "a_9", "a_10", "a_11"]) updated_city = tested_population.get_attribute_values( "city", ["a_0", "a_7", "a_9", "a_10", "a_11"]) assert updated_age.tolist() == [10, 139, 123, 2, 1] assert updated_city.tolist() == [ "a", "city_7", "city_9", "city_10", "city_11" ]
def test_io_round_trip(): with path.tempdir() as root_dir: population = Population(circus=tc, size=5, ids_gen=SequencialGenerator(prefix="abc", max_length=1)) orig = Attribute(population, init_values=[10, 20, 30, 40, 50]) full_path = os.path.join(root_dir, "attribute.csv") orig.save_to(full_path) retrieved = Attribute.load_from(full_path) assert orig._table.equals(retrieved._table)
def test_get_activity_should_be_aligned_for_each_state(): excited_call_activity = ConstantGenerator(value=10) back_to_normal_prob = ConstantGenerator(value=.3) population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) story = Story(name="tested", initiating_population=population, member_id_field="", states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }) # by default, each population should be in the default state with activity 1 assert [1] * 10 == story.get_param("activity", population.ids).tolist() assert [1] * 10 == story.get_param("back_to_default_probability", population.ids).tolist() assert sorted(story.get_possible_states()) == ["default", "excited"] story.transit_to_state(["ac_2", "ac_5", "ac_9"], ["excited", "excited", "excited"]) # activity and probability of getting back to normal should now be updated expected_activity = [1, 1, 10, 1, 1, 10, 1, 1, 1, 10] assert expected_activity == story.get_param("activity", population.ids).tolist() # also, doing a get_param for some specific population ids should return the # correct values (was buggy if we requested sth else than the whole list) assert expected_activity[2:7] == story.get_param( "activity", population.ids[2:7]).tolist() assert [1, 10] == story.get_param("activity", population.ids[-2:]).tolist() expected_probs = [1, 1, .3, 1, 1, .3, 1, 1, 1, .3] assert expected_probs == story.get_param( "back_to_default_probability", population.ids, ).tolist()
def test_bugfix_force_populations_should_only_act_once(): population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([2] * 5 + [5] * 5, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen) recording_op = FakeRecording() story.set_operations(recording_op) forced = pd.Index(["ac_1", "ac_3", "ac_7", "ac_8", "ac_9"]) # force_act_next should only impact those ids story.force_act_next(forced) assert story.timer["remaining"].tolist() == [2, 0, 2, 0, 2, 5, 5, 0, 0, 0] story.execute() assert recording_op.last_seen_population_ids == [ "ac_1", "ac_3", "ac_7", "ac_8", "ac_9" ] print(story.timer["remaining"].tolist()) assert story.timer["remaining"].tolist() == [1, 2, 1, 2, 1, 4, 4, 5, 5, 5] recording_op.reset() story.execute() assert recording_op.last_seen_population_ids == [] assert story.timer["remaining"].tolist() == [0, 1, 0, 1, 0, 3, 3, 4, 4, 4] story.execute() assert recording_op.last_seen_population_ids == ["ac_0", "ac_2", "ac_4"] assert story.timer["remaining"].tolist() == [2, 0, 2, 0, 2, 2, 2, 3, 3, 3]
def test_io_round_trip(): with path.tempdir() as p: population_path = os.path.join(p, "test_location") dummy_population.save_to(population_path) retrieved = Population.load_from(circus=None, folder=population_path) assert dummy_population.size == retrieved.size assert dummy_population.ids.tolist() == retrieved.ids.tolist() ids = dummy_population.ids.tolist() for att_name in dummy_population.attribute_names(): assert dummy_population.get_attribute_values(att_name, ids).equals( retrieved.get_attribute_values(att_name, ids)) for rel_name in dummy_population.relationship_names(): assert dummy_population.get_relationship(rel_name)._table.equals( retrieved.get_relationship(rel_name)._table)
def test_active_inactive_ids_should_mark_all_populations_active_when_all_timers_0( ): population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([0] * 10, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen, auto_reset_timer=True) assert (population.ids.tolist(), []) == story.active_inactive_ids()
def test_story_autoreset_true_and_dropping_rows_should_reset_all_timers(): # in case an story is configured to perform an auto-reset, but also # drops some rows, after one execution, # - all executed rows (dropped or not) should have a timer back to some # positive value # - all non executed rows should have gone down one tick population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen, auto_reset_timer=True) # simulating an operation that drop the last 2 rows story.set_operations(MockDropOp(0, 2)) # initial timers should be those provided by the generator assert story.timer["remaining"].equals(init_timers) # after one execution, no population id has been selected and all counters # are decreased by 1 story.execute() assert story.timer["remaining"].equals(init_timers - 1) # this time, the last 5 should have executed => and the last 3 of them # should have been dropped. Nonetheless, all 5 of them should be back to 1 story.execute() expected_timers = pd.Series([0] * 5 + [1] * 5, index=population.ids) assert story.timer["remaining"].equals(expected_timers)
def test_story_autoreset_true_not_dropping_rows_should_reset_all_timers(): # in case an story is configured to perform an auto-reset, after one # execution, # - all executed rows should have a timer back to some positive value # - all non executed rows should have gone down one tick population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # 5 populations should trigger in 2 ticks, and 5 more init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids) timers_gen = MockTimerGenerator(init_timers) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", # forcing the timer of all populations to be initialized to 0 timer_gen=timers_gen, auto_reset_timer=True) # empty operation list as initialization story.set_operations(Operation()) # initial timers should be those provided by the generator assert story.timer["remaining"].equals(init_timers) # after one execution, no population id has been selected and all counters # are decreased by 1 story.execute() assert story.timer["remaining"].equals(init_timers - 1) # this time, the last 5 should have executed => go back up to 1. The # other 5 should now be at 0, ready to execute at next step story.execute() expected_timers = pd.Series([0] * 5 + [1] * 5, index=population.ids) assert story.timer["remaining"].equals(expected_timers)
def create_random_cells(self, n_cells): """ Creation of a basic population for cells, with latitude and longitude """ cells = Population(size=n_cells) latitude_generator = FakerGenerator(method="latitude", seed=next(self.seeder)) longitude_generator = FakerGenerator(method="longitude", seed=next(self.seeder)) cells.create_attribute("latitude", init_gen=latitude_generator) cells.create_attribute("longitude", init_gen=longitude_generator) return cells
def test_populations_with_zero_activity_should_never_have_positive_timer(): population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) story = Story( name="tested", initiating_population=population, # fake generator that assign zero activity to 3 populations activity_gen=ConstantsMockGenerator([1, 1, 1, 1, 0, 1, 1, 0, 0, 1]), timer_gen=ConstantDependentGenerator(value=10), member_id_field="") story.reset_timers() # all non zero populations should have been through the profiler => timer to 10 # all others should be locked to -1, to reflect that activity 0 never # triggers anything expected_timers = [10, 10, 10, 10, -1, 10, 10, -1, -1, 10] assert expected_timers == story.timer["remaining"].tolist()
def test_scenario_transiting_to_state_with_0_back_to_default_prob_should_remain_there( ): """ we create an story with a transit_to_state operation and 0 probability of going back to normal => after the execution, all triggered populations should still be in that starte """ population = Population(circus=None, size=10, ids_gen=SequencialGenerator(prefix="ac_", max_length=1)) # here we are saying that some story on populations 5 to 9 is triggering a # state change on populations 0 to 4 active_ids_gens = ConstantsMockGenerator(values=[np.nan] * 5 + population.ids[:5].tolist()) excited_state_gens = ConstantsMockGenerator(values=[np.nan] * 5 + ["excited"] * 5) excited_call_activity = ConstantGenerator(value=10) # forcing to stay excited back_to_normal_prob = ConstantGenerator(value=0) story = Story( name="tested", initiating_population=population, member_id_field="ac_id", states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }, # forcing the timer of all populations to be initialized to 0 timer_gen=ConstantDependentGenerator(value=0)) story.set_operations( # first 5 population are "active" active_ids_gens.ops.generate(named_as="active_ids"), excited_state_gens.ops.generate(named_as="new_state"), # forcing a transition to "excited" state of the 5 populations story.ops.transit_to_state(member_id_field="active_ids", state_field="new_state")) # before any execution, the state should be default for all assert ["default"] * 10 == story.timer["state"].tolist() logs = story.execute() # no logs are expected as output assert logs == {} # the first 5 populations should still be in "excited", since # "back_to_normal_probability" is 0, the other 5 should not have # moved expected_state = ["excited"] * 5 + ["default"] * 5 assert expected_state == story.timer["state"].tolist()
def test_insert_population_value_for_existing_and_new_populations_should_update_and_add_values( ): # copy of dummy population that will be updated tested_population = Population(circus=None, size=10, ids_gen=SequencialGenerator(max_length=1, prefix="a_")) ages = [10, 20, 40, 10, 100, 98, 12, 39, 76, 23] tested_population.create_attribute("age", init_values=ages) city = ["a", "b", "b", "a", "d", "e", "r", "a", "z", "c"] tested_population.create_attribute("city", init_values=city) current = tested_population.get_attribute_values("age", ["a_0", "a_7", "a_9"]) assert current.tolist() == [10, 39, 23] update = pd.DataFrame( { "age": [139, 123, 54, 25], "city": ["city_7", "city_9", "city_11", "city_10"] }, index=["a_7", "a_9", "a_11", "a_10"]) tested_population.update(update) # we should have 2 new populations assert tested_population.ids.shape[0] == 12 updated_age = tested_population.get_attribute_values( "age", ["a_0", "a_7", "a_9", "a_10", "a_11"]) updated_city = tested_population.get_attribute_values( "city", ["a_0", "a_7", "a_9", "a_10", "a_11"]) assert updated_age.tolist() == [10, 139, 123, 25, 54] assert updated_city.tolist() == [ "a", "city_7", "city_9", "city_10", "city_11" ]
import path import pandas as pd import os import pytest from trumania.core.random_generators import SequencialGenerator from trumania.core.population import Population dummy_population = Population(circus=None, size=10, ids_gen=SequencialGenerator(max_length=1, prefix="id_")) ages = [10, 20, 40, 10, 100, 98, 12, 39, 76, 23] dummy_population.create_attribute("age", init_values=ages) city = ["a", "b", "b", "a", "d", "e", "r", "a", "z", "c"] dummy_population.create_attribute("city", init_values=city) # some fake story data with an index corresponding to another population # => simulates an story triggered by that other population # the column "NEIGHBOUR" contains value that point to the dummy population, with # a duplication (id2) story_data = pd.DataFrame( { "A": ["a1", "a2", "a3", "a4"], "B": ["b1", "b2", "b3", "b4"], "NEIGHBOUR": ["id_2", "id_4", "id_7", "id_2"], "COUSINS": [ ["id_2", "id_4", "id_7", "id_2"], ["id_3"],
def load_population(namespace, population_id, circus): return Population.load_from(population_folder(namespace, population_id), circus)