Coverage for pybeepop/beepop/beelist.py: 71%
413 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 13:34 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 13:34 +0000
1"""BeePop+ Bee List Management Module.
3This module contains the BeeList base class and its derived classes that manage
4age-structured populations of honey bees at different life stages. These classes
5implement the "boxcar" model where bees are grouped into age cohorts that advance
6through discrete time steps.
8The bee list system provides the core data structure for tracking population
9dynamics, aging, transitions between life stages, and mortality events within
10the colony simulation. Each life stage (eggs, larvae, brood, adults, foragers)
11has its own specialized list class with stage-specific behaviors.
13Key Classes:
14 BeeList: Base class for all bee population management
15 EggList: Manages egg populations and hatching
16 LarvaList: Manages larval development and feeding
17 BroodList: Manages capped brood and mite reproduction
18 AdultList: Manages adult worker and drone populations
19 ForagerListA: Manages foraging worker populations with enhanced tracking
21Architecture:
22 The bee list system uses a "boxcar" or cohort model where:
23 - Each list element represents a daily age cohort
24 - Bees age by advancing through list positions
25 - Transitions occur when bees move between life stages
26 - The "caboose" captures overflow from the oldest cohorts
28List Management:
29 - Daily aging advances all cohorts by one position
30 - New bees are added to the head (youngest position)
31 - Old bees exit from the tail or transition to next life stage
32 - Population counts are maintained for each age cohort
33 - Mite loads and other attributes track with bee cohorts
34"""
36from pybeepop.beepop.adult import Adult
37from pybeepop.beepop.brood import Brood
38from pybeepop.beepop.larva import Larva
39from pybeepop.beepop.egg import Egg
40from pybeepop.beepop.globaloptions import GlobalOptions
43class BeeList:
44 """Base class for age-structured bee population management.
46 This class provides the foundation for managing bee populations using a
47 "boxcar" or cohort model where bees are grouped by age and advance through
48 discrete time steps. Each list element represents a daily age cohort with
49 specific population counts and characteristics.
51 The BeeList class handles common operations for all bee life stages including
52 aging, mortality, transitions, and population tracking. Derived classes
53 extend this functionality with stage-specific behaviors and attributes.
55 Class Attributes:
56 DroneCount (int): Global counter for drone populations
57 ForagerCount (int): Global counter for forager populations
58 WorkerCount (int): Global counter for worker populations
60 Attributes:
61 bees (List[Bee]): List of bee cohorts ordered by age (youngest first)
62 _list_length (int): Maximum length of the bee list (life stage duration)
63 _colony (Colony): Reference to parent colony object
64 _prop_transition (float): Base proportion of bees transitioning out (0.0-1.0)
65 _drv_list (DateRangeValues): Date-dependent transition proportions
66 """
68 # Class-level counters (shared across all instances)
69 DroneCount = 0
70 ForagerCount = 0
71 WorkerCount = 0
73 def __init__(self):
74 """Initialize a new BeeList with empty populations.
76 Creates an empty bee list with default settings for managing
77 age-structured bee populations. The list starts with no bees
78 and default transition parameters.
79 """
80 self.bees = []
81 self._list_length = 0
82 self._colony = None
83 self._prop_transition = 1.0
84 self._drv_list = None # DateRangeValues object for dynamic transition control
86 def set_length(self, length):
87 """Set the list length (life stage duration)."""
88 self._list_length = length
90 def get_length(self):
91 """Get the list length (life stage duration)."""
92 return self._list_length
94 def set_colony(self, colony):
95 """Set reference to parent colony object."""
96 self._colony = colony
98 def get_colony(self):
99 """Get reference to parent colony object."""
100 return self._colony
102 def set_prop_transition(self, prop):
103 """Set proportion of bees transitioning from this list."""
104 self._prop_transition = prop
106 def get_prop_transition(self):
107 """Get proportion of bees transitioning from this list."""
108 return self._prop_transition
110 def set_drv_list(self, drv_list):
111 """Set the DateRangeValues object for this bee list."""
112 self._drv_list = drv_list
114 def get_drv_list(self):
115 """Get the DateRangeValues object for this bee list."""
116 return self._drv_list
118 def get_effective_prop_transition(self, current_date=None):
119 """
120 Get the effective proportion transition for the current date.
121 If a DateRangeValues object is set and enabled, it will override the base proportion.
122 Following C++ logic from colony.cpp lines 1392-1434.
123 """
124 # If no date range values are set, use the base proportion
125 if self._drv_list is None or not self._drv_list.is_enabled():
126 return self._prop_transition
128 # If no date is provided, use the base proportion
129 if current_date is None:
130 return self._prop_transition
132 # Try to get the active value from the date range values
133 drv_value = self._drv_list.get_active_value(current_date)
134 if drv_value is not None:
135 # Convert from percentage to proportion (following C++ logic: PropTransition / 100)
136 return drv_value / 100.0
137 else:
138 # No active date range value found, use base proportion
139 return self._prop_transition
141 def clear(self):
142 """Clears the bee list."""
143 self.bees = []
145 def get_count(self):
146 """Get the number of bees in the list (alias for get_quantity)."""
147 return self.get_quantity()
149 def add_head(self, bee):
150 """Add a bee to the head (beginning) of the list."""
151 self.bees.insert(0, bee)
153 def get_bee_class(self):
154 """Get the bee class for this list type. Override in derived classes."""
155 return Egg
157 def get_quantity(self):
158 """Returns the total number of alive bees in the list."""
159 total = 0
160 for bee in self.bees:
161 if bee.is_alive():
162 total += bee.get_number()
163 return int(total)
165 def kill_all(self):
166 """Kills all bees in the list."""
167 for bee in self.bees:
168 bee.kill()
170 def remove_list_elements(self):
171 """Removes all bees from the list and deletes them."""
172 self.bees.clear()
174 def get_quantity_at(self, index):
175 """Returns the quantity of bees at the specified index (boxcar). Single index version."""
176 if 0 <= index < len(self.bees) and self.bees[index] is not None:
177 number = self.bees[index].get_number()
178 return int(number) if number is not None else 0
179 return 0
181 def set_quantity_at(self, index, quan):
182 """Sets the quantity of bees at the specified index (boxcar). Single index version."""
183 if 0 <= index < len(self.bees):
184 self.bees[index].set_number(quan)
186 def get_quantity_at_range(self, from_idx, to_idx):
187 """Returns the sum of quantities from 'from_idx' to 'to_idx' inclusive. Range version."""
188 count = 0
189 list_length = len(self.bees)
191 if from_idx >= 0 and from_idx < list_length:
192 for i in range(from_idx, min(to_idx + 1, list_length)):
193 count += self.get_quantity_at(i) # Calls single-index version
194 return int(count)
196 def set_quantity_at_range(self, from_idx, to_idx, quan):
197 """Evenly divides 'quan' bees between boxcars from 'from_idx' to 'to_idx' inclusive. Range version."""
198 if from_idx > to_idx:
199 return
201 list_length = len(self.bees)
202 if to_idx > list_length - 1:
203 to_idx = list_length - 1
205 if from_idx >= 0 and from_idx <= to_idx:
206 num_boxcars = 1 + (to_idx - from_idx)
207 quan_per_boxcar = int(quan / num_boxcars) if num_boxcars > 0 else 0
208 for i in range(from_idx, to_idx + 1):
209 self.set_quantity_at(i, quan_per_boxcar) # Calls single-index version
211 def set_quantity_at_proportional(self, from_idx, to_idx, proportion):
212 """Sets the quantity of bees in boxcars between from_idx and to_idx = Number*Proportion."""
213 list_length = len(self.bees)
214 if to_idx > list_length - 1:
215 to_idx = list_length - 1
217 if from_idx >= 0 and from_idx <= to_idx:
218 for i in range(from_idx, to_idx + 1):
219 boxcar_quant = self.get_quantity_at(i)
220 self.set_quantity_at(i, int(boxcar_quant * proportion))
222 def factor_quantity(self, factor):
223 """Factor quantity increases or decreases the number of bees in each age group."""
224 for bee in self.bees:
225 bee.set_number(int(bee.get_number() * factor))
227 def status(self):
228 """Returns a string status of the bee list."""
229 stat = f"Tot BC: {len(self.bees)}, "
230 stat += ", ".join(
231 f"BC{i+1}: {bee.get_number()}" for i, bee in enumerate(self.bees)
232 )
233 return stat
236# Derived classes (stubs, ported from colony.cpp)
237# ForagerListA will be defined after AdultList since it inherits from it
240class AdultList(BeeList):
241 """Adult bee list for drones and workers. Ported from CAdultlist."""
243 def __init__(self):
244 super().__init__()
245 self.caboose = Adult(0) # Initialize with empty adult, not None
247 def get_bee_class(self):
248 """Get the bee class for adult lists."""
249 return Adult
251 def get_caboose(self):
252 """Returns the caboose (oldest bee group)."""
253 return self.caboose
255 def clear_caboose(self):
256 """Resets the caboose."""
257 if self.caboose:
258 self.caboose.reset()
259 self.caboose = None
261 def add(self, brood, colony, event, is_worker=True):
262 new_adult = Adult(brood.get_number())
263 if is_worker:
264 AdultList.WorkerCount += 1
265 else:
266 AdultList.DroneCount += 1
267 new_adult.set_mites(brood.get_mites())
268 new_adult.set_prop_virgins(brood.get_prop_virgins())
269 new_adult.set_lifespan(colony.wadllife)
270 brood.reset()
271 if len(self.bees) > 0:
272 adult = self.bees[0]
273 adult.set_number(adult.get_number() + new_adult.get_number())
274 adult.set_mites(adult.get_mites() + new_adult.get_mites())
275 adult.set_prop_virgins(
276 adult.get_mites().get_total()
277 if adult.get_mites().get_total() > 0
278 else 0
279 )
280 else:
281 self.bees.insert(0, new_adult)
283 def update(self, brood, colony, event, is_worker=True):
284 """Updates the adult list with incoming brood and ages existing adults."""
285 # Create new adult from brood
286 new_adult = self.get_bee_class()(brood.get_number())
287 if is_worker:
288 AdultList.WorkerCount += 1
289 else:
290 AdultList.DroneCount += 1
292 new_adult.set_mites(brood.get_mites())
293 new_adult.set_prop_virgins(brood.get_prop_virgins())
294 new_adult.set_lifespan(21) # WADLLIFE = 21
296 # Save emerging mites for colony
297 if hasattr(colony, "emerging_mites_w"):
298 if is_worker:
299 colony.emerging_mites_w = brood.get_mites()
300 colony.prop_emerging_virgins_w = brood.get_prop_virgins()
301 colony.num_emerging_brood_w = brood.get_number()
302 else:
303 colony.emerging_mites_d = brood.get_mites()
304 colony.prop_emerging_virgins_d = brood.get_prop_virgins()
305 colony.num_emerging_brood_d = brood.get_number()
307 brood.reset() # These brood are now gone
308 self.add_head(new_adult)
310 # Check if list is full - move oldest to caboose
311 if len(self.bees) >= (self._list_length + 1):
312 adult_to_remove = self.bees.pop()
313 # Use effective transition proportion based on date range values
314 effective_transition = self.get_effective_prop_transition(
315 event.get_time() if event else None
316 )
317 self.caboose.age = adult_to_remove.age
318 self.caboose.alive = adult_to_remove.alive
319 self.caboose.number = int(adult_to_remove.number * effective_transition)
321 if not is_worker:
322 # Drone adults die - update stats
323 if hasattr(colony, "m_InOutEvent"):
324 colony.m_InOutEvent.m_DeadDAdults = self.caboose.get_number()
325 self.caboose.reset()
326 AdultList.DroneCount -= 1
327 else:
328 self.caboose.reset()
330 # Check for age beyond reduced lifespan due to mite infestation (workers only)
331 if is_worker:
332 day = 1
333 for adult in self.bees:
334 num_mites = adult.get_mites()
335 if (
336 adult.is_alive()
337 and num_mites.get_total() > 0
338 and adult.get_number() > 0
339 ):
340 index = min(num_mites.get_total() // adult.get_number(), 7)
341 prop_redux = (
342 colony.long_redux[int(index)]
343 if hasattr(colony, "long_redux")
344 else 0
345 )
346 if day > (1 - prop_redux) * (
347 adult.get_lifespan() + colony.m_CurrentForagerLifespan
348 ):
349 adult.kill()
350 day += 1
352 def kill_all(self):
353 super().kill_all()
354 if self.caboose:
355 self.caboose.kill()
357 def update_length(self, length, is_worker=True):
358 """Adjusts the list length, moving excess bees to caboose or adding boxcars."""
359 if is_worker:
360 if length < len(self.bees):
361 adults_to_foragers = 0
362 while len(self.bees) > length and len(self.bees) > 0:
363 adult = self.bees.pop()
364 adults_to_foragers += adult.get_number()
365 # Add to caboose instead of deleting
366 if hasattr(self, "caboose"):
367 self.caboose.set_number(
368 self.caboose.get_number() + adult.get_number()
369 )
371 # Note: adults_to_foragers will become foragers in next Update call
372 else:
373 # Add empty boxcars if needed
374 while len(self.bees) < length:
375 self.bees.append(self.get_bee_class()())
377 self._list_length = length
379 def move_to_end(self, quantity_to_move, min_age):
380 """Moves the oldest quantity_to_move bees to the last boxcar, not moving any adults younger than min_age."""
381 if min_age < 0 or quantity_to_move <= 0:
382 return 0
383 total_moved = 0
384 end_index = len(self.bees) - 1
385 index = len(self.bees) - 2
386 while total_moved < quantity_to_move and index >= min_age:
387 currently_moving = self.get_quantity_at(index)
388 if currently_moving > quantity_to_move - total_moved:
389 currently_moving = quantity_to_move - total_moved
390 # Add currently_moving to the end boxcar
391 self.set_quantity_at(
392 end_index, currently_moving + self.get_quantity_at(end_index)
393 )
394 # Remove them from the current boxcar
395 self.set_quantity_at(index, self.get_quantity_at(index) - currently_moving)
396 total_moved += currently_moving
397 index -= 1
398 # Optionally, print status for debugging
399 # print(f"In move_to_end: {self.status()}")
400 return total_moved
403# --- ForagerListA class (inherits from AdultList to match C++ CForagerlistA : public CAdultlist) ---
404class ForagerListA(AdultList):
405 """Specialized adult list managing forager bee populations with unemployment tracking.
407 Extends AdultList to handle forager-specific dynamics including active vs. unemployed
408 foragers, pending forager pools, and proportional colony size limits on active foraging.
409 Implements the BEEPOP algorithm's forager management where only a proportion of potential
410 foragers are actively employed based on colony needs.
412 Attributes:
413 pending_foragers (list): Queue of unemployed forager Adult objects awaiting activation.
414 prop_actual_foragers (float): Proportion of colony that can be active foragers (default 0.3).
415 """
417 def __init__(self):
418 super().__init__()
419 self.pending_foragers = []
420 self.prop_actual_foragers = 0.3
422 def get_bee_class(self):
423 """Get the bee class for forager lists."""
424 return Adult # Foragers are adult bees
426 def clear_pending_foragers(self):
427 """Clears all pending foragers."""
428 self.pending_foragers.clear()
430 def get_quantity(self):
431 """Returns total foragers (active + pending)."""
432 return int(
433 super().get_quantity() + sum(a.get_number() for a in self.pending_foragers)
434 )
436 def get_active_quantity(self):
437 """Returns the number of active foragers, limited by colony size proportion."""
438 quan = self.get_quantity()
439 if quan > 0:
440 colony_size = self._colony.get_colony_size()
441 max_active = int(colony_size * self.prop_actual_foragers)
442 if quan > max_active:
443 quan = max_active
444 return quan
446 def get_unemployed_quantity(self):
447 """Returns the number of unemployed foragers."""
448 return self.get_quantity() - self.get_active_quantity()
450 def kill_all(self):
451 super().kill_all()
452 for bee in self.pending_foragers:
453 bee.kill()
454 self.clear_pending_foragers()
456 def set_prop_actual_foragers(self, proportion):
457 """Sets the actual foragers proportion."""
458 self.prop_actual_foragers = proportion
460 def get_prop_actual_foragers(self):
461 """Gets the actual foragers proportion."""
462 return self.prop_actual_foragers
464 def set_length(self, length):
465 """Sets the length of the forager list, adding/removing boxcars as needed."""
466 self._list_length = length # Fixed: use _list_length to match base class
467 while len(self.bees) > length:
468 self.bees.pop()
469 while len(self.bees) < length:
470 # Fixed: Create Adult objects for foragers, not base Bee objects
471 self.bees.append(Adult())
473 def update(self, adult, colony, event):
474 """Updates the forager list with incoming adults and manages pending foragers."""
475 WINTER_MORTALITY_PER_DAY = 0.10 / 152
477 # C++ logic: Always process the adult, even if number is 0
478 # C++ lines 285-287: Change lifespan from worker to forager
479 ForagerListA.WorkerCount -= 1
480 ForagerListA.ForagerCount += 1
481 adult.set_lifespan(colony.m_CurrentForagerLifespan)
482 adult.set_current_age(0.0)
484 pending_foragers_first = (
485 GlobalOptions.get().should_foragers_always_age_based_on_forage_inc
486 )
488 if pending_foragers_first:
489 # Add adult to pending foragers (even if 0 bees)
490 if len(self.pending_foragers) == 0:
491 add_adult = Adult()
492 add_adult.copy_from(adult)
493 self.pending_foragers.insert(0, add_adult)
494 else:
495 pending_adult = self.pending_foragers[0]
496 if pending_adult.get_forage_inc() == 0.0:
497 # Add to existing pending adult with 0 forage increment
498 pending_adult.set_number(
499 pending_adult.get_number() + adult.get_number()
500 )
501 adult.reset()
502 ForagerListA.ForagerCount -= 1
503 else:
504 # Create new pending adult
505 add_adult = Adult()
506 add_adult.copy_from(adult)
507 self.pending_foragers.insert(0, add_adult)
509 if event.is_forage_day():
510 if not pending_foragers_first:
511 # Add adult to pending foragers (even if 0 bees)
512 add_adult = Adult()
513 add_adult.copy_from(adult)
514 self.pending_foragers.insert(0, add_adult)
516 # Increment forage increment for all pending foragers
517 for pending_adult in self.pending_foragers:
518 pending_adult.set_forage_inc(
519 pending_adult.get_forage_inc() + event.get_forage_inc()
520 )
522 # Move pending foragers with >= 1.0 forage increment to main list
523 add_head = False
524 foragers_head = Adult()
525 to_remove = []
527 for i in reversed(range(len(self.pending_foragers))):
528 pending_adult = self.pending_foragers[i]
529 if pending_adult.get_forage_inc() >= 1.0:
530 add_head = True
531 foragers_head.set_number(
532 foragers_head.get_number() + pending_adult.get_number()
533 )
534 to_remove.append(i)
536 # Remove processed pending foragers
537 for i in to_remove:
538 del self.pending_foragers[i]
540 # Add new foragers to head of list
541 if add_head:
542 self.bees.insert(0, foragers_head)
543 # C++ logic: IMMEDIATELY remove tail when adding head (line 387-388)
544 if len(self.bees) > 1: # Only remove if there's something to remove
545 removed = self.bees.pop() # delete (RemoveTail())
547 # C++ logic: Additional check if list is still full (lines 390-394)
548 if len(self.bees) >= (self._list_length + 1):
549 self.caboose = self.bees.pop()
550 ForagerListA.ForagerCount -= 1
551 else:
552 self.caboose.reset()
553 else:
554 # When not adding head, still check if list is full and remove oldest
555 if len(self.bees) >= (self._list_length + 1):
556 self.caboose = self.bees.pop()
557 ForagerListA.ForagerCount -= 1
558 else:
559 self.caboose.reset()
561 elif not pending_foragers_first:
562 # Non-forage day - add adult (even if 0 bees) to first boxcar
563 if len(self.bees) == 0:
564 if self._list_length > 0:
565 add_adult = Adult()
566 add_adult.copy_from(adult)
567 self.pending_foragers.insert(0, add_adult)
568 self.bees.insert(0, add_adult)
569 else:
570 # Add to first boxcar (even 0 bees)
571 forager_head = self.bees[0]
572 forager_head.set_number(forager_head.get_number() + adult.get_number())
573 adult.reset()
574 ForagerListA.ForagerCount -= 1
576 # Age existing foragers and check for mite mortality (ALWAYS runs - this is key!)
577 self._age_existing_foragers(colony, event)
579 def _age_existing_foragers(self, colony, event):
580 """Ages existing foragers and applies mite mortality and winter mortality."""
581 WINTER_MORTALITY_PER_DAY = 0.10 / 152
583 # Check for lifespan reduction due to mite infestation
584 day = 21 + 1 # WADLLIFE + 1
585 colony.m_InOutEvent.m_PropRedux = 0
587 for list_adult in self.bees:
588 if list_adult.is_alive():
589 if list_adult.get_number() <= 0:
590 prop_redux = (
591 colony.long_redux[0] if hasattr(colony, "long_redux") else 0
592 )
593 else:
594 mite_index = min(
595 int(
596 list_adult.get_mites().get_total() / list_adult.get_number()
597 ),
598 (
599 len(colony.long_redux) - 1
600 if hasattr(colony, "long_redux")
601 else 0
602 ),
603 )
604 prop_redux = (
605 colony.long_redux[mite_index]
606 if hasattr(colony, "long_redux")
607 else 0
608 )
610 if prop_redux > 0:
611 colony.m_InOutEvent.m_PropRedux = (
612 list_adult.get_mites().get_total() / list_adult.get_number()
613 )
615 if day > (1 - prop_redux) * (
616 21 + colony.m_CurrentForagerLifespan
617 ): # WADLLIFE = 21
618 list_adult.kill()
619 day += 1
621 # Winter mortality (November through March)
622 if event.get_time().month >= 11 or event.get_time().month < 4:
623 for the_forager in self.bees:
624 number = the_forager.get_number()
625 new_number = int(number * (1 - WINTER_MORTALITY_PER_DAY))
626 if hasattr(colony, "m_InOutEvent"):
627 colony.m_InOutEvent.m_WinterMortalityForagersLoss += (
628 number - new_number
629 )
630 the_forager.set_number(new_number)
633class BroodList(BeeList):
634 """Age-structured list managing capped brood populations with mite infestation tracking.
636 Specialized BeeList for managing capped brood (pupae) using boxcar model dynamics.
637 Tracks mite infestation levels, distribution patterns, and provides colony health
638 metrics related to brood viability. Handles transitions from larvae to emerging adults
639 with proper mite load inheritance.
641 Attributes:
642 caboose (Brood): Oldest brood cohort ready for emergence as adults.
643 """
645 def __init__(self):
646 super().__init__()
647 self.caboose = Brood(0) # Initialize with empty brood, not None
649 def get_bee_class(self):
650 """Get the bee class for brood lists."""
651 return Brood
653 def get_caboose(self):
654 """Returns the caboose (oldest bee group)."""
655 return self.caboose
657 def clear_caboose(self):
658 """Resets the caboose."""
659 if self.caboose:
660 self.caboose.set_number(0)
661 self.caboose = None
663 def update(self, larva, current_date=None):
664 """Updates brood list with incoming larvae."""
665 new_brood = self.get_bee_class()(int(larva.get_number()))
666 larva.reset() # These larvae are now gone
667 self.add_head(new_brood)
669 if len(self.bees) >= (self._list_length + 1):
670 tail = self.bees.pop()
671 # Move to caboose with proper transition using effective proportion
672 effective_transition = self.get_effective_prop_transition(current_date)
673 self.caboose.age = tail.age
674 self.caboose.alive = tail.alive
675 self.caboose.set_mites(tail.get_mites())
676 self.caboose.set_prop_virgins(tail.get_prop_virgins())
677 self.caboose.number = int(tail.number * effective_transition)
678 else:
679 self.caboose.reset()
681 def get_mite_count(self):
682 """Returns total mite count in all brood."""
683 total_count = 0.0
684 for bee in self.bees:
685 if hasattr(bee, "mites"):
686 total_count += bee.mites.get_total()
687 return total_count
689 def get_prop_infest(self):
690 """Returns proportion of infested brood cells."""
691 total_cells = sum(bee.get_number() for bee in self.bees)
692 total_uninfested = 0
693 for bee in self.bees:
694 mite_count = bee.mites.get_total() if hasattr(bee, "mites") else 0
695 total_uninfested += max(bee.get_number() - mite_count, 0)
696 if total_cells > 0:
697 return 1 - (total_uninfested / total_cells)
698 return 0.0
700 def get_mites_per_cell(self):
701 """Returns average mites per brood cell."""
702 total_cells = sum(bee.get_number() for bee in self.bees)
703 total_mites = self.get_mite_count()
704 return (total_mites / total_cells) if total_cells > 0 else 0.0
706 def distribute_mites(self, mites):
707 """Distributes mites evenly across all brood."""
708 if not self.bees:
709 return
711 # Use total mite count divided by number of boxcars, like C++ version
712 mites_per_boxcar = mites.get_total() / len(self.bees)
713 percent_resistant = mites.get_pct_resistant()
715 for bee in self.bees:
716 # Match C++ behavior exactly:
717 # pBrood->m_Mites = MitesPerBoxcar (assignment operator)
718 # pBrood->m_Mites.SetPctResistant(PercentRes)
719 bee.mites.assign_value(mites_per_boxcar) # Matches C++ operator=(double)
720 bee.mites.set_pct_resistant(percent_resistant)
723class LarvaList(BeeList):
724 """Age-structured list managing larval bee populations during development phase.
726 Specialized BeeList for managing larvae using boxcar model dynamics. Handles
727 transitions from eggs to capped brood stage, tracking larval development timing
728 and survival rates.
730 Attributes:
731 caboose (Larva): Oldest larval cohort ready for transition to capped brood.
732 """
734 def get_bee_class(self):
735 """Get the bee class for larva lists."""
736 return Larva
738 def get_caboose(self):
739 """Returns the caboose (oldest bee group)."""
740 return self.caboose
742 def clear_caboose(self):
743 """Resets the caboose."""
744 if self.caboose:
745 self.caboose.set_number(0)
746 self.caboose = None
748 def __init__(self):
749 super().__init__()
750 self.caboose = Larva(0) # Initialize with empty larva, not None
752 def update(self, egg, current_date=None):
753 """Updates larva list with incoming eggs."""
754 new_larva = self.get_bee_class()(int(egg.get_number()))
755 egg.reset() # These eggs are now gone
756 self.add_head(new_larva)
758 if len(self.bees) >= (self._list_length + 1):
759 tail = self.bees.pop()
760 # Move to caboose with proper transition using effective proportion
761 effective_transition = self.get_effective_prop_transition(current_date)
762 self.caboose.age = tail.age
763 self.caboose.alive = tail.alive
764 self.caboose.number = int(tail.number * effective_transition)
765 else:
766 self.caboose.reset()
769# --- EggList class (Python port of CEgglist) ---
770class EggList(BeeList):
771 """Age-structured list managing egg populations from queen laying to larval emergence.
773 Specialized BeeList for managing eggs using boxcar model dynamics. Handles
774 initial egg placement from queen laying activities and tracks incubation timing
775 until eggs transition to larval stage. Forms the foundation of the colony's
776 reproductive pipeline.
778 Attributes:
779 caboose (Egg): Oldest egg cohort ready for transition to larval stage.
780 """
782 def __init__(self):
783 super().__init__()
784 self.caboose = Egg(0) # Initialize with empty egg, not None
786 def get_bee_class(self):
787 """Get the bee class for egg lists."""
788 return Egg
790 def get_caboose(self):
791 """Returns the caboose (oldest egg group)."""
792 return self.caboose
794 def update(self, new_egg, current_date=None):
795 """Updates egg list with new eggs from queen."""
796 new_eggs = self.get_bee_class()(new_egg.get_number())
797 self.add_head(new_eggs)
799 if len(self.bees) >= (self._list_length + 1):
800 tail = self.bees.pop()
801 # Move to caboose with proper transition using effective proportion
802 effective_transition = self.get_effective_prop_transition(current_date)
803 self.caboose.age = tail.age
804 self.caboose.alive = tail.alive
805 self.caboose.number = int(tail.number * effective_transition)
806 else:
807 self.caboose.reset()
809 def kill_all(self):
810 super().kill_all()