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 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 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 add_agent_stock_log_action(circus, params): """ Adds an action per agent, recording the daily stock level and value of pos, dist_l1 and dist_l2. All those actions contribute to the same log_id """ for agent_name in ["pos", "dist_l1", "dist_l2"]: agent = circus.actors[agent_name] for product in params["products"].keys(): product_actor = circus.actors[product] stock_ratio = 1 / product_actor.size mean_price = np.mean(params["products"][product]["item_prices"]) stock_levels_logs = circus.create_story( name="{}_{}_stock_log".format(agent_name, product), initiating_actor=agent, actorid_field="agent_id", timer_gen=ConstantDependentGenerator( value=circus.clock.n_iterations( duration=pd.Timedelta("24h")) - 1)) stock_levels_logs.append_operations( circus.clock.ops.timestamp(named_as="TIME"), # We're supposed to report the stock level of every product id # => what we actually do is counting the full stock accross # all products and report randomly on one product id, scaling # down the stock volume. # It's ok if not all product id get reported every day product_actor.ops.select_one(named_as="product_id"), agent.get_relationship(product).ops.get_neighbourhood_size( from_field="agent_id", named_as="full_stock_volume"), Apply(source_fields="full_stock_volume", named_as="stock_volume", f=lambda v: (v * stock_ratio).astype(np.int), f_args="series"), # estimate stock value based on stock volume Apply(source_fields="stock_volume", named_as="stock_value", f=lambda v: v * mean_price, f_args="series"), # The log_id (=> the resulting file name) is the same for all # actions => we just merge the stock level of all populations as # we go. I dare to find that pretty neat ^^ FieldLogger(log_id="agent_stock_log", cols=[ "TIME", "product_id", "agent_id", "stock_volume", "stock_value" ]))
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()
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 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 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 add_attractiveness_evolution_action(circus): pos = circus.actors["pos"] # once per day the attractiveness of each POS evolves according to the delta attractiveness_evolution = circus.create_story( name="attractiveness_evolution", initiating_actor=pos, actorid_field="POS_ID", # exactly one attractiveness evolution per day # caveat: all at the same time for now timer_gen=ConstantDependentGenerator( value=circus.clock.n_iterations(pd.Timedelta("1 day")))) def update_attract_base(df): base = df.apply(lambda row: row["ATTRACT_BASE"] + row["ATTRACT_DELTA"], axis=1) base = base.mask(base > 50, 50).mask(base < -50, -50) return pd.DataFrame(base, columns=["result"]) attractiveness_evolution.set_operations( pos.ops.lookup(id_field="POS_ID", select={ "ATTRACT_BASE": "ATTRACT_BASE", "ATTRACT_DELTA": "ATTRACT_DELTA", }), Apply( source_fields=["ATTRACT_BASE", "ATTRACT_DELTA"], named_as="NEW_ATTRACT_BASE", f=update_attract_base, ), pos.get_attribute("ATTRACT_BASE").ops.update( id_field="POS_ID", copy_from_field="NEW_ATTRACT_BASE"), Apply(source_fields=["ATTRACT_BASE"], named_as="NEW_ATTRACTIVENESS", f=_attractiveness_sigmoid(), f_args="series"), pos.get_attribute("ATTRACTIVENESS").ops.update( id_field="POS_ID", copy_from_field="NEW_ATTRACTIVENESS"), # TODO: remove this (currently there just for debugs) circus.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="att_updates")) delta_updater = NumpyRandomGenerator(method="choice", a=[-1, 0, 1], seed=next(circus.seeder)) # random walk around of the attractiveness delta, once per week attractiveness_delta_evolution = circus.create_story( name="attractiveness_delta_evolution", initiating_actor=pos, actorid_field="POS_ID", timer_gen=ConstantDependentGenerator( value=circus.clock.n_iterations(pd.Timedelta("7 days")))) attractiveness_delta_evolution.set_operations( pos.ops.lookup(id_field="POS_ID", select={"ATTRACT_DELTA": "ATTRACT_DELTA"}), delta_updater.ops.generate(named_as="DELTA_UPDATE"), Apply(source_fields=["ATTRACT_DELTA", "DELTA_UPDATE"], named_as="NEW_ATTRACT_DELTA", f=np.add, f_args="series"), pos.get_attribute("ATTRACT_DELTA").ops.update( id_field="POS_ID", copy_from_field="NEW_ATTRACT_DELTA"), # TODO: remove this (currently there just for debugs) circus.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id="att_delta_updates"))