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 _init_pos_product(circus, product, description): """ Initialize the required stock and generators for this """ logging.info( "Building a generator of {} POS bulk purchase size".format(product)) bulk_size_gen = NumpyRandomGenerator( method="choice", a=description["pos_bulk_purchase_sizes"], p=description["pos_bulk_purchase_sizes_dist"], seed=next(circus.seeder)) circus.attach_generator("pos_{}_bulk_size_gen".format(product), bulk_size_gen) logging.info( "Building a generators of {} POS initial stock size".format(product)) if "pos_init_distro" in description: logging.info(" using pre-defined initial distribution") gen_namespace, gen_id = description["pos_init_distro"].split("/") # TODO: with the new save/load, this is now a mere numpyGenerator init_stock_size_gen = db.load_empirical_discrete_generator( namespace=gen_namespace, gen_id=gen_id, seed=next(circus.seeder)) else: logging.info(" using bulk size distribution") init_stock_size_gen = bulk_size_gen circus.attach_generator("pos_{}_init_stock_size_gen".format(product), init_stock_size_gen) logging.info("Building a generator of {} ids".format(product)) product_id_gen = SequencialGenerator(prefix="{}_".format(product)) circus.attach_generator("{}_id_gen".format(product), product_id_gen) logging.info("Initializing POS {} stock".format(product)) stock_gen = init_stock_size_gen.flatmap( DependentBulkGenerator(element_generator=product_id_gen)) circus.actors["pos"].create_stock_relationship_grp( name=product, stock_bulk_gen=stock_gen)
def _add_person_population(self): id_gen = SequencialGenerator(prefix="PERSON_") age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5, seed=next(self.seeder)) name_gen = FakerGenerator(method="name", seed=next(self.seeder)) person = self.create_population(name="person", size=1000, ids_gen=id_gen) person.create_attribute("NAME", init_gen=name_gen) person.create_attribute("AGE", init_gen=age_gen) quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True, seed=next(self.seeder)) quotes_rel = self.populations["person"].create_relationship("quotes") for w in range(4): quotes_rel.add_relations( from_ids=person.ids, to_ids=quote_generator.generate(size=person.size), weights=w )
def step2(): 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))) 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( person.ops.lookup(id_field="PERSON_ID", select={"NAME": "NAME"}), ConstantGenerator(value="hello world").ops.generate(named_as="HELLO"), 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_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" ]
def create_circus_with_population(): example_circus = circus.Circus( name="example", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) person = example_circus.create_population( name="person", size=1000, ids_gen=SequencialGenerator(prefix="PERSON_")) person.create_attribute( "NAME", init_gen=FakerGenerator(method="name", seed=next(example_circus.seeder))) person.create_attribute( "age", init_gen=NumpyRandomGenerator( method="normal", loc=35, scale=5, seed=next(example_circus.seeder))) return example_circus
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 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)
def create_field_agents(circus, params): logging.info(" adding {} field agents".format(params["n_field_agents"])) field_agents = circus.create_population( name="field_agents", size=params["n_field_agents"], ids_gen=SequencialGenerator(prefix="FA_")) logging.info(" adding mobility relationships to field agents") mobility_rel = field_agents.create_relationship("POSSIBLE_SITES") # TODO: make sure the number of sites per field agent is "reasonable" mobility_df = pd.DataFrame.from_records(make_random_bipartite_data( field_agents.ids, circus.actors["sites"].ids, 0.4, seed=next(circus.seeder)), columns=["FA_ID", "SID"]) mobility_weight_gen = NumpyRandomGenerator(method="exponential", scale=1., seed=next(circus.seeder)) mobility_rel.add_relations(from_ids=mobility_df["FA_ID"], to_ids=mobility_df["SID"], weights=mobility_weight_gen.generate( mobility_df.shape[0])) # Initialize the mobility by allocating one first random site to each # field agent among its network field_agents.create_attribute(name="CURRENT_SITE", init_relationship="POSSIBLE_SITES") return field_agents
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 step5(): 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") # 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) 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))
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"],
val = 1 / num_log_per_hr step_dur = "{0}h".format(val) #7am-7pm = 13 hrs num_log = 13 * (num_log_per_hr) setup_logging() start_date = pd.Timestamp("09 Aug 2020 00:00:00") example1 = circus.Circus(name="example1", master_seed=123456, start=start_date, step_duration=pd.Timedelta(step_dur)) person = example1.create_population(name="person", size=num_user, ids_gen=SequencialGenerator(prefix="")) person.create_attribute("NAME", init_gen=FakerGenerator(method="name", seed=next(example1.seeder))) activity = (100, 100, 100, 100, 54, 54, 54, 26, 20, 22) normed_activity = [float(i) / sum(activity) for i in activity] """ sites = SequencialGenerator(prefix="").generate(num_sites) random_site_gen = NumpyRandomGenerator(method="choice", a=sites, seed=next(example1.seeder), p=normed_activity) allowed_sites = person.create_relationship(name="sites")
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()
import logging import pandas as pd from tabulate import tabulate from trumania.core import circus, operations from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator, ConstantDependentGenerator, ConstantGenerator import trumania.core.util_functions as util_functions util_functions.setup_logging() example_circus = circus.Circus(name="example", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) id_gen = SequencialGenerator(prefix="PERSON_") age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5, seed=next(example_circus.seeder)) name_gen = FakerGenerator(method="name", seed=next(example_circus.seeder)) person = example_circus.create_population(name="person", size=1000, ids_gen=id_gen) person.create_attribute("NAME", init_gen=name_gen) person.create_attribute("AGE", init_gen=age_gen) hello_world = example_circus.create_story( name="hello_world", initiating_population=example_circus.populations["person"],
from trumania.core import circus import trumania.core.util_functions as util_functions from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator util_functions.setup_logging() logging.info("building circus") example = circus.Circus(name="example", master_seed=12345, start=pd.Timestamp("1 Jan 2017 00:00"), step_duration=pd.Timedelta("1h")) person = example.create_population( name="person", size=1000, ids_gen=SequencialGenerator(prefix="PERSON_")) person.create_attribute("NAME", init_gen=FakerGenerator(method="name", seed=next(example.seeder))) person.create_attribute("age", init_gen=NumpyRandomGenerator(method="normal", loc=35, scale=5, seed=next( example.seeder))) example.run(duration=pd.Timedelta("48h"), log_output_folder="output/example2", delete_existing_logs=True)
def add_purchase_actions(circus, params): customers = circus.actors["customers"] pos = circus.actors["pos"] sites = circus.actors["sites"] for product, description in params["products"].items(): logging.info("creating customer {} purchase action".format(product)) purchase_timer_gen = DefaultDailyTimerGenerator( circus.clock, next(circus.seeder)) max_activity = purchase_timer_gen.activity( n=1, per=pd.Timedelta( days=description["customer_purchase_min_period_days"])) min_activity = purchase_timer_gen.activity( n=1, per=pd.Timedelta( days=description["customer_purchase_max_period_days"])) purchase_activity_gen = NumpyRandomGenerator( method="uniform", low=1 / max_activity, high=1 / min_activity, seed=next(circus.seeder)).map(f=lambda per: 1 / per) low_stock_bulk_purchase_trigger = DependentTriggerGenerator( value_to_proba_mapper=bounded_sigmoid( x_min=1, x_max=description["max_pos_stock_triggering_pos_restock"], shape=description["restock_sigmoid_shape"], incrementing=False)) item_price_gen = NumpyRandomGenerator(method="choice", a=description["item_prices"], seed=next(circus.seeder)) action_name = "customer_{}_purchase".format(product) purchase_action = circus.create_story( name=action_name, initiating_actor=customers, actorid_field="CUST_ID", timer_gen=purchase_timer_gen, activity_gen=purchase_activity_gen) purchase_action.set_operations( customers.ops.lookup(id_field="CUST_ID", select={"CURRENT_SITE": "SITE"}), sites.get_relationship("POS").ops.select_one( from_field="SITE", named_as="POS", weight=pos.get_attribute_values("ATTRACTIVENESS"), # TODO: this means customer in a location without POS do not buy # anything => we could add a re-try mechanism here discard_empty=True), sites.get_relationship("CELLS").ops.select_one(from_field="SITE", named_as="CELL_ID"), # injecting geo level 2 and distributor in purchase action: # this is only required for approximating targets of that # distributor sites.ops.lookup(id_field="SITE", select={ "GEO_LEVEL_2": "geo_level2_id", "{}__dist_l1".format(product): "distributor_l1" }), pos.get_relationship(product).ops.select_one( from_field="POS", named_as="INSTANCE_ID", pop=True, discard_empty=False), circus.actors[product].ops.select_one(named_as="PRODUCT_ID"), Apply(source_fields="INSTANCE_ID", named_as="FAILED_SALE_OUT_OF_STOCK", f=pd.isnull, f_args="series"), SequencialGenerator( prefix="TX_CUST_{}".format(product)).ops.generate( named_as="TX_ID"), item_price_gen.ops.generate(named_as="VALUE"), circus.clock.ops.timestamp(named_as="TIME"), FieldLogger(log_id=action_name), patterns.trigger_action_if_low_stock( circus, stock_relationship=pos.get_relationship(product), actor_id_field="POS", restock_trigger=low_stock_bulk_purchase_trigger, triggered_action_name="pos_{}_bulk_purchase".format(product)), )