Skip to content

Fallbacks

FallbackManager

Handles fallback allocation strategies when normal allocation fails.

Source code in may/venue_distributor/fallbacks.py
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class FallbackManager:
    """
    Handles fallback allocation strategies when normal allocation fails.
    """

    def __init__(self, distributor):
        self.distributor = distributor
        self.config = distributor.config

    def handle_fallbacks(self, unallocated_people: List, venues: List, world) -> List:
        """Handle people who couldn't be allocated during the normal pass."""
        fallback_config = self.config.get('fallback', {})
        strategy = fallback_config.get('strategy', 'skip')

        if strategy == 'skip' or not unallocated_people:
            return unallocated_people

        logger.debug(f"Handling fallbacks for {len(unallocated_people)} people using strategy '{strategy}'")

        if strategy == 'relax_distance':
            return self._relax_distance(unallocated_people, venues, fallback_config)
        elif strategy == 'relax_capacity':
            return self._relax_capacity(unallocated_people, venues)
        elif strategy == 'assign_closest':
            return self._assign_closest(unallocated_people, venues)
        else:
            logger.warning(f"Unknown fallback strategy: {strategy}")
            return unallocated_people

    def _relax_distance(self, people: List, venues: List, config: Dict) -> List:
        """Retry allocation with progressively relaxed distance constraints."""
        relax_params = config.get('relax_params', {})
        multiplier = relax_params.get('distance_multiplier', 2.0)
        max_iters = relax_params.get('max_iterations', 3)

        remaining = list(people)
        selection_config = self.config.get('venue_selection', {})
        original_max_dist = selection_config.get('max_distance')
        original_count = selection_config.get('count')

        try:
            for i in range(max_iters):
                if not remaining: break

                scale = multiplier ** (i + 1)
                if original_max_dist is not None:
                    selection_config['max_distance'] = original_max_dist * scale
                if original_count is not None:
                    selection_config['count'] = int(original_count * scale)

                logger.info(f"  Relaxation iteration {i+1}/{max_iters} (distance x{scale:.1f})...")
                remaining = self.distributor.allocation.allocate_individual(remaining, venues)
        finally:
            if original_max_dist is not None:
                selection_config['max_distance'] = original_max_dist
            if original_count is not None:
                selection_config['count'] = original_count

        return remaining

    def _relax_capacity(self, people: List, venues: List) -> List:
        """Retry allocation while ignoring capacity limits."""
        original_when_full = self.config.get('allocation', {}).get('when_full', 'exclude')
        self.config.setdefault('allocation', {})['when_full'] = 'overflow'

        try:
            logger.info("  Relaxing capacity constraints...")
            remaining = self.distributor.allocation.allocate_individual(people, venues)
        finally:
            self.config['allocation']['when_full'] = original_when_full

        return remaining

    def _assign_closest(self, people: List, venues: List) -> List:
        """Assign each person to their absolute closest venue, ignoring ALL other constraints."""
        allocated_count = 0
        remaining = []
        logger.debug("  Assigning to closest venue regardless of eligibility or capacity...")

        for person in people:
            location = self.distributor._get_person_location(person)
            if location:
                closest = self.distributor._find_closest_venues(location, self.distributor.venue_type, 1)
                if closest:
                    venue = closest[0]
                    venue.add_to_subset(
                        person,
                        subset_key=self.distributor.subset_key,
                        activity_name=self.distributor.activity_map_key,
                        activity_type=self.distributor.activity_type
                    )
                    self.distributor._increment_venue_count(venue)
                    allocated_count += 1
                else:
                    logger.warning(f"Could not find ANY venue for person {person.id} in assign_closest fallback")
                    remaining.append(person)
            else:
                remaining.append(person)

        self.distributor.allocated_this_run += allocated_count
        logger.info(f"  Fallback (assign_closest): {allocated_count}/{len(people)} placed")

        return remaining

handle_fallbacks(unallocated_people, venues, world)

Handle people who couldn't be allocated during the normal pass.

Source code in may/venue_distributor/fallbacks.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def handle_fallbacks(self, unallocated_people: List, venues: List, world) -> List:
    """Handle people who couldn't be allocated during the normal pass."""
    fallback_config = self.config.get('fallback', {})
    strategy = fallback_config.get('strategy', 'skip')

    if strategy == 'skip' or not unallocated_people:
        return unallocated_people

    logger.debug(f"Handling fallbacks for {len(unallocated_people)} people using strategy '{strategy}'")

    if strategy == 'relax_distance':
        return self._relax_distance(unallocated_people, venues, fallback_config)
    elif strategy == 'relax_capacity':
        return self._relax_capacity(unallocated_people, venues)
    elif strategy == 'assign_closest':
        return self._assign_closest(unallocated_people, venues)
    else:
        logger.warning(f"Unknown fallback strategy: {strategy}")
        return unallocated_people