def test_circular_get_window_demand(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # For circular - I'll do one manual for sanity and a couple programmatic
     assert s._get_window_demand(1,
                                 4) == self.expected_flat_week_demand[1:4]
     assert s._get_window_demand(11, 13) == [0, 1]
     assert s._get_window_demand(11, 11) == []
예제 #2
0
 def test_circular_get_window_demand(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # For circular - I'll do one manual for sanity and a couple programmatic
     assert s._get_window_demand(1,
                                 4) == self.expected_flat_week_demand[1:4]
     assert s._get_window_demand(11, 13) == [0, 1]
     assert s._get_window_demand(11, 11) == []
예제 #3
0
 def test_windowing_split_valid_subproblem(self):
     # Window so large that it gets recursively split
     self.min_length = 2  # Update versus prior test
     self.week_demand = [[1, 0, 0, 4], [1, 2, 1, 0], [1, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (8, 10), (10, 13)]
     assert s._windows == expected_windows
 def test_windowing_split_valid_subproblem(self):
     # Window so large that it gets recursively split
     self.min_length = 2  # Update versus prior test
     self.week_demand = [[1, 0, 0, 4], [1, 2, 1, 0], [1, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (8, 10), (10, 13)]
     assert s._windows == expected_windows
예제 #5
0
 def test_circular_get_demand(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # For circular - I'll do one manual for sanity and a couple programmatic
     assert s._get_flat_demand(13) == self.expected_flat_week_demand[1]
     for t in range(len(self.expected_flat_week_demand)):
         assert s._get_flat_demand(t) == self.expected_flat_week_demand[t]
         assert s._get_flat_demand(t + len(self.expected_flat_week_demand)
                                   ) == self.expected_flat_week_demand[t]
 def test_windowing_short_subproblem(self):
     self.week_demand = [[1, 2, 0, 0], [1, 3, 1, 0], [0, 1, 1, 0]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [
         (4, 7),
     ]
     assert s._windows == expected_windows
 def test_circular_get_demand(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # For circular - I'll do one manual for sanity and a couple programmatic
     assert s._get_flat_demand(13) == self.expected_flat_week_demand[1]
     for t in range(len(self.expected_flat_week_demand)):
         assert s._get_flat_demand(t) == self.expected_flat_week_demand[t]
         assert s._get_flat_demand(t + len(self.expected_flat_week_demand)
                                   ) == self.expected_flat_week_demand[t]
예제 #8
0
 def test_windowing_short_subproblem(self):
     self.week_demand = [[1, 2, 0, 0], [1, 3, 1, 0], [0, 1, 1, 0]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [
         (4, 7),
     ]
     assert s._windows == expected_windows
 def test_windowing_split_invalid_subproblem(self):
     # Window that is so large that it gets recursively split . . .
     # BUT one of the split problems is less the min length, so
     # it shoudl abandon the search
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [0, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (9, 14)]
     assert s._windows == expected_windows
예제 #10
0
 def test_windowing_split_invalid_subproblem(self):
     # Window that is so large that it gets recursively split . . .
     # BUT one of the split problems is less the min length, so
     # it shoudl abandon the search
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [0, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (9, 14)]
     assert s._windows == expected_windows
예제 #11
0
    def test_london_on_demand_two(self):
        """Test on demand trial data that supposedly had low efficiency"""
        # yapf: disable
        demand = [
            [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 4, 8, 12, 10, 9, 8, 9, 15, 15, 20,
             17, 14, 7, 4],
            [0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 6, 10, 14, 14, 9, 9, 9, 12, 20, 25,
             20, 12, 9, 5],
            [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 7, 12, 15, 12, 10, 9, 12, 12, 16,
             21, 20, 12, 9, 4],
            [0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 7, 10, 13, 13, 9, 9, 11, 15, 17, 24,
             23, 14, 7, 5],
            [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 7, 10, 13, 13, 11, 11, 11, 12, 25,
             29, 29, 20, 12, 9],
            [6, 0, 0, 0, 0, 0, 0, 0, 3, 6, 6, 12, 19, 18, 17, 16, 16, 18, 24,
             25, 25, 18, 11, 7],
            [4, 0, 0, 0, 0, 0, 0, 0, 3, 6, 7, 12, 19, 19, 17, 16, 16, 18, 23,
             29, 26, 14, 9, 5],
        ]
        # yapf: enable
        min_length = 4
        max_length = 14

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()
        # For these, we really need perfect efficiency
        assert s.efficiency() < 0.01
예제 #12
0
    def test_splitter_init(self):
        s = Splitter(self.week_demand, self.min_length, self.max_length)

        assert len(
            s.flat_demand) == len(self.week_demand) * len(self.week_demand[0])
        assert s.flat_demand == self.expected_flat_week_demand
        assert s.min_length == self.min_length
        assert s.max_length == self.max_length
        assert s.day_length == len(self.week_demand[0])
예제 #13
0
    def test_flat_index_to_time(self):
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        flat_to_time = [
            (0, 0),
            (1, 1),
            (2, 2),
            (3, 3),
            (4, 0),
            (5, 1),
            (6, 2),
            (7, 3),
            (8, 0),
            (9, 1),
            (10, 2),
            (11, 3),
        ]

        for (flat, time) in flat_to_time:
            assert s._flat_index_to_time(flat) == time
    def test_flat_index_to_time(self):
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        flat_to_time = [
            (0, 0),
            (1, 1),
            (2, 2),
            (3, 3),
            (4, 0),
            (5, 1),
            (6, 2),
            (7, 3),
            (8, 0),
            (9, 1),
            (10, 2),
            (11, 3),
        ]

        for (flat, time) in flat_to_time:
            assert s._flat_index_to_time(flat) == time
예제 #15
0
    def test_flat_index_to_day(self):
        """Take the flat demand index and return the day integer"""
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        flat_to_day = [
            (0, 0),
            (1, 0),
            (2, 0),
            (3, 0),
            (4, 1),
            (5, 1),
            (6, 1),
            (7, 1),
            (8, 2),
            (9, 2),
            (10, 2),
            (11, 2),
        ]

        for (flat, day) in flat_to_day:
            assert s._flat_index_to_day(flat) == day
    def test_flat_index_to_day(self):
        """Take the flat demand index and return the day integer"""
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        flat_to_day = [
            (0, 0),
            (1, 0),
            (2, 0),
            (3, 0),
            (4, 1),
            (5, 1),
            (6, 1),
            (7, 1),
            (8, 2),
            (9, 2),
            (10, 2),
            (11, 2),
        ]

        for (flat, day) in flat_to_day:
            assert s._flat_index_to_day(flat) == day
예제 #17
0
    def _process_task(self, task):
        # 1. Fetch schedule
        self.org = self.client.get_organization(
            task.data.get("organization_id"))
        self.loc = self.org.get_location(task.data.get("location_id"))
        self.role = self.loc.get_role(task.data.get("role_id"))
        self.sched = self.role.get_schedule(task.data.get("schedule_id"))

        self._compute_demand()
        self._subtract_existing_shifts_from_demand()

        # Run the  calculation
        s = Splitter(self.demand,
                     self.sched.data.get("min_shift_length_hour"),
                     self.sched.data.get("max_shift_length_hour"))
        s.calculate()
        s.efficiency()

        # Naive becuase not yet datetimes
        naive_shifts = s.get_shifts()
        logger.info("Starting upload of %s shifts", len(naive_shifts))

        local_start_time = self._get_local_start_time()

        for shift in naive_shifts:
            # We have to think of daylight savings time here, so we need to
            # guarantee that we don't have any errors. We do this by overshooting
            # the timedelta by an extra two hours, then rounding back to midnight.

            logger.debug("Processing shift %s", shift)

            start_day = normalize_to_midnight(
                deepcopy(local_start_time) + timedelta(days=shift["day"]))

            # Beware of time changes - duplicate times are possible
            try:
                start = start_day.replace(hour=shift["start"])
            except pytz.AmbiguousTimeError:
                # Randomly pick one. Minor tech debt.
                start = start_day.replace(hour=shift["start"], is_dst=False)

            stop = start + timedelta(hours=shift["length"])

            # Convert to the strings we are passing up to the cLoUd
            utc_start_str = start.astimezone(self.default_tz).isoformat()
            utc_stop_str = stop.astimezone(self.default_tz).isoformat()

            logger.info("Creating shift with start %s stop %s", start, stop)
            self.role.create_shift(start=utc_start_str, stop=utc_stop_str)
예제 #18
0
    def _process_task(self, task):
        # 1. Fetch schedule
        self.org = self.client.get_organization(
            task.data.get("organization_id"))
        self.loc = self.org.get_location(task.data.get("location_id"))
        self.role = self.loc.get_role(task.data.get("role_id"))
        self.sched = self.role.get_schedule(task.data.get("schedule_id"))

        self._compute_demand()
        self._subtract_existing_shifts_from_demand()

        # Run the  calculation
        s = Splitter(self.demand,
                     self.sched.data.get("min_shift_length_hour"),
                     self.sched.data.get("max_shift_length_hour"))
        s.calculate()
        s.efficiency()

        # Naive becuase not yet datetimes
        naive_shifts = s.get_shifts()
        logger.info("Starting upload of %s shifts", len(naive_shifts))

        local_start_time = self._get_local_start_time()

        for shift in naive_shifts:
            # We have to think of daylight savings time here, so we need to
            # guarantee that we don't have any errors. We do this by overshooting
            # the timedelta by an extra two hours, then rounding back to midnight.

            logger.debug("Processing shift %s", shift)

            start_day = normalize_to_midnight(
                deepcopy(local_start_time) + timedelta(days=shift["day"]))

            # Beware of time changes - duplicate times are possible
            try:
                start = start_day.replace(hour=shift["start"])
            except pytz.AmbiguousTimeError:
                # Randomly pick one. Minor tech debt.
                start = start_day.replace(hour=shift["start"], is_dst=False)

            stop = start + timedelta(hours=shift["length"])

            # Convert to the strings we are passing up to the cLoUd
            utc_start_str = start.astimezone(self.default_tz).isoformat()
            utc_stop_str = stop.astimezone(self.default_tz).isoformat()

            logger.info("Creating shift with start %s stop %s", start, stop)
            self.role.create_shift(start=utc_start_str, stop=utc_stop_str)
예제 #19
0
    def test_la(self):
        """24/7 LA client data"""
        # yapf: disable
        demand = [
            # They sometimes have leading zeros
            [0, 0, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 1],
            [1, 1, 1, 1, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3],
            [4, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4],
            [3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3],
            [3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4],
            [4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
            [4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4],
        ]
        # yapf: enable
        min_length = 6
        max_length = 8

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()
        assert s.efficiency() < 0.08
예제 #20
0
    def test_call_center(self):
        """See if it chokes for call center client data"""
        # yapf: disable
        demand = [
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0],
        ]
        # yapf: enable
        min_length = 9
        max_length = 9

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()
        # For these, we really need perfect efficiency
        assert s.efficiency() < 0.001
예제 #21
0
    def test_succeeds_with_zero_demand(self):
        # yapf: disable
        demand = [
            [0]*24,
            [0]*24,
            [0]*24,
            [0]*24,
            [0]*24,
            [0]*24,
            [0]*24,
        ]
        # yapf: enable

        min_length = 4
        max_length = 8

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()

        efficiency = s.efficiency()
        assert efficiency == PERFECT_OPTIMALITY
예제 #22
0
    def test_online_store_prod_failure(self):
        """This data caused a production error"""
        # yapf: disable
        demand = [
            [8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8],
            [8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8],
            [8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8],
            [8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8],
            [8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8],
        ]
        # yapf: enable
        min_length = 8
        max_length = 8

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()
예제 #23
0
    def test_on_demand_old(self):
        """Old on demand data - good test of wrap-around"""
        # yapf: disable
        demand = [
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 4, 4, 6, 6, 7, 7, 5, 2],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 4, 5, 3, 3, 4, 4, 7, 6, 5, 4, 2],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 5, 4, 5, 4, 5, 5, 7, 6, 6, 5, 3],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 3, 5, 5, 2, 5, 6, 7, 6, 6, 5, 3],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 3, 5, 4, 6, 5, 6, 8, 5, 4, 4],
            [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 3, 4, 5, 6, 7, 8, 6, 6, 5, 2],
            [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 5, 5, 4, 6, 6, 11, 8, 6, 4, 2],
        ]
        # yapf: enable

        min_length = 4
        max_length = 8

        s = Splitter(demand, min_length, max_length)
        s.calculate()
        s.validate()

        efficiency = s.efficiency()
        assert efficiency < EFFICIENCY_LIMIT
예제 #24
0
    def test_windowing_standard(self):
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        s._generate_windows()
        expected_windows = [(0, 3), (4, 7), (8, 11)]

        assert s._windows == expected_windows
예제 #25
0
    def test_splitter_init_different_day_lengths(self):
        self.week_demand[1].pop()

        with pytest.raises(UnequalDayLengthException):

            Splitter(self.week_demand, self.min_length, self.max_length)
    def test_windowing_standard(self):
        s = Splitter(self.week_demand, self.min_length, self.max_length)
        s._generate_windows()
        expected_windows = [(0, 3), (4, 7), (8, 11)]

        assert s._windows == expected_windows
 def test_windowing_standard_again(self):
     self.week_demand = [[0, 2, 3, 4, 0, 3, 1, 8], [1, 2, 3, 0, 2, 3, 1, 8]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(1, 4), (5, 11), (12, 16)]
     assert s._windows == expected_windows
 def test_is_always_open_case_false(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # Just going through this whole week, cause f**k it
     assert s._is_always_open() is False
 def test_is_circular_necessary_case_false(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_circular_necessary() is False
 def test_is_circular_necessary_case_true(self):
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [1, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_circular_necessary() is True
 def test_windowing_circular(self):
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [0, 0, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (10, 14)]
     assert s._windows == expected_windows
예제 #32
0
 def test_is_always_open_case_false(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     # Just going through this whole week, cause f**k it
     assert s._is_always_open() is False
예제 #33
0
 def test_is_always_open_case_carryover_true(self):
     self.week_demand = [[0, 0, 0, 4], [1, 3, 1, 8], [1, 1, 1, 2]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_always_open() is True
예제 #34
0
 def test_windowing_standard_again(self):
     self.week_demand = [[0, 2, 3, 4, 0, 3, 1, 8], [1, 2, 3, 0, 2, 3, 1, 8]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(1, 4), (5, 11), (12, 16)]
     assert s._windows == expected_windows
예제 #35
0
 def test_windowing_always_open(self):
     self.week_demand = [[1, 2, 3, 4], [1, 3, 1, 8], [1, 1, 1, 2]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(0, 4), (4, 8), (8, 12)]
     assert s._windows == expected_windows
예제 #36
0
 def test_is_circular_necessary_case_false(self):
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_circular_necessary() is False
예제 #37
0
 def test_windowing_circular(self):
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [0, 0, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(3, 7), (10, 14)]
     assert s._windows == expected_windows
예제 #38
0
 def test_is_circular_necessary_case_true(self):
     self.week_demand = [[1, 1, 0, 4], [1, 2, 1, 0], [1, 1, 1, 1]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_circular_necessary() is True
 def test_is_always_open_case_carryover_true(self):
     self.week_demand = [[0, 0, 0, 4], [1, 3, 1, 8], [1, 1, 1, 2]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     assert s._is_always_open() is True
 def test_windowing_always_open(self):
     self.week_demand = [[1, 2, 3, 4], [1, 3, 1, 8], [1, 1, 1, 2]]
     s = Splitter(self.week_demand, self.min_length, self.max_length)
     s._generate_windows()
     expected_windows = [(0, 4), (4, 8), (8, 12)]
     assert s._windows == expected_windows