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

1"""BeePop+ Bee List Management Module. 

2 

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. 

7 

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. 

12 

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 

20 

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 

27 

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""" 

35 

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 

41 

42 

43class BeeList: 

44 """Base class for age-structured bee population management. 

45 

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. 

50 

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. 

54 

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 

59 

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 """ 

67 

68 # Class-level counters (shared across all instances) 

69 DroneCount = 0 

70 ForagerCount = 0 

71 WorkerCount = 0 

72 

73 def __init__(self): 

74 """Initialize a new BeeList with empty populations. 

75 

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 

85 

86 def set_length(self, length): 

87 """Set the list length (life stage duration).""" 

88 self._list_length = length 

89 

90 def get_length(self): 

91 """Get the list length (life stage duration).""" 

92 return self._list_length 

93 

94 def set_colony(self, colony): 

95 """Set reference to parent colony object.""" 

96 self._colony = colony 

97 

98 def get_colony(self): 

99 """Get reference to parent colony object.""" 

100 return self._colony 

101 

102 def set_prop_transition(self, prop): 

103 """Set proportion of bees transitioning from this list.""" 

104 self._prop_transition = prop 

105 

106 def get_prop_transition(self): 

107 """Get proportion of bees transitioning from this list.""" 

108 return self._prop_transition 

109 

110 def set_drv_list(self, drv_list): 

111 """Set the DateRangeValues object for this bee list.""" 

112 self._drv_list = drv_list 

113 

114 def get_drv_list(self): 

115 """Get the DateRangeValues object for this bee list.""" 

116 return self._drv_list 

117 

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 

127 

128 # If no date is provided, use the base proportion 

129 if current_date is None: 

130 return self._prop_transition 

131 

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 

140 

141 def clear(self): 

142 """Clears the bee list.""" 

143 self.bees = [] 

144 

145 def get_count(self): 

146 """Get the number of bees in the list (alias for get_quantity).""" 

147 return self.get_quantity() 

148 

149 def add_head(self, bee): 

150 """Add a bee to the head (beginning) of the list.""" 

151 self.bees.insert(0, bee) 

152 

153 def get_bee_class(self): 

154 """Get the bee class for this list type. Override in derived classes.""" 

155 return Egg 

156 

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) 

164 

165 def kill_all(self): 

166 """Kills all bees in the list.""" 

167 for bee in self.bees: 

168 bee.kill() 

169 

170 def remove_list_elements(self): 

171 """Removes all bees from the list and deletes them.""" 

172 self.bees.clear() 

173 

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 

180 

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) 

185 

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) 

190 

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) 

195 

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 

200 

201 list_length = len(self.bees) 

202 if to_idx > list_length - 1: 

203 to_idx = list_length - 1 

204 

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 

210 

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 

216 

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)) 

221 

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)) 

226 

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 

234 

235 

236# Derived classes (stubs, ported from colony.cpp) 

237# ForagerListA will be defined after AdultList since it inherits from it 

238 

239 

240class AdultList(BeeList): 

241 """Adult bee list for drones and workers. Ported from CAdultlist.""" 

242 

243 def __init__(self): 

244 super().__init__() 

245 self.caboose = Adult(0) # Initialize with empty adult, not None 

246 

247 def get_bee_class(self): 

248 """Get the bee class for adult lists.""" 

249 return Adult 

250 

251 def get_caboose(self): 

252 """Returns the caboose (oldest bee group).""" 

253 return self.caboose 

254 

255 def clear_caboose(self): 

256 """Resets the caboose.""" 

257 if self.caboose: 

258 self.caboose.reset() 

259 self.caboose = None 

260 

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) 

282 

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 

291 

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 

295 

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() 

306 

307 brood.reset() # These brood are now gone 

308 self.add_head(new_adult) 

309 

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) 

320 

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() 

329 

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 

351 

352 def kill_all(self): 

353 super().kill_all() 

354 if self.caboose: 

355 self.caboose.kill() 

356 

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 ) 

370 

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()()) 

376 

377 self._list_length = length 

378 

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 

401 

402 

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. 

406 

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. 

411 

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 """ 

416 

417 def __init__(self): 

418 super().__init__() 

419 self.pending_foragers = [] 

420 self.prop_actual_foragers = 0.3 

421 

422 def get_bee_class(self): 

423 """Get the bee class for forager lists.""" 

424 return Adult # Foragers are adult bees 

425 

426 def clear_pending_foragers(self): 

427 """Clears all pending foragers.""" 

428 self.pending_foragers.clear() 

429 

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 ) 

435 

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 

445 

446 def get_unemployed_quantity(self): 

447 """Returns the number of unemployed foragers.""" 

448 return self.get_quantity() - self.get_active_quantity() 

449 

450 def kill_all(self): 

451 super().kill_all() 

452 for bee in self.pending_foragers: 

453 bee.kill() 

454 self.clear_pending_foragers() 

455 

456 def set_prop_actual_foragers(self, proportion): 

457 """Sets the actual foragers proportion.""" 

458 self.prop_actual_foragers = proportion 

459 

460 def get_prop_actual_foragers(self): 

461 """Gets the actual foragers proportion.""" 

462 return self.prop_actual_foragers 

463 

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()) 

472 

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 

476 

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) 

483 

484 pending_foragers_first = ( 

485 GlobalOptions.get().should_foragers_always_age_based_on_forage_inc 

486 ) 

487 

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) 

508 

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) 

515 

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 ) 

521 

522 # Move pending foragers with >= 1.0 forage increment to main list 

523 add_head = False 

524 foragers_head = Adult() 

525 to_remove = [] 

526 

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) 

535 

536 # Remove processed pending foragers 

537 for i in to_remove: 

538 del self.pending_foragers[i] 

539 

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()) 

546 

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() 

560 

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 

575 

576 # Age existing foragers and check for mite mortality (ALWAYS runs - this is key!) 

577 self._age_existing_foragers(colony, event) 

578 

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 

582 

583 # Check for lifespan reduction due to mite infestation 

584 day = 21 + 1 # WADLLIFE + 1 

585 colony.m_InOutEvent.m_PropRedux = 0 

586 

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 ) 

609 

610 if prop_redux > 0: 

611 colony.m_InOutEvent.m_PropRedux = ( 

612 list_adult.get_mites().get_total() / list_adult.get_number() 

613 ) 

614 

615 if day > (1 - prop_redux) * ( 

616 21 + colony.m_CurrentForagerLifespan 

617 ): # WADLLIFE = 21 

618 list_adult.kill() 

619 day += 1 

620 

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) 

631 

632 

633class BroodList(BeeList): 

634 """Age-structured list managing capped brood populations with mite infestation tracking. 

635 

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. 

640 

641 Attributes: 

642 caboose (Brood): Oldest brood cohort ready for emergence as adults. 

643 """ 

644 

645 def __init__(self): 

646 super().__init__() 

647 self.caboose = Brood(0) # Initialize with empty brood, not None 

648 

649 def get_bee_class(self): 

650 """Get the bee class for brood lists.""" 

651 return Brood 

652 

653 def get_caboose(self): 

654 """Returns the caboose (oldest bee group).""" 

655 return self.caboose 

656 

657 def clear_caboose(self): 

658 """Resets the caboose.""" 

659 if self.caboose: 

660 self.caboose.set_number(0) 

661 self.caboose = None 

662 

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) 

668 

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() 

680 

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 

688 

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 

699 

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 

705 

706 def distribute_mites(self, mites): 

707 """Distributes mites evenly across all brood.""" 

708 if not self.bees: 

709 return 

710 

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() 

714 

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) 

721 

722 

723class LarvaList(BeeList): 

724 """Age-structured list managing larval bee populations during development phase. 

725 

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. 

729 

730 Attributes: 

731 caboose (Larva): Oldest larval cohort ready for transition to capped brood. 

732 """ 

733 

734 def get_bee_class(self): 

735 """Get the bee class for larva lists.""" 

736 return Larva 

737 

738 def get_caboose(self): 

739 """Returns the caboose (oldest bee group).""" 

740 return self.caboose 

741 

742 def clear_caboose(self): 

743 """Resets the caboose.""" 

744 if self.caboose: 

745 self.caboose.set_number(0) 

746 self.caboose = None 

747 

748 def __init__(self): 

749 super().__init__() 

750 self.caboose = Larva(0) # Initialize with empty larva, not None 

751 

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) 

757 

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() 

767 

768 

769# --- EggList class (Python port of CEgglist) --- 

770class EggList(BeeList): 

771 """Age-structured list managing egg populations from queen laying to larval emergence. 

772 

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. 

777 

778 Attributes: 

779 caboose (Egg): Oldest egg cohort ready for transition to larval stage. 

780 """ 

781 

782 def __init__(self): 

783 super().__init__() 

784 self.caboose = Egg(0) # Initialize with empty egg, not None 

785 

786 def get_bee_class(self): 

787 """Get the bee class for egg lists.""" 

788 return Egg 

789 

790 def get_caboose(self): 

791 """Returns the caboose (oldest egg group).""" 

792 return self.caboose 

793 

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) 

798 

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() 

808 

809 def kill_all(self): 

810 super().kill_all()