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 step1(): example1 = circus.Circus(name="example1", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) person = example1.create_population( name="person", size=1000, ids_gen=SequencialGenerator(prefix="PERSON_")) hello_world = example1.create_story( name="hello_world", initiating_population=person, member_id_field="PERSON_ID", # after each story, reset the timer to 0, so that it will get # executed again at the next clock tick (next hour) timer_gen=ConstantDependentGenerator(value=0)) hello_world.set_operations( ConstantGenerator(value="hello world").ops.generate(named_as="HELLO"), FieldLogger(log_id="hello")) example1.run(duration=pd.Timedelta("48h"), log_output_folder="output/example1", delete_existing_logs=True) with open("output/example1/hello.csv") as f: print("Logged {} lines".format(len(f.readlines()) - 1))
def step4(): """ Woah, this got drastically slower """ example1 = circus.Circus(name="example1", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) person = example1.create_population( name="person", size=1000, ids_gen=SequencialGenerator(prefix="PERSON_")) person.create_attribute("NAME", init_gen=FakerGenerator(method="name", seed=next( example1.seeder))) sites = SequencialGenerator(prefix="SITE_").generate(1000) random_site_gen = NumpyRandomGenerator(method="choice", a=sites, seed=next(example1.seeder)) allowed_sites = person.create_relationship(name="sites") for i in range(5): allowed_sites \ .add_relations(from_ids=person.ids, to_ids=random_site_gen.generate(person.size)) hello_world = example1.create_story( name="hello_world", initiating_population=person, member_id_field="PERSON_ID", # after each story, reset the timer to 0, so that it will get # executed again at the next clock tick (next hour) timer_gen=ConstantDependentGenerator(value=0)) duration_gen = NumpyRandomGenerator(method="exponential", scale=60, seed=next(example1.seeder)) hello_world.set_operations( person.ops.lookup(id_field="PERSON_ID", select={"NAME": "NAME"}), ConstantGenerator(value="hello world").ops.generate(named_as="HELLO"), duration_gen.ops.generate(named_as="DURATION"), allowed_sites.ops.select_one(from_field="PERSON_ID", named_as="SITE"), example1.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="hello")) example1.run(duration=pd.Timedelta("48h"), log_output_folder="output/example1", delete_existing_logs=True) with open("output/example1/hello.csv") as f: print("Logged {} lines".format(len(f.readlines()) - 1))
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 add_dealer_bulk_sim_purchase_story(self, dealers, distributors): """ Adds a SIM purchase story from agents to dealer, with impact on stock of both populations """ logging.info("Creating bulk purchase story") timegen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) purchase_activity_gen = ConstantGenerator(value=100) build_purchases = self.create_story(name="bulk_purchases", initiating_population=dealers, member_id_field="DEALER_ID", timer_gen=timegen, activity_gen=purchase_activity_gen) build_purchases.set_operations( self.clock.ops.timestamp(named_as="DATETIME"), dealers.get_relationship("DISTRIBUTOR").ops.select_one( from_field="DEALER_ID", named_as="DISTRIBUTOR"), dealers.ops.lookup(id_field="DEALER_ID", select={"BULK_BUY_SIZE": "BULK_BUY_SIZE"}), distributors.get_relationship("SIM").ops.select_many( from_field="DISTRIBUTOR", named_as="SIM_BULK", quantity_field="BULK_BUY_SIZE", # if a SIM is selected, it is removed from the dealer's stock pop=True # (not modeling out-of-stock provider to keep the example simple... ), dealers.get_relationship("SIM").ops.add_grouped( from_field="DEALER_ID", grouped_items_field="SIM_BULK"), # not modeling money transfer to keep the example simple... # just logging the number of sims instead of the sims themselves... operations.Apply(source_fields="SIM_BULK", named_as="NUMBER_OF_SIMS", f=lambda s: s.map(len), f_args="series"), # finally, triggering some re-stocking by the distributor distributors.get_attribute("SIMS_TO_RESTOCK").ops.add( member_id_field="DISTRIBUTOR", added_value_field="NUMBER_OF_SIMS"), self.get_story("distributor_restock").ops.force_act_next( member_id_field="DISTRIBUTOR"), operations.FieldLogger( log_id="bulk_purchases", cols=["DEALER_ID", "DISTRIBUTOR", "NUMBER_OF_SIMS"]), )
def add_survey_action(circus): logging.info(" creating field agent survey action") field_agents = circus.actors["field_agents"] # Surveys only happen during work hours survey_timer_gen = WorkHoursTimerGenerator(clock=circus.clock, seed=next(circus.seeder)) min_activity = survey_timer_gen.activity( n=10, per=pd.Timedelta("7 days"), ) max_activity = survey_timer_gen.activity( n=100, per=pd.Timedelta("7 days"), ) survey_activity_gen = NumpyRandomGenerator(method="choice", a=np.arange( min_activity, max_activity), seed=next(circus.seeder)) survey_action = circus.create_story(name="pos_surveys", initiating_actor=field_agents, actorid_field="FA_ID", timer_gen=survey_timer_gen, activity_gen=survey_activity_gen) survey_action.set_operations( field_agents.ops.lookup(id_field="FA_ID", select={"CURRENT_SITE": "SITE"}), # TODO: We should select a POS irrespectively of the relationship weight circus.actors["sites"].get_relationship("POS").ops.select_one( from_field="SITE", named_as="POS_ID", # a field agent in a location without a POS won't serve any discard_empty=True), circus.actors["pos"].ops.lookup(id_field="POS_ID", select={ "LATITUDE": "POS_LATITUDE", "LONGITUDE": "POS_LONGITUDE", "AGENT_NAME": "POS_NAME", }), SequencialGenerator(prefix="TASK").ops.generate(named_as="TASK_ID"), ConstantGenerator(value="Done").ops.generate(named_as="STATUS"), circus.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="pos_surveys", cols=[ "TASK_ID", "FA_ID", "POS_ID", "POS_NAME", "POS_LATITUDE", "POS_LONGITUDE", "TIME", "STATUS" ]))
def add_telcos(circus, params, distributor_id_gen): logging.info("creating telcos") telcos = circus.create_population(name="telcos", size=params["n_telcos"], ids_gen=distributor_id_gen) for product, description in params["products"].items(): logging.info("generating telco initial {} stock".format(product)) init_stock_size = params["n_customers"] * description[ "telco_init_stock_customer_ratio"] product_id_gen = circus.generators["{}_id_gen".format(product)] int_stock_gen = ConstantGenerator(value=init_stock_size)\ .flatmap(DependentBulkGenerator(element_generator=product_id_gen)) telcos.create_stock_relationship_grp(name=product, stock_bulk_gen=int_stock_gen)
def run_test_scenario_1(clock_step, simulation_duration, n_stories, per, log_folder): circus = Circus(name="tested_circus", master_seed=1, start=pd.Timestamp("8 June 2016"), step_duration=pd.Timedelta(clock_step)) population = circus.create_population(name="a", size=1000, ids_gen=SequencialGenerator( max_length=3, prefix="id_")) daily_profile = CyclicTimerGenerator( clock=circus.clock, config=CyclicTimerProfile(profile=[1] * 24, profile_time_steps="1h", start_date=pd.Timestamp("8 June 2016")), seed=1234) # each of the 500 populations have a constant 12 logs per day rate activity_gen = ConstantGenerator( value=daily_profile.activity(n=n_stories, per=per)) # just a dummy operation to produce some logs story = circus.create_story(name="test_story", initiating_population=population, member_id_field="some_id", timer_gen=daily_profile, activity_gen=activity_gen) story.set_operations(circus.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="the_logs")) circus.run(duration=pd.Timedelta(simulation_duration), log_output_folder=log_folder)
person.create_attribute("age", init_gen=NumpyRandomGenerator( method="normal", loc=3, scale=5, seed=next(example_circus.seeder))) return example_circus example = create_circus_with_population() hello_world = example.create_story( name="hello_world", initiating_population=example.populations["person"], member_id_field="PERSON_ID", timer_gen=ConstantDependentGenerator(value=1)) hello_world.set_operations( ConstantGenerator(value="hello world").ops.generate(named_as="MESSAGE"), FieldLogger(log_id="hello")) example.run(duration=pd.Timedelta("48h"), log_output_folder="output/example3", delete_existing_logs=True) with open("output/example3/hello.csv") as log: logging.info("some produced logs: \n\n" + "".join(log.readlines(1000)[:10]))
def __init__(self, name, initiating_population, member_id_field, activity_gen=ConstantGenerator(value=1.), states=None, timer_gen=ConstantDependentGenerator(value=-1), auto_reset_timer=True): """ :param name: name of this story :param initiating_population: population from which the operations of this story are started :param member_id_field: when building the story data, a field will be automatically inserted containing the member ids, with this name :param activity_gen: generator for the default activity levels of the population members for this story. Default: same level for everybody :param states: dictionary of states providing activity level for other states of the population + a probability level to transit back to the default state after each execution (NOT after each clock tick). Default: no supplementary states. :param timer_gen: timer generator: this must be a generator able to generate new timer values based on the activity level. Default: no such generator, in which case the timer never triggers this story. :param auto_reset_timer: if True, we automatically re-schedule a new execution for the same member id after at the end of the previous ont, by resetting the timer. """ self.name = name self.triggering_population = initiating_population self.member_id_field = member_id_field self.size = initiating_population.size self.time_generator = timer_gen self.auto_reset_timer = auto_reset_timer self.forced_to_act_next = pd.Series() # activity and transition probability parameters, for each state self.params = pd.DataFrame({("default", "activity"): 0}, index=initiating_population.ids) default_state = { "default": { "activity": activity_gen, "back_to_default_probability": ConstantGenerator(value=1.), } } for state, state_gens in merge_2_dicts(default_state, states).items(): activity_vals = state_gens["activity"].generate(size=self.size) probs_vals = state_gens["back_to_default_probability"].generate( size=self.size) self.params[("activity", state)] = activity_vals self.params[("back_to_default_probability", state)] = probs_vals # current state and timer value for each population member id self.timer = pd.DataFrame({ "state": "default", "remaining": -1 }, index=self.params.index) if self.auto_reset_timer: self.reset_timers() self.ops = self.StoryOps(self) # in case self.operations is not called, at least we have a basic # selection self.operation_chain = Chain()
def add_communications(self, subs, sims, cells): """ Adds Calls and SMS story, which in turn may trigger topups story. """ logging.info("Adding calls and sms story ") # generators for topups and call duration voice_duration_generator = NumpyRandomGenerator(method="choice", a=range(20, 240), seed=next(self.seeder)) # call and sms timer generator, depending on the day of the week call_timegen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) # probability of doing a topup, with high probability when the depended # variable (i.e. the main account value, see below) gets close to 0 recharge_trigger = DependentTriggerGenerator( value_to_proba_mapper=operations.logistic(k=-0.01, x0=1000), seed=next(self.seeder)) # call activity level, under normal and "excited" states normal_call_activity = ParetoGenerator(xmin=10, a=1.2, seed=next(self.seeder)) excited_call_activity = ParetoGenerator(xmin=100, a=1.1, seed=next(self.seeder)) # after a call or SMS, excitability is the probability of getting into # "excited" mode (i.e., having a shorted expected delay until next call excitability_gen = NumpyRandomGenerator(method="beta", a=7, b=3, seed=next(self.seeder)) subs.create_attribute(name="EXCITABILITY", init_gen=excitability_gen) # same "basic" trigger, without any value mapper flat_trigger = DependentTriggerGenerator(seed=next(self.seeder)) back_to_normal_prob = NumpyRandomGenerator(method="beta", a=3, b=7, seed=next(self.seeder)) # Calls and SMS stories themselves calls = self.create_story(name="calls", initiating_population=subs, member_id_field="A_ID", timer_gen=call_timegen, activity_gen=normal_call_activity, states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }) sms = self.create_story(name="sms", initiating_population=subs, member_id_field="A_ID", timer_gen=call_timegen, activity_gen=normal_call_activity, states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }) # common logic between Call and SMS: selecting A and B + their related # fields compute_ab_fields = Chain( self.clock.ops.timestamp(named_as="DATETIME"), # selects a B party subs.get_relationship("FRIENDS").ops.select_one(from_field="A_ID", named_as="B_ID", one_to_one=True), # fetches information about all SIMs of A and B subs.get_relationship("SIMS").ops.select_all(from_field="A_ID", named_as="A_SIMS"), sims.ops.lookup(id_field="A_SIMS", select={ "OPERATOR": "OPERATORS_A", "MSISDN": "MSISDNS_A", "MAIN_ACCT": "MAIN_ACCTS_A" }), subs.get_relationship("SIMS").ops.select_all(from_field="B_ID", named_as="B_SIMS"), sims.ops.lookup(id_field="B_SIMS", select={ "OPERATOR": "OPERATORS_B", "MSISDN": "MSISDNS_B" }), # A selects the sims and related values based on the best match # between the sims of A and B operations.Apply(source_fields=[ "MSISDNS_A", "OPERATORS_A", "A_SIMS", "MAIN_ACCTS_A", "MSISDNS_B", "OPERATORS_B", "B_SIMS" ], named_as=[ "MSISDN_A", "OPERATOR_A", "SIM_A", "MAIN_ACCT_OLD", "MSISDN_B", "OPERATOR_B", "SIM_B" ], f=select_sims), operations.Apply(source_fields=["OPERATOR_A", "OPERATOR_B"], named_as="TYPE", f=compute_cdr_type), ) # Both CELL_A and CELL_B might drop the call based on their current HEALTH compute_cell_status = Chain( # some static fields subs.ops.lookup(id_field="A_ID", select={ "CELL": "CELL_A", "EXCITABILITY": "EXCITABILITY_A" }), subs.ops.lookup(id_field="B_ID", select={ "CELL": "CELL_B", "EXCITABILITY": "EXCITABILITY_B" }), cells.ops.lookup(id_field="CELL_A", select={"HEALTH": "CELL_A_HEALTH"}), cells.ops.lookup(id_field="CELL_B", select={"HEALTH": "CELL_B_HEALTH"}), flat_trigger.ops.generate(observed_field="CELL_A_HEALTH", named_as="CELL_A_ACCEPTS"), flat_trigger.ops.generate(observed_field="CELL_B_HEALTH", named_as="CELL_B_ACCEPTS"), operations.Apply( source_fields=["CELL_A_ACCEPTS", "CELL_B_ACCEPTS"], named_as="STATUS", f=compute_call_status)) # update the main account based on the value of this CDR update_accounts = Chain( operations.Apply(source_fields=["MAIN_ACCT_OLD", "VALUE"], named_as="MAIN_ACCT_NEW", f=np.subtract, f_args="series"), sims.get_attribute("MAIN_ACCT").ops.update( member_id_field="SIM_A", copy_from_field="MAIN_ACCT_NEW"), ) # triggers the topup story if the main account is low trigger_topups = Chain( # A subscribers with low account are now more likely to topup the # SIM they just used to make a call recharge_trigger.ops.generate(observed_field="MAIN_ACCT_NEW", named_as="SHOULD_TOP_UP"), self.get_story("topups").ops.force_act_next( member_id_field="SIM_A", condition_field="SHOULD_TOP_UP"), ) # get BOTH sms and Call "bursty" after EITHER a call or an sms get_bursty = Chain( # Trigger to get into "excited" mode because A gave a call or sent an # SMS flat_trigger.ops.generate(observed_field="EXCITABILITY_A", named_as="A_GETTING_BURSTY"), calls.ops.transit_to_state(member_id_field="A_ID", condition_field="A_GETTING_BURSTY", state="excited"), sms.ops.transit_to_state(member_id_field="A_ID", condition_field="A_GETTING_BURSTY", state="excited"), # Trigger to get into "excited" mode because B received a call flat_trigger.ops.generate(observed_field="EXCITABILITY_B", named_as="B_GETTING_BURSTY"), # transiting to excited mode, according to trigger value calls.ops.transit_to_state(member_id_field="B_ID", condition_field="B_GETTING_BURSTY", state="excited"), sms.ops.transit_to_state(member_id_field="B_ID", condition_field="B_GETTING_BURSTY", state="excited"), # # B party need to have their time reset explicitally since they were # not active at this round. A party will be reset automatically calls.ops.reset_timers(member_id_field="B_ID"), sms.ops.reset_timers(member_id_field="B_ID"), ) calls.set_operations( compute_ab_fields, compute_cell_status, ConstantGenerator(value="VOICE").ops.generate(named_as="PRODUCT"), voice_duration_generator.ops.generate(named_as="DURATION"), operations.Apply(source_fields=["DURATION", "DATETIME", "TYPE"], named_as="VALUE", f=compute_call_value), update_accounts, trigger_topups, get_bursty, # final CDRs operations.FieldLogger(log_id="voice_cdr", cols=[ "DATETIME", "MSISDN_A", "MSISDN_B", "STATUS", "DURATION", "VALUE", "CELL_A", "OPERATOR_A", "CELL_B", "OPERATOR_B", "TYPE", "PRODUCT" ]), ) sms.set_operations( compute_ab_fields, compute_cell_status, ConstantGenerator(value="SMS").ops.generate(named_as="PRODUCT"), operations.Apply(source_fields=["DATETIME", "TYPE"], named_as="VALUE", f=compute_sms_value), update_accounts, trigger_topups, get_bursty, # final CDRs operations.FieldLogger(log_id="sms_cdr", cols=[ "DATETIME", "MSISDN_A", "MSISDN_B", "STATUS", "VALUE", "CELL_A", "OPERATOR_A", "CELL_B", "OPERATOR_B", "TYPE", "PRODUCT" ]), )
def create_subs_and_sims(self): """ Creates the subs and sims + a relationship between them + an agent relationship. We have at least one sim per subs: sims.size >= subs.size The sims population contains the "OPERATOR", "MAIN_ACCT" and "MSISDN" attributes. The subs population has a "SIMS" relationship that points to the sims owned by each subs. The sims population also has a relationship to the set of agents where this sim can be topped up. """ npgen = RandomState(seed=next(self.seeder)) # subs are empty here but will receive a "CELLS" and "EXCITABILITY" # attributes later on subs = self.create_population( name="subs", size=self.params["n_subscribers"], ids_gen=SequencialGenerator(prefix="SUBS_")) number_of_operators = npgen.choice(a=range(1, 5), size=subs.size) operator_ids = build_ids(size=4, prefix="OPERATOR_", max_length=1) def pick_operators(qty): """ randomly choose a set of unique operators of specified size """ return npgen.choice(a=operator_ids, p=[.8, .05, .1, .05], size=qty, replace=False).tolist() # set of operators of each subs subs_operators_list = map(pick_operators, number_of_operators) # Dataframe with 4 columns for the 1rst, 2nd,... operator of each subs. # Since subs_operators_list don't all have the size, some entries of this # dataframe contains None, which are just discarded by the stack() below subs_operators_df = pd.DataFrame(data=list(subs_operators_list), index=subs.ids) # same info, vertically: the index contains the sub id (with duplicates) # and "operator" one of the operators of this subs subs_ops_mapping = subs_operators_df.stack() subs_ops_mapping.index = subs_ops_mapping.index.droplevel(level=1) # SIM population, each with an OPERATOR and MAIN_ACCT attributes sims = self.create_population( name="sims", size=subs_ops_mapping.size, ids_gen=SequencialGenerator(prefix="SIMS_")) sims.create_attribute("OPERATOR", init_values=subs_ops_mapping.values) recharge_gen = ConstantGenerator(value=1000.) sims.create_attribute(name="MAIN_ACCT", init_gen=recharge_gen) # keeping track of the link between population and sims as a relationship sims_of_subs = subs.create_relationship("SIMS") sims_of_subs.add_relations(from_ids=subs_ops_mapping.index, to_ids=sims.ids) msisdn_gen = MSISDNGenerator( countrycode="0032", prefix_list=["472", "473", "475", "476", "477", "478", "479"], length=6, seed=next(self.seeder)) sims.create_attribute(name="MSISDN", init_gen=msisdn_gen) # Finally, adding one more relationship that defines the set of possible # shops where we can topup each SIM. # TODO: to make this a bit more realistic, we should probably generate # such relationship first from the subs to their favourite shops, and then # copy that info to each SIM, maybe with some fluctuations to account # for the fact that not all shops provide topups of all operators. agents = build_ids(self.params["n_agents"], prefix="AGENT_", max_length=3) agent_df = pd.DataFrame.from_records(make_random_bipartite_data( sims.ids, agents, 0.3, seed=next(self.seeder)), columns=["SIM_ID", "AGENT"]) logging.info(" creating random sim/agent relationship ") sims_agents_rel = sims.create_relationship("POSSIBLE_AGENTS") agent_weight_gen = NumpyRandomGenerator(method="exponential", scale=1., seed=next(self.seeder)) sims_agents_rel.add_relations(from_ids=agent_df["SIM_ID"], to_ids=agent_df["AGENT"], weights=agent_weight_gen.generate( agent_df.shape[0])) return subs, sims, recharge_gen
def step7(): example1 = circus.Circus(name="example1", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) person = example1.create_population( name="person", size=1000, ids_gen=SequencialGenerator(prefix="PERSON_")) person.create_attribute("NAME", init_gen=FakerGenerator(method="name", seed=next( example1.seeder))) person.create_attribute("POPULARITY", init_gen=NumpyRandomGenerator( method="uniform", low=0, high=1, seed=next(example1.seeder))) sites = SequencialGenerator(prefix="SITE_").generate(1000) random_site_gen = NumpyRandomGenerator(method="choice", a=sites, seed=next(example1.seeder)) allowed_sites = person.create_relationship(name="sites") # SITES ------------------ # Add HOME sites allowed_sites.add_relations(from_ids=person.ids, to_ids=random_site_gen.generate(person.size), weights=0.4) # Add WORK sites allowed_sites.add_relations(from_ids=person.ids, to_ids=random_site_gen.generate(person.size), weights=0.3) # Add OTHER sites for i in range(3): allowed_sites \ .add_relations(from_ids=person.ids, to_ids=random_site_gen.generate(person.size), weights=0.1) # FRIENDS ------------------ friends = person.create_relationship(name="friends") friends_df = pd.DataFrame.from_records( make_random_bipartite_data( person.ids, person.ids, p=0.005, # probability for a node to be connected to # another one : 5 friends on average = 5/1000 seed=next(example1.seeder)), columns=["A", "B"]) friends.add_relations(from_ids=friends_df["A"], to_ids=friends_df["B"]) # PRICE ------------------ def price(story_data): result = pd.DataFrame(index=story_data.index) result["PRICE"] = story_data["DURATION"] * 0.05 result["CURRENCY"] = "EUR" return result # STORIES ------------------ hello_world = example1.create_story( name="hello_world", initiating_population=person, member_id_field="PERSON_ID", # after each story, reset the timer to 0, so that it will get # executed again at the next clock tick (next hour) timer_gen=ConstantDependentGenerator(value=0)) duration_gen = NumpyRandomGenerator(method="exponential", scale=60, seed=next(example1.seeder)) hello_world.set_operations( person.ops.lookup(id_field="PERSON_ID", select={"NAME": "NAME"}), ConstantGenerator(value="hello world").ops.generate(named_as="HELLO"), duration_gen.ops.generate(named_as="DURATION"), friends.ops.select_one( from_field="PERSON_ID", named_as="COUNTERPART_ID", weight=person.get_attribute_values("POPULARITY"), # For people that do not have friends, it will try to find # the POPULARITY attribute of a None and crash miserably # Adding this flag will discard people that do not have friends discard_empty=True), person.ops.lookup(id_field="COUNTERPART_ID", select={"NAME": "COUNTER_PART_NAME"}), allowed_sites.ops.select_one(from_field="PERSON_ID", named_as="SITE"), allowed_sites.ops.select_one(from_field="COUNTERPART_ID", named_as="COUNTERPART_SITE"), Apply(source_fields=["DURATION", "SITE", "COUNTERPART_SITE"], named_as=["PRICE", "CURRENCY"], f=price, f_args="dataframe"), example1.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="hello")) example1.run(duration=pd.Timedelta("48h"), log_output_folder="output/example1", delete_existing_logs=True) with open("output/example1/hello.csv") as f: print("Logged {} lines".format(len(f.readlines()) - 1))
def add_agent_holidays_story(self, agents): """ Adds stories that reset to 0 the activity level of the purchases story of some populations """ logging.info("Adding 'holiday' periods for agents ") # TODO: this is a bit weird, I think what I'd need is a profiler that would # return duration (i.e timer count) with probability related to time # until next typical holidays :) # We could call this YearProfile though the internal mechanics would be # different than week and day profiler holiday_time_gen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) # TODO: we'd obviously have to adapt those weight to longer periods # thought this interface is not very intuitive # => create a method where can can specify the expected value of the # inter-event interval, and convert that into an activity holiday_start_activity = ParetoGenerator(xmin=.25, a=1.2, seed=next(self.seeder)) holiday_end_activity = ParetoGenerator(xmin=150, a=1.2, seed=next(self.seeder)) going_on_holidays = self.create_story( name="agent_start_holidays", initiating_population=agents, member_id_field="AGENT", timer_gen=holiday_time_gen, activity_gen=holiday_start_activity) returning_from_holidays = self.create_story( name="agent_stops_holidays", initiating_population=agents, member_id_field="AGENT", timer_gen=holiday_time_gen, activity_gen=holiday_end_activity, auto_reset_timer=False) going_on_holidays.set_operations( self.get_story("purchases").ops.transit_to_state( member_id_field="AGENT", state="on_holiday"), returning_from_holidays.ops.reset_timers(member_id_field="AGENT"), # just for the logs self.clock.ops.timestamp(named_as="TIME"), ConstantGenerator(value="going").ops.generate(named_as="STATES"), operations.FieldLogger(log_id="holidays"), ) returning_from_holidays.set_operations( self.get_story("purchases").ops.transit_to_state( member_id_field="AGENT", state="default"), # just for the logs self.clock.ops.timestamp(named_as="TIME"), ConstantGenerator(value="returning").ops.generate( named_as="STATES"), operations.FieldLogger(log_id="holidays"), )
def add_agent_sim_purchase_story(self, agents, dealers): """ Adds a SIM purchase story from agents to dealer, with impact on stock of both populations """ logging.info("Creating purchase story") timegen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) purchase_activity_gen = NumpyRandomGenerator(method="choice", a=range(1, 4), seed=next(self.seeder)) # TODO: if we merge profiler and generator, we could have higher probs here # based on calendar # TODO2: or not, maybe we should have a sub-operation with its own counter # to "come back to normal", instead of sampling a random variable at # each turn => would improve efficiency purchase = self.create_story(name="purchases", initiating_population=agents, member_id_field="AGENT", timer_gen=timegen, activity_gen=purchase_activity_gen, states={ "on_holiday": { "activity": ConstantGenerator(value=0), "back_to_default_probability": ConstantGenerator(value=0) } }) purchase.set_operations( self.clock.ops.timestamp(named_as="DATETIME"), agents.get_relationship("DEALERS").ops.select_one( from_field="AGENT", named_as="DEALER"), dealers.get_relationship("SIM").ops.select_one( from_field="DEALER", named_as="SOLD_SIM", # each SIM can only be sold once one_to_one=True, # if a SIM is selected, it is removed from the dealer's stock pop=True, # If a chosen dealer has empty stock, we don't want to drop the # row in story_data, but keep it with a None sold SIM, # which indicates the sale failed discard_empty=False), operations.Apply(source_fields="SOLD_SIM", named_as="FAILED_SALE", f=pd.isnull, f_args="series"), # any agent who failed to buy a SIM will try again at next round # (we could do that probabilistically as well, just add a trigger..) purchase.ops.force_act_next(member_id_field="AGENT", condition_field="FAILED_SALE"), # not specifying the logged columns => by defaults, log everything # ALso, we log the sale before dropping to failed sales, to keep operations.FieldLogger(log_id="purchases"), # only successful sales actually add a SIM to agents operations.DropRow(condition_field="FAILED_SALE"), agents.get_relationship("SIM").ops.add(from_field="AGENT", item_field="SOLD_SIM"), )
def test_constant_generator_should_produce_constant_values(): tested = ConstantGenerator(value="c") assert [] == tested.generate(size=0) assert ["c"] == tested.generate(size=1) assert ["c", "c", "c", "c", "c"] == tested.generate(size=5)
member_id_field="PERSON_ID", # after each story, reset the timer to 0, so that it will get # executed again at the next clock tick (next hour) timer_gen=story_timer_gen, activity_gen=activity_gen) #3600 seconds duration_gen = NumpyRandomGenerator(method="exponential", scale=3600, seed=next(example1.seeder)) hello_world.set_operations( person.ops.lookup(id_field="PERSON_ID", select={"NAME": "NAME"}), duration_gen.ops.generate(named_as="DURATION"), ConstantGenerator(value=1).ops.generate(named_as="LOCATION"), example1.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="dummy")) example1.run(duration=pd.Timedelta(hrs), log_output_folder="output/example1", delete_existing_logs=True) df = pd.read_csv("output/example1/dummy.csv") print(df) """ with open("output/example1/dummy.csv") as f: print("Logged {} lines".format(len(f.readlines()) - 1)) # import pandas as pd
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()