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
|