示例#1
0
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()
示例#2
0
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))
示例#3
0
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))
示例#4
0
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()
示例#5
0
    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"]),
        )
示例#6
0
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"
                    ]))
示例#7
0
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)
示例#8
0
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)
示例#9
0
    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]))
示例#10
0
    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()
示例#11
0
    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"
                                   ]),
        )
示例#12
0
    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
示例#13
0
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))
示例#14
0
    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"),
        )
示例#15
0
    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)
示例#17
0
    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
示例#18
0
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()