예제 #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 test_get_activity_should_be_default_by_default():

    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))
    story = Story(name="tested",
                  initiating_population=population,
                  member_id_field="")

    # by default, each population should be in the default state with activity 1
    assert [1.] * 10 == story.get_param("activity", population.ids).tolist()
    assert story.get_possible_states() == ["default"]
예제 #3
0
def test_empty_story_should_do_nothing_and_not_crash():

    customers = Population(circus=None,
                           size=1000,
                           ids_gen=SequencialGenerator(prefix="a"))
    empty_story = Story(name="purchase",
                        initiating_population=customers,
                        member_id_field="AGENT")

    logs = empty_story.execute()

    # no logs should be produced
    assert logs == {}
예제 #4
0
def test_bugfix_collisions_force_act_next():
    # Previously, resetting the timer of reset populations was cancelling the reset.
    #
    # We typically want to reset the timer when we have change the activity
    # state => we want to generate new timer values that reflect the new state.
    #
    # But force_act_next should still have priority on that: if somewhere else
    # we force some populations to act at the next clock step (e.g. to re-try
    # buying an ER or so), the fact that their activity level changed should
    # not cancel the retry.

    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))

    # 5 populations should trigger in 2 ticks, and 5 more
    init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids)
    timers_gen = MockTimerGenerator(init_timers)

    story = Story(
        name="tested",
        initiating_population=population,
        member_id_field="ac_id",

        # forcing the timer of all populations to be initialized to 0
        timer_gen=timers_gen)

    timer_values = story.timer["remaining"].copy()

    forced = pd.Index(["ac_1", "ac_3", "ac_7", "ac_8", "ac_9"])
    not_forced = pd.Index(["ac_0", "ac_2", "ac_4", "ac_4", "ac_6"])

    # force_act_next should only impact those ids
    story.force_act_next(forced)
    assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0]
    assert story.timer.loc[not_forced]["remaining"].equals(
        timer_values[not_forced])

    # resetting the timers should not change the timers of the populations that
    # are being forced
    story.reset_timers()
    assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0]

    # Ticking the timers should not change the timers of the populations that
    # are being forced.
    # This is important for population forcing themselves to act at the next
    # clock
    # step (typical scenario for retry) => the fact of thick the clock at the
    # end of the execution should not impact them.
    story.timer_tick(population.ids)
    assert story.timer.loc[forced]["remaining"].tolist() == [0, 0, 0, 0, 0]
    assert story.timer.loc[not_forced]["remaining"].equals(
        timer_values[not_forced] - 1)
예제 #5
0
def test_active_inactive_ids_should_mark_all_populations_active_when_all_timers_0(
):
    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))

    # 5 populations should trigger in 2 ticks, and 5 more
    init_timers = pd.Series([0] * 10, index=population.ids)
    timers_gen = MockTimerGenerator(init_timers)

    story = Story(
        name="tested",
        initiating_population=population,
        member_id_field="ac_id",

        # forcing the timer of all populations to be initialized to 0
        timer_gen=timers_gen,
        auto_reset_timer=True)
    assert (population.ids.tolist(), []) == story.active_inactive_ids()
예제 #6
0
def _add_pos_latlong(circus, params):

    logging.info("Generating POS attributes from Sites info (coord, dist l2)")

    pos = circus.actors["pos"]
    sites = circus.actors["sites"]

    # 1 deg is about 1km at 40 degree north => standard deviation of about 200m
    coord_noise = NumpyRandomGenerator(method="normal",
                                       loc=0,
                                       scale=1 / (85 * 5),
                                       seed=next(circus.seeder))

    # using an at build time to generate random values :)
    pos_coord_act = Chain(
        pos.ops.lookup(id_field="POS_ID", select={"SITE": "SITE_ID"}),
        sites.ops.lookup(id_field="SITE_ID",
                         select={
                             "LATITUDE": "SITE_LATITUDE",
                             "LONGITUDE": "SITE_LONGITUDE"
                         }),
        coord_noise.ops.generate(named_as="LAT_NOISE"),
        coord_noise.ops.generate(named_as="LONG_NOISE"),
        Apply(source_fields=["SITE_LATITUDE", "LAT_NOISE"],
              named_as="POS_LATITUDE",
              f=np.add,
              f_args="series"),
        Apply(source_fields=["SITE_LONGITUDE", "LONG_NOISE"],
              named_as="POS_LONGITUDE",
              f=np.add,
              f_args="series"),
    )

    # also looks up the dist l1 and dist l2 associated to the site
    for product in params["products"].keys():
        pos_coord_act.append(
            sites.ops.lookup(id_field="SITE_ID",
                             select={
                                 "{}__dist_l2".format(product):
                                 "{}__provider".format(product),
                                 "{}__dist_l1".format(product):
                                 "{}__dist_l1".format(product),
                             }), )

    pos_info, _ = pos_coord_act(Story.init_story_data("POS_ID", pos.ids))

    pos.create_attribute("LATITUDE", init_values=pos_info["POS_LATITUDE"])
    pos.create_attribute("LONGITUDE", init_values=pos_info["POS_LONGITUDE"])

    # copies the dist l1 and l2 of the site to the pos
    for product in params["products"].keys():
        rel_l2 = pos.create_relationship(name="{}__provider".format(product))
        rel_l2.add_relations(from_ids=pos_info["POS_ID"],
                             to_ids=pos_info["{}__provider".format(product)])
예제 #7
0
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()
예제 #8
0
    def create_story(self, name, **story_params):
        """
        Creates a story with the provided parameters and attach it to this
        circus.
        """

        existing = self.get_story(name)

        if existing is None:
            story = Story(name=name, **story_params)
            self.stories.append(story)
            return story

        else:
            raise ValueError("Cannot add story {}: another story with "
                             "identical name is already in the circus".format(name))
예제 #9
0
def test_story_autoreset_true_and_dropping_rows_should_reset_all_timers():
    # in case an story is configured to perform an auto-reset, but also
    # drops some rows, after one execution,
    #  - all executed rows (dropped or not) should have a timer back to some
    # positive value
    #  - all non executed rows should have gone down one tick

    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))

    # 5 populations should trigger in 2 ticks, and 5 more
    init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids)
    timers_gen = MockTimerGenerator(init_timers)

    story = Story(
        name="tested",
        initiating_population=population,
        member_id_field="ac_id",

        # forcing the timer of all populations to be initialized to 0
        timer_gen=timers_gen,
        auto_reset_timer=True)

    # simulating an operation that drop the last 2 rows
    story.set_operations(MockDropOp(0, 2))

    # initial timers should be those provided by the generator
    assert story.timer["remaining"].equals(init_timers)

    # after one execution, no population id has been selected and all counters
    # are decreased by 1
    story.execute()
    assert story.timer["remaining"].equals(init_timers - 1)

    # this time, the last 5 should have executed => and the last 3 of them
    # should have been dropped. Nonetheless, all 5 of them should be back to 1
    story.execute()
    expected_timers = pd.Series([0] * 5 + [1] * 5, index=population.ids)
    assert story.timer["remaining"].equals(expected_timers)
예제 #10
0
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)
예제 #11
0
def test_bugfix_force_populations_should_only_act_once():

    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))

    # 5 populations should trigger in 2 ticks, and 5 more
    init_timers = pd.Series([2] * 5 + [5] * 5, index=population.ids)
    timers_gen = MockTimerGenerator(init_timers)

    story = Story(
        name="tested",
        initiating_population=population,
        member_id_field="ac_id",

        # forcing the timer of all populations to be initialized to 0
        timer_gen=timers_gen)

    recording_op = FakeRecording()
    story.set_operations(recording_op)

    forced = pd.Index(["ac_1", "ac_3", "ac_7", "ac_8", "ac_9"])

    # force_act_next should only impact those ids
    story.force_act_next(forced)

    assert story.timer["remaining"].tolist() == [2, 0, 2, 0, 2, 5, 5, 0, 0, 0]

    story.execute()
    assert recording_op.last_seen_population_ids == [
        "ac_1", "ac_3", "ac_7", "ac_8", "ac_9"
    ]
    print(story.timer["remaining"].tolist())
    assert story.timer["remaining"].tolist() == [1, 2, 1, 2, 1, 4, 4, 5, 5, 5]
    recording_op.reset()

    story.execute()
    assert recording_op.last_seen_population_ids == []
    assert story.timer["remaining"].tolist() == [0, 1, 0, 1, 0, 3, 3, 4, 4, 4]

    story.execute()
    assert recording_op.last_seen_population_ids == ["ac_0", "ac_2", "ac_4"]
    assert story.timer["remaining"].tolist() == [2, 0, 2, 0, 2, 2, 2, 3, 3, 3]
예제 #12
0
def test_story_autoreset_false_and_dropping_rows_should_reset_all_timers():
    # in case an story is configured not to perform an auto-reset, after one
    # execution:
    #  - all executed rows should now be at -1 (dropped or not)
    #  - all non executed rows should have gone down one tick

    population = Population(circus=None,
                            size=10,
                            ids_gen=SequencialGenerator(prefix="ac_",
                                                        max_length=1))

    # 5 populations should trigger in 2 ticks, and 5 more
    init_timers = pd.Series([2] * 5 + [1] * 5, index=population.ids)
    timers_gen = MockTimerGenerator(init_timers)

    story = Story(
        name="tested",
        initiating_population=population,
        member_id_field="ac_id",

        # forcing the timer of all populations to be initialized to 0
        timer_gen=timers_gen,
        auto_reset_timer=False)

    # empty operation list as initialization
    # simulating an operation that drop the last 2 rows
    story.set_operations(MockDropOp(0, 2))

    # we have no auto-reset => all timers should intially be at -1
    all_minus_1 = pd.Series([-1] * 10, index=population.ids)
    assert story.timer["remaining"].equals(all_minus_1)

    # executing once => should do nothing, and leave all timers at -1
    story.execute()
    assert story.timer["remaining"].equals(all_minus_1)

    # triggering explicitaly the story => timers should have the hard-coded
    # values from the mock generator
    story.reset_timers()
    assert story.timer["remaining"].equals(init_timers)

    # after one execution, no population id has been selected and all counters
    # are decreased by 1
    story.execute()
    assert story.timer["remaining"].equals(init_timers - 1)

    # this time, the last 5 should have executed, but we should not have
    # any timer reste => they should go to -1.
    # The other 5 should now be at 0, ready to execute at next step
    story.execute()
    expected_timers = pd.Series([0] * 5 + [-1] * 5, index=population.ids)
    assert story.timer["remaining"].equals(expected_timers)

    # executing once more: the previously at -1 should still be there, and the
    # just executed at this stage should be there too
    story.execute()
    expected_timers = pd.Series([-1] * 10, index=population.ids)
    assert story.timer["remaining"].equals(expected_timers)
예제 #13
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()
예제 #14
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()