Coverage for pybeepop/beepop/colony.py: 82%

1146 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-19 20:58 +0000

1"""BeePop+ Colony Simulation Module. 

2 

3This module contains the core Colony class that manages all aspects of honey bee 

4colony dynamics and simulation. It serves as a Python port of the C++ CColony 

5class from the original BeePop+ system. 

6 

7The Colony class is the heart of the simulation system, coordinating all bee life 

8stages, mite populations, resource management, environmental responses, and 

9pesticide effects within a single honey bee colony. 

10 

11Architecture: 

12 The Colony class manages multiple interconnected subsystems: 

13 

14 Colony (Main Controller) 

15 ├── Queen (Egg laying and reproduction) 

16 ├── Bee Life Stages 

17 │ ├── EggList (Developing eggs) 

18 │ ├── LarvaList (Larval development) 

19 │ ├── BroodList (Pupal development) 

20 │ ├── AdultList (Adult workers and drones) 

21 │ └── ForagerListA (Foraging workers) 

22 ├── Mite Management 

23 │ ├── Running Mites (Free-living mites) 

24 │ ├── Brood Mites (Mites in cells) 

25 │ └── MiteTreatments (Treatment protocols) 

26 ├── Resource Management 

27 │ ├── ColonyResource (Pollen/nectar stores) 

28 │ └── Supplemental Feeding 

29 ├── Environmental Interaction 

30 │ ├── WeatherEvents (Daily conditions) 

31 │ ├── Daylight responses 

32 │ └── Seasonal patterns 

33 └── Toxicology 

34 ├── EPAData (Pesticide tracking) 

35 ├── NutrientContaminationTable (Exposure records) 

36 └── Mortality calculations 

37 

38Key Constants: 

39 EGGLIFE (int): Duration of egg stage (3 days) 

40 WLARVLIFE (int): Worker larva development time (5 days) 

41 DLARVLIFE (int): Drone larva development time (7 days) 

42 WBROODLIFE (int): Worker brood development time (13 days) 

43 DBROODLIFE (int): Drone brood development time (14 days) 

44 WADLLIFE (int): Worker adult lifespan (21 days) 

45 DADLLIFE (int): Drone adult lifespan (21 days) 

46 PROPINFSTW (float): Proportion of worker cells that get infested (0.08) 

47 PROPINFSTD (float): Proportion of drone cells that get infested (0.92) 

48 MAXMITES_PER_WORKER_CELL (int): Maximum mites per worker cell (4) 

49 MAXMITES_PER_DRONE_CELL (int): Maximum mites per drone cell (7) 

50 

51Notes: 

52 - This class is ported from C++ and maintains C++ naming conventions 

53 where necessary for compatibility 

54 - All bee populations are tracked in discrete age cohorts 

55 - Time progression is handled through daily update cycles 

56 - The class is designed to be deterministic for reproducible results 

57""" 

58 

59# Imports for referenced objects 

60import math 

61from datetime import datetime, timedelta 

62from types import SimpleNamespace 

63 

64from pybeepop.beepop.beelist import ( 

65 AdultList, 

66 BroodList, 

67 EggList, 

68 ForagerListA, 

69 LarvaList, 

70) 

71from pybeepop.beepop.brood import Brood 

72from pybeepop.beepop.coldstoragesimulator import ColdStorageSimulator 

73from pybeepop.beepop.colonyresource import ColonyResource, ResourceItem 

74from pybeepop.beepop.daterangevalues import DateRangeValues 

75from pybeepop.beepop.egg import Egg 

76from pybeepop.beepop.epadata import EPAData 

77from pybeepop.beepop.globaloptions import GlobalOptions 

78from pybeepop.beepop.mite import Mite 

79from pybeepop.beepop.mitetreatments import MiteTreatments 

80from pybeepop.beepop.nutrientcontaminationtable import NutrientContaminationTable 

81from pybeepop.beepop.queen import Queen 

82from pybeepop.beepop.spores import Spores 

83 

84# Life stage durations (from colony.h) 

85EGGLIFE = 3 

86DLARVLIFE = 7 

87WLARVLIFE = 5 

88DBROODLIFE = 14 

89WBROODLIFE = 13 

90DADLLIFE = 21 

91WADLLIFE = 21 

92 

93# Mite attributes 

94PROPINFSTW = 0.08 

95PROPINFSTD = 0.92 

96MAXMITES_PER_DRONE_CELL = 7 

97MAXMITES_PER_WORKER_CELL = 4 

98 

99# Discrete event codes 

100DE_NONE = 1 

101DE_SWARM = 2 

102DE_CHALKBROOD = 3 

103DE_RESOURCEDEP = 4 

104DE_SUPERCEDURE = 5 

105DE_PESTICIDE = 6 

106 

107 

108class InOutEvent: 

109 """Additional statistics container for detailed colony simulation output. 

110 

111 This class tracks transition events between bee life stages and mortality 

112 events for detailed analysis. These statistics are appended to normal 

113 simulation output when GlobalOptions.ShouldOutputInOutCounts() is activated. 

114 

115 Note: 

116 All counts are initialized to -1 to indicate unset values. 

117 Call reset() to reinitialize all values to -1. 

118 """ 

119 

120 def __init__(self): 

121 self.m_NewWEggs = -1 # new worker eggs 

122 self.m_NewDEggs = -1 # new drone eggs 

123 self.m_WEggsToLarv = -1 # worker eggs moving to larvae 

124 self.m_DEggsToLarv = -1 # drone eggs moving to larvae 

125 self.m_WLarvToBrood = -1 # worker larvae moving to brood 

126 self.m_DLarvToBrood = -1 # drone larvae moving to brood 

127 self.m_WBroodToAdult = -1 # worker drone moving to adult 

128 self.m_DBroodToAdult = -1 # drone drone moving to adult 

129 self.m_DeadDAdults = -1 # drone adult dying 

130 self.m_ForagersKilledByPesticide = -1 # forager killed by pesticide 

131 self.m_WAdultToForagers = -1 # worker adult moving to forager 

132 self.m_WinterMortalityForagersLoss = -1 # forager dying due to winter mortality 

133 self.m_DeadForagers = -1 # forager dying 

134 self.m_PropRedux = -1.0 # Debug for a double 

135 

136 def reset(self): 

137 """Reset all event counters to uninitialized state (-1). 

138 

139 This method reinitializes all tracking counters to -1, indicating 

140 that no events have been recorded for the current simulation day. 

141 Should be called at the beginning of each simulation day. 

142 """ 

143 self.m_NewWEggs = -1 

144 self.m_NewDEggs = -1 

145 self.m_WEggsToLarv = -1 

146 self.m_DEggsToLarv = -1 

147 self.m_WLarvToBrood = -1 

148 self.m_DLarvToBrood = -1 

149 self.m_WBroodToAdult = -1 

150 self.m_DBroodToAdult = -1 

151 self.m_DeadDAdults = -1 

152 self.m_ForagersKilledByPesticide = -1 

153 self.m_WAdultToForagers = -1 

154 self.m_WinterMortalityForagersLoss = -1 

155 self.m_DeadForagers = -1 

156 self.m_PropRedux = -1.0 

157 

158 

159class Colony: 

160 """Main simulation class for honey bee colony dynamics. 

161 

162 This class represents a single honey bee colony and manages all aspects of 

163 its simulation including bee populations across all life stages, mite 

164 infestations, resource management, environmental responses, and pesticide 

165 effects. It serves as a Python port of the C++ CColony class. 

166 

167 The Colony class operates on a daily time step, updating all bee populations, 

168 mite dynamics, resource consumption, and environmental interactions each 

169 simulation day. 

170 

171 """ 

172 

173 def __init__(self, session=None): 

174 """Initialize a new Colony instance. 

175 

176 Creates a new honey bee colony with default initial conditions, 

177 empty bee populations, and initialized subsystems for mites, 

178 resources, and environmental tracking. 

179 

180 Args: 

181 session (VarroaPopSession, optional): The simulation session that 

182 manages this colony. If None, the colony will operate 

183 independently. Defaults to None. 

184 

185 """ 

186 # Attributes from CColony constructor 

187 self.name = "" 

188 self.has_been_initialized = False 

189 self.prop_rm_virgins = 1.0 

190 self.long_redux = [0.0, 0.1, 0.2, 0.6, 0.9, 0.9, 0.9, 0.9] 

191 self.m_vt_treatment_active = False 

192 self.m_vt_enable = False 

193 

194 self.m_ColonyNecMaxAmount = 0 

195 self.m_ColonyPolMaxAmount = 0 

196 self.m_ColonyNecInitAmount = 0 

197 self.m_ColonyPolInitAmount = 0 

198 self.m_NoResourceKillsColony = False 

199 self.m_epadata = EPAData() 

200 self.resources = ColonyResource() # Changed from m_resources for consistency 

201 self.m_colony_event_list = [] 

202 self.m_nutrient_ct = NutrientContaminationTable() 

203 self.m_dead_worker_larvae_pesticide = 0 

204 self.m_dead_drone_larvae_pesticide = 0 

205 self.m_dead_worker_adults_pesticide = 0 

206 self.m_dead_drone_adults_pesticide = 0 

207 self.m_dead_foragers_pesticide = 0 

208 

209 self.m_event_map = {} 

210 self.queen = Queen() 

211 self.m_p_session = ( 

212 session # Accept session reference instead of creating new one 

213 ) 

214 # Add bee lists and other simulation objects as needed 

215 # Bee lists (port from CColony) 

216 self.foragers = ForagerListA() 

217 self.foragers.set_colony(self) # Set colony reference for C++ compatibility 

218 self.dadl = AdultList() # Drone adults 

219 self.wadl = AdultList() # Worker adults 

220 self.capwkr = BroodList() # Worker capped brood 

221 self.capdrn = BroodList() # Drone capped brood 

222 self.wlarv = LarvaList() # Worker larvae 

223 self.dlarv = LarvaList() # Drone larvae 

224 self.weggs = EggList() # Worker eggs 

225 self.deggs = EggList() # Drone eggs 

226 

227 # Lifespan constants (accessible as instance attributes for BeeList classes) 

228 self.egglife = EGGLIFE 

229 self.dlarvlife = DLARVLIFE 

230 self.wlarvlife = WLARVLIFE 

231 self.dbroodlife = DBROODLIFE 

232 self.wbroodlife = WBROODLIFE 

233 self.dadllife = DADLLIFE 

234 self.wadllife = WADLLIFE 

235 

236 # Mite state 

237 self.run_mite = Mite() # Free running mites 

238 self.prop_rm_virgins = 1.0 

239 self.emerging_mites_w = Mite() # Worker emerging mites 

240 self.prop_emerging_virgins_w = 0.0 

241 self.num_emerging_brood_w = 0 

242 self.emerging_mites_d = Mite() # Drone emerging mites 

243 self.prop_emerging_virgins_d = 0.0 

244 self.num_emerging_brood_d = 0 

245 

246 # Spore population 

247 self.m_spores = Spores() 

248 

249 # Mite treatment info 

250 self.m_mite_treatment_info = MiteTreatments() 

251 

252 # Initial conditions container (port from ColonyInitCond) 

253 self.m_init_cond = SimpleNamespace() 

254 self.m_init_cond.m_droneAdultInfestField = 0.0 

255 self.m_init_cond.m_droneBroodInfestField = 0.0 

256 self.m_init_cond.m_droneMiteOffspringField = 2.7 

257 self.m_init_cond.m_droneMiteSurvivorshipField = 100.0 

258 self.m_init_cond.m_workerAdultInfestField = 0.0 

259 self.m_init_cond.m_workerBroodInfestField = 0.0 

260 self.m_init_cond.m_workerMiteOffspring = 1.5 

261 self.m_init_cond.m_workerMiteSurvivorship = 100.0 

262 self.m_init_cond.m_droneAdultsField = 0 

263 self.m_init_cond.m_droneBroodField = 0 

264 self.m_init_cond.m_droneEggsField = 0 

265 self.m_init_cond.m_droneLarvaeField = 0 

266 self.m_init_cond.m_workerAdultsField = 5000 

267 self.m_init_cond.m_workerBroodField = 5000 

268 self.m_init_cond.m_workerEggsField = 5000 

269 self.m_init_cond.m_workerLarvaeField = 5000 

270 self.m_init_cond.m_totalEggsField = 0 

271 self.m_init_cond.m_QueenStrength = 4.0 

272 self.m_init_cond.m_ForagerLifespan = 12 

273 

274 # Initialize Date Range Value objects 

275 self.m_init_cond.m_AdultLifespanDRV = DateRangeValues() 

276 self.m_init_cond.m_ForagerLifespanDRV = DateRangeValues() 

277 self.m_init_cond.m_EggTransitionDRV = DateRangeValues() 

278 self.m_init_cond.m_BroodTransitionDRV = DateRangeValues() 

279 self.m_init_cond.m_LarvaeTransitionDRV = DateRangeValues() 

280 self.m_init_cond.m_AdultTransitionDRV = DateRangeValues() 

281 

282 # Adult aging delay parameters 

283 self.adult_aging_delay_armed = False 

284 self.m_days_since_egg_laying_began = 0 

285 self.m_adult_age_delay_limit = 24 # Default from CColony 

286 self.m_adult_aging_delay_egg_threshold = 50 # Default from CColony 

287 

288 # Feeding day flags 

289 self.m_pollen_feeding_day = False 

290 self.m_nectar_feeding_day = False 

291 

292 # Sample period and mite death tracking 

293 self.m_mites_dying_today = 0.0 

294 self.m_mites_dying_this_period = 0.0 

295 

296 # Additional attributes from colony.h 

297 self.m_SPStart = 0 

298 self.m_SPEnable = False 

299 self.m_SPTreatmentActive = False 

300 self.m_InitMitePctResistant = 0.0 

301 self.m_CurrentForagerLifespan = 0 

302 self.m_RQQueenStrengthArray = [] 

303 self.m_NutrientContEnabled = False 

304 self.m_SuppPollenEnabled = False 

305 self.m_SuppNectarEnabled = False 

306 self.m_SuppPollenAnnual = False 

307 self.m_SuppNectarAnnual = False 

308 

309 # Supplemental feeding resource objects (match C++ structure) 

310 self.m_SuppPollen = SimpleNamespace() 

311 self.m_SuppPollen.m_BeginDate = datetime.now() 

312 self.m_SuppPollen.m_EndDate = datetime.now() 

313 self.m_SuppPollen.m_CurrentAmount = 0.0 

314 self.m_SuppPollen.m_StartingAmount = 0.0 

315 

316 self.m_SuppNectar = SimpleNamespace() 

317 self.m_SuppNectar.m_BeginDate = datetime.now() 

318 self.m_SuppNectar.m_EndDate = datetime.now() 

319 self.m_SuppNectar.m_CurrentAmount = 0.0 

320 self.m_SuppNectar.m_StartingAmount = 0.0 

321 

322 self.m_InOutEvent = InOutEvent() 

323 

324 # Property aliases for consistent naming in consume_food methods 

325 @property 

326 def epa_data(self): 

327 return self.m_epadata 

328 

329 @property 

330 def nutrient_ct(self): 

331 return self.m_nutrient_ct 

332 

333 # Methods from colony.h not yet implemented 

334 def get_adult_aging_delay(self): 

335 return self.m_adult_age_delay_limit 

336 

337 def set_adult_aging_delay(self, delay): 

338 self.m_adult_age_delay_limit = delay 

339 

340 def get_adult_aging_delay_egg_threshold(self): 

341 return self.m_adult_aging_delay_egg_threshold 

342 

343 def set_adult_aging_delay_egg_threshold(self, threshold): 

344 self.m_adult_aging_delay_egg_threshold = threshold 

345 

346 def is_adult_aging_delay_armed(self): 

347 return self.adult_aging_delay_armed 

348 

349 def set_adult_aging_delay_armed(self, armed_state): 

350 self.adult_aging_delay_armed = armed_state 

351 

352 def set_initialized(self, val): 

353 self.has_been_initialized = val 

354 

355 def is_initialized(self): 

356 return self.has_been_initialized 

357 

358 def get_forager_lifespan(self): 

359 return self.m_init_cond.m_ForagerLifespan 

360 

361 def get_cold_storage_simulator(self): 

362 """Return the singleton instance of the cold storage simulator.""" 

363 return ColdStorageSimulator.get() 

364 

365 def get_adult_drones(self): 

366 """Get the total number of adult drones.""" 

367 return self.dadl.get_quantity() 

368 

369 def get_adult_workers(self): 

370 """Get the total number of adult workers.""" 

371 return self.wadl.get_quantity() 

372 

373 def get_foragers(self): 

374 """Get the total number of foragers.""" 

375 return self.foragers.get_quantity() 

376 

377 def get_active_foragers(self): 

378 """Get the number of active foragers following C++ logic.""" 

379 # Following C++ CForagerlistA::GetActiveQuantity() logic: 

380 # Limits active foragers to a proportion of total colony size 

381 return self.foragers.get_active_quantity() 

382 

383 def get_drone_brood(self): 

384 """Get the total number of drone brood.""" 

385 return self.capdrn.get_quantity() 

386 

387 def get_worker_brood(self): 

388 """Get the total number of worker brood.""" 

389 return self.capwkr.get_quantity() 

390 

391 def get_drone_larvae(self): 

392 """Get the total number of drone larvae.""" 

393 return self.dlarv.get_quantity() 

394 

395 def get_worker_larvae(self): 

396 """Get the total number of worker larvae.""" 

397 return self.wlarv.get_quantity() 

398 

399 def get_drone_eggs(self): 

400 """Get the total number of drone eggs.""" 

401 return self.deggs.get_quantity() 

402 

403 def get_worker_eggs(self): 

404 """Get the total number of worker eggs.""" 

405 return self.weggs.get_quantity() 

406 

407 def get_total_eggs_laid_today(self): 

408 """Get the total number of all eggs laid today.""" 

409 return self.queen.get_teggs() 

410 

411 def get_free_mites(self): 

412 """Get the number of free mites.""" 

413 return self.run_mite.get_total() 

414 

415 def get_drone_brood_mites(self): 

416 """Get the number of mites in drone brood.""" 

417 return self.capdrn.get_mite_count() 

418 

419 def get_worker_brood_mites(self): 

420 """Get the number of mites in worker brood.""" 

421 return self.capwkr.get_mite_count() 

422 

423 def get_mites_per_drone_brood(self): 

424 """Get the mites per drone brood ratio.""" 

425 return self.capdrn.get_mites_per_cell() 

426 

427 def get_mites_per_worker_brood(self): 

428 """Get the mites per worker brood ratio.""" 

429 return self.capwkr.get_mites_per_cell() 

430 

431 def get_prop_mites_dying(self): 

432 """Get the proportion of mites dying.""" 

433 if (self.get_mites_dying_this_period() + self.get_total_mite_count()) > 0: 

434 proportion_dying = self.get_mites_dying_this_period() / ( 

435 self.get_mites_dying_this_period() + self.get_total_mite_count() 

436 ) 

437 return proportion_dying 

438 else: 

439 return 0.0 

440 

441 def get_col_pollen(self): 

442 """Get colony pollen amount in grams.""" 

443 return self.resources.get_pollen_quantity() 

444 

445 def get_pollen_pest_conc(self): 

446 """Get pollen pesticide concentration in ug/g.""" 

447 return self.resources.get_pollen_pesticide_concentration() * 1000000.0 

448 

449 def get_col_nectar(self): 

450 """Get colony nectar amount in grams.""" 

451 return self.resources.get_nectar_quantity() 

452 

453 def get_nectar_pest_conc(self): 

454 """Get nectar pesticide concentration in ug/g.""" 

455 return self.resources.get_nectar_pesticide_concentration() * 1000000.0 

456 

457 # Pesticide-specific death getters (to match C++ output behavior) 

458 def get_dead_drone_larvae_pesticide(self): 

459 """Get number of drone larvae killed by pesticide.""" 

460 return getattr(self, "m_dead_drone_larvae_pesticide", 0) 

461 

462 def get_dead_worker_larvae_pesticide(self): 

463 """Get number of worker larvae killed by pesticide.""" 

464 return getattr(self, "m_dead_worker_larvae_pesticide", 0) 

465 

466 def get_dead_drone_adults_pesticide(self): 

467 """Get number of drone adults killed by pesticide.""" 

468 return getattr(self, "m_dead_drone_adults_pesticide", 0) 

469 

470 def get_dead_worker_adults_pesticide(self): 

471 """Get number of worker adults killed by pesticide.""" 

472 return getattr(self, "m_dead_worker_adults_pesticide", 0) 

473 

474 def get_dead_foragers_pesticide(self): 

475 """Get number of foragers killed by pesticide.""" 

476 return getattr(self, "m_dead_foragers_pesticide", 0) 

477 

478 def get_queen_strength(self): 

479 """Get the queen strength.""" 

480 return self.queen.get_strength() if self.queen else 0.0 

481 

482 def get_dd_lower(self): 

483 """Get the lower degree day value.""" 

484 return self.get_dd_today_lower() 

485 

486 def get_l_lower(self): 

487 """Get the lower L value.""" 

488 return self.get_l_today_lower() 

489 

490 def get_n_lower(self): 

491 """Get the lower N value.""" 

492 return self.get_n_today_lower() 

493 

494 def set_mite_pct_resistance(self, pct): 

495 """Set the percentage of the initial mite population resistant to treatment.""" 

496 self.m_InitMitePctResistant = pct 

497 

498 def set_vt_enable(self, value): 

499 self.m_vt_enable = value 

500 

501 def initialize_colony(self): 

502 # CRITICAL FIX: Set lengths of the various lists before initializing bees 

503 # This matches the C++ CColony::InitializeColony() logic 

504 self.deggs.set_length(EGGLIFE) 

505 self.deggs.set_prop_transition(1.0) 

506 self.weggs.set_length(EGGLIFE) 

507 self.weggs.set_prop_transition(1.0) 

508 self.dlarv.set_length(DLARVLIFE) 

509 self.dlarv.set_prop_transition(1.0) 

510 self.wlarv.set_length(WLARVLIFE) 

511 self.wlarv.set_prop_transition(1.0) 

512 self.capdrn.set_length(DBROODLIFE) 

513 self.capdrn.set_prop_transition(1.0) 

514 self.capwkr.set_length(WBROODLIFE) 

515 self.capwkr.set_prop_transition(1.0) 

516 self.dadl.set_length(DADLLIFE) 

517 self.dadl.set_prop_transition(1.0) 

518 self.wadl.set_length(WADLLIFE) 

519 self.wadl.set_prop_transition(1.0) 

520 self.wadl.set_colony(self) 

521 self.foragers.set_length(self.m_CurrentForagerLifespan) 

522 self.foragers.set_colony(self) 

523 

524 self.initialize_bees() 

525 self.initialize_mites() 

526 # Set pesticide Dose rate to 0 

527 if self.m_epadata: 

528 for attr in [ 

529 "m_D_L4", 

530 "m_D_L5", 

531 "m_D_LD", 

532 "m_D_A13", 

533 "m_D_A410", 

534 "m_D_A1120", 

535 "m_D_AD", 

536 "m_D_C_Foragers", 

537 "m_D_D_Foragers", 

538 "m_D_L4_Max", 

539 "m_D_L5_Max", 

540 "m_D_LD_Max", 

541 "m_D_A13_Max", 

542 "m_D_A410_Max", 

543 "m_D_A1120_Max", 

544 "m_D_AD_Max", 

545 "m_D_C_Foragers_Max", 

546 "m_D_D_Foragers_Max", 

547 ]: 

548 setattr(self.m_epadata, attr, 0) 

549 # Set resources 

550 if self.resources: 

551 self.resources.initialize( 

552 self.m_ColonyPolInitAmount, self.m_ColonyNecInitAmount 

553 ) 

554 if self.m_SuppPollen: 

555 self.m_SuppPollen.m_CurrentAmount = self.m_SuppPollen.m_StartingAmount 

556 if self.m_SuppNectar: 

557 self.m_SuppNectar.m_CurrentAmount = self.m_SuppNectar.m_StartingAmount 

558 # Set pesticide mortality trackers to zero 

559 self.m_dead_worker_larvae_pesticide = 0 

560 self.m_dead_drone_larvae_pesticide = 0 

561 self.m_dead_worker_adults_pesticide = 0 

562 self.m_dead_drone_adults_pesticide = 0 

563 self.m_dead_foragers_pesticide = 0 

564 self.m_colony_event_list.clear() 

565 # Nutrient contamination table logic (if enabled) 

566 # Note: In Python version, contamination table is loaded via set_contamination_table method 

567 # rather than loading from file during initialization 

568 if ( 

569 self.m_nutrient_ct 

570 and getattr(self.m_nutrient_ct, "is_enabled", lambda: False)() 

571 ): 

572 # Contamination table is already loaded via set_contamination_table 

573 pass 

574 # Set initial state of AdultAgingDelayArming 

575 if self.m_p_session: 

576 monthnum = self.m_p_session.get_sim_start().month 

577 # Ported logic for arming AdultAgingDelay 

578 # Set armed if the first date is January or February (C++: if ((monthnum >= 1) && (monthnum < 3))) 

579 if 1 <= monthnum < 3: 

580 self.adult_aging_delay_armed = True 

581 else: 

582 self.adult_aging_delay_armed = False 

583 self.has_been_initialized = True 

584 

585 def add_event_notification(self, date_stg, msg): 

586 event_string = f"{date_stg}: {msg}" 

587 if self.m_p_session and self.m_p_session.is_info_reporting_enabled(): 

588 self.m_colony_event_list.append(event_string) 

589 

590 def get_day_num_date(self, day_num): 

591 # Returns a date object for the given simulation day number 

592 if not self.m_p_session: 

593 return None 

594 sim_start = self.m_p_session.get_sim_start() 

595 # Use timedelta to add days to datetime object 

596 return sim_start + timedelta(days=day_num - 1) 

597 

598 def kill_colony(self): 

599 # Set queen strength to 1 (minimum) 

600 if self.queen: 

601 self.queen.set_strength(1) 

602 # Kill all bee lists (attributes must be set elsewhere) 

603 for attr in [ 

604 "deggs", 

605 "weggs", 

606 "dlarv", 

607 "wlarv", 

608 "capdrn", 

609 "capwkr", 

610 "dadl", 

611 "wadl", 

612 "foragers", 

613 ]: 

614 bee_list = getattr(self, attr, None) 

615 if bee_list: 

616 bee_list.kill_all() 

617 if hasattr(self, "foragers"): 

618 self.foragers.clear_pending_foragers() 

619 

620 def create(self): 

621 self.clear() # Clear all lists in case they have been built already 

622 

623 # Set lengths and prop transitions for all bee lists 

624 self.deggs.set_length(EGGLIFE) 

625 self.deggs.set_prop_transition(1.0) 

626 self.weggs.set_length(EGGLIFE) 

627 self.weggs.set_prop_transition(1.0) 

628 self.dlarv.set_length(DLARVLIFE) 

629 self.dlarv.set_prop_transition(1.0) 

630 self.wlarv.set_length(WLARVLIFE) 

631 self.wlarv.set_prop_transition(1.0) 

632 self.capdrn.set_length(DBROODLIFE) 

633 self.capdrn.set_prop_transition(1.0) 

634 self.capwkr.set_length(WBROODLIFE) 

635 self.capwkr.set_prop_transition(1.0) 

636 self.dadl.set_length(DADLLIFE) 

637 self.dadl.set_prop_transition(1.0) 

638 self.wadl.set_length(WADLLIFE) 

639 self.wadl.set_prop_transition(1.0) 

640 

641 # Set colony reference for lists that need it 

642 if hasattr(self.wadl, "set_colony"): 

643 self.wadl.set_colony(self) 

644 if hasattr(self.foragers, "set_length"): 

645 self.foragers.set_length( 

646 getattr(self, "m_init_cond", None).m_ForagerLifespan 

647 if hasattr(self, "m_init_cond") 

648 else 12 

649 ) 

650 if hasattr(self.foragers, "set_colony"): 

651 self.foragers.set_colony(self) 

652 

653 # Remove any current list boxcars in preparation for new initialization 

654 self.set_default_init_conditions() 

655 

656 def clear(self): 

657 # Clear all lists and simulation state 

658 for attr in [ 

659 "deggs", 

660 "weggs", 

661 "dlarv", 

662 "wlarv", 

663 "capdrn", 

664 "capwkr", 

665 "dadl", 

666 "wadl", 

667 "foragers", 

668 ]: 

669 bee_list = getattr(self, attr, None) 

670 if bee_list: 

671 bee_list.kill_all() 

672 self.m_colony_event_list.clear() 

673 

674 def is_adult_aging_delay_active(self): 

675 """ 

676 Returns True if adult aging delay is active (CColony::IsAdultAgingDelayActive). 

677 Logic matches C++ implementation exactly. 

678 """ 

679 # C++ logic: First check if armed and handle disarming 

680 egg_quant_threshold = self.get_adult_aging_delay_egg_threshold() 

681 

682 if self.is_adult_aging_delay_armed(): 

683 if self.queen.get_teggs() > egg_quant_threshold: 

684 self.set_adult_aging_delay_armed( 

685 False 

686 ) # Disarm when eggs exceed threshold 

687 self.m_days_since_egg_laying_began = 0 # Reset counter 

688 

689 # C++ logic: active = ((m_DaysSinceEggLayingBegan++ < m_AdultAgeDelayLimit) && !IsAdultAgingDelayArmed()); 

690 active = ( 

691 self.m_days_since_egg_laying_began < self.m_adult_age_delay_limit 

692 ) and not self.is_adult_aging_delay_armed() 

693 self.m_days_since_egg_laying_began += 1 # Increment counter (C++ does ++) 

694 

695 return active 

696 

697 def set_default_init_conditions(self): 

698 """ 

699 Sets default initial conditions for the colony (CColony::SetDefaultInitConditions). 

700 Resets bee lists and initial state variables. 

701 """ 

702 # Reset bee lists 

703 for bee_list in [ 

704 self.deggs, 

705 self.weggs, 

706 self.dlarv, 

707 self.wlarv, 

708 self.capdrn, 

709 self.capwkr, 

710 self.dadl, 

711 self.wadl, 

712 self.foragers, 

713 ]: 

714 if hasattr(bee_list, "clear"): 

715 bee_list.clear() 

716 # Reset initial conditions 

717 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit 

718 self.adult_aging_delay_armed = False 

719 self.m_dead_worker_larvae_pesticide = 0 

720 self.m_dead_drone_larvae_pesticide = 0 

721 self.m_dead_worker_adults_pesticide = 0 

722 self.m_dead_drone_adults_pesticide = 0 

723 self.m_dead_foragers_pesticide = 0 

724 self.m_colony_event_list.clear() 

725 # Optionally reset other state variables as needed 

726 

727 def initialize_bees(self): 

728 """ 

729 Ported from CColony::InitializeBees. 

730 Distributes bees from initial conditions into age groupings (boxcars) for each type. 

731 """ 

732 # Set current forager lifespan and adult aging delay 

733 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan 

734 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit 

735 

736 # Initialize Queen 

737 self.queen.set_strength(self.m_init_cond.m_QueenStrength) 

738 

739 # CRITICAL: Set forager length again to match C++ InitializeBees() exactly 

740 # This is needed because m_CurrentForagerLifespan may have changed from initial conditions 

741 self.foragers.set_length(self.m_CurrentForagerLifespan) 

742 self.foragers.set_colony(self) 

743 

744 # Helper to distribute bees into boxcars 

745 def distribute_bees(init_count, bee_list, bee_class): 

746 boxcar_len = bee_list.get_length() 

747 if boxcar_len == 0: 

748 return 

749 avg = init_count // boxcar_len 

750 remainder = init_count - avg * boxcar_len 

751 for i in range(boxcar_len): 

752 count = avg if i < (boxcar_len - 1) else avg + remainder 

753 bee = bee_class(count) 

754 bee_list.add_head(bee) 

755 

756 # Eggs 

757 distribute_bees( 

758 self.m_init_cond.m_droneEggsField, self.deggs, self.deggs.get_bee_class() 

759 ) 

760 distribute_bees( 

761 self.m_init_cond.m_workerEggsField, self.weggs, self.weggs.get_bee_class() 

762 ) 

763 

764 # Larvae 

765 distribute_bees( 

766 self.m_init_cond.m_droneLarvaeField, self.dlarv, self.dlarv.get_bee_class() 

767 ) 

768 distribute_bees( 

769 self.m_init_cond.m_workerLarvaeField, self.wlarv, self.wlarv.get_bee_class() 

770 ) 

771 

772 # Capped Brood 

773 distribute_bees( 

774 self.m_init_cond.m_droneBroodField, self.capdrn, self.capdrn.get_bee_class() 

775 ) 

776 distribute_bees( 

777 self.m_init_cond.m_workerBroodField, 

778 self.capwkr, 

779 self.capwkr.get_bee_class(), 

780 ) 

781 

782 # Drone Adults 

783 boxcar_len = self.dadl.get_length() 

784 avg = self.m_init_cond.m_droneAdultsField // boxcar_len if boxcar_len else 0 

785 remainder = ( 

786 self.m_init_cond.m_droneAdultsField - avg * boxcar_len if boxcar_len else 0 

787 ) 

788 for i in range(boxcar_len): 

789 count = avg if i < (boxcar_len - 1) else avg + remainder 

790 drone = self.dadl.get_bee_class()(count) 

791 drone.set_lifespan(DADLLIFE) 

792 self.dadl.add_head(drone) 

793 

794 # Worker Adults and Foragers 

795 total_boxcars = self.wadl.get_length() + self.foragers.get_length() 

796 avg = ( 

797 self.m_init_cond.m_workerAdultsField // total_boxcars 

798 if total_boxcars 

799 else 0 

800 ) 

801 remainder = ( 

802 self.m_init_cond.m_workerAdultsField - avg * total_boxcars 

803 if total_boxcars 

804 else 0 

805 ) 

806 for i in range(self.wadl.get_length()): 

807 worker = self.wadl.get_bee_class()(avg) 

808 worker.set_lifespan(WADLLIFE) 

809 self.wadl.add_head(worker) 

810 for i in range(self.foragers.get_length()): 

811 count = avg if i < (self.foragers.get_length() - 1) else avg + remainder 

812 forager = self.foragers.get_bee_class()(count) 

813 forager.set_lifespan(self.foragers.get_length()) 

814 self.foragers.add_head(forager) 

815 

816 # Set queen day one and egg laying delay 

817 self.queen.set_day_one(1) 

818 self.queen.set_egg_laying_delay(0) 

819 

820 def get_colony_size(self): 

821 """ 

822 Returns the total colony size (CColony::GetColonySize). 

823 Sum of drone adults, worker adults, and foragers. 

824 """ 

825 return int( 

826 self.dadl.get_quantity() 

827 + self.wadl.get_quantity() 

828 + self.foragers.get_quantity() 

829 ) 

830 

831 def update_bees(self, event, day_num): 

832 """ 

833 Ported from CColony::UpdateBees. 

834 Updates bee lists and colony state for the current day. 

835 """ 

836 # Calculate larvae per bee 

837 total_larvae = self.wlarv.get_quantity() + self.dlarv.get_quantity() 

838 total_adults = ( 

839 self.wadl.get_quantity() 

840 + self.dadl.get_quantity() 

841 + self.foragers.get_quantity() 

842 ) 

843 # Match C++ behavior - division by zero produces large value that triggers larv_per_bee > 2 

844 if total_adults == 0: 

845 larv_per_bee = float("inf") # Match C++ division by zero behavior 

846 else: 

847 larv_per_bee = float(total_larvae) / total_adults 

848 

849 # Arm Adult Aging Delay on Jan 1 

850 if event.get_time().month == 1 and event.get_time().day == 1: 

851 self.set_adult_aging_delay_armed(True) 

852 

853 # Apply date range values 

854 date_stg = event.get_date_stg("%m/%d/%Y") 

855 the_date = event.parse_date(date_stg) 

856 if the_date: 

857 # Eggs Transition Rate 

858 prop_transition = self.m_init_cond.m_EggTransitionDRV.get_active_value( 

859 the_date 

860 ) 

861 if ( 

862 prop_transition is not None 

863 and self.m_init_cond.m_EggTransitionDRV.is_enabled() 

864 ): 

865 self.deggs.set_prop_transition(prop_transition / 100) 

866 self.weggs.set_prop_transition(prop_transition / 100) 

867 else: 

868 self.deggs.set_prop_transition(1.0) 

869 self.weggs.set_prop_transition(1.0) 

870 # Larvae Transition Rate 

871 prop_transition = self.m_init_cond.m_LarvaeTransitionDRV.get_active_value( 

872 the_date 

873 ) 

874 if ( 

875 prop_transition is not None 

876 and self.m_init_cond.m_LarvaeTransitionDRV.is_enabled() 

877 ): 

878 self.dlarv.set_prop_transition(prop_transition / 100) 

879 self.wlarv.set_prop_transition(prop_transition / 100) 

880 else: 

881 self.dlarv.set_prop_transition(1.0) 

882 self.wlarv.set_prop_transition(1.0) 

883 # Brood Transition Rate 

884 prop_transition = self.m_init_cond.m_BroodTransitionDRV.get_active_value( 

885 the_date 

886 ) 

887 if ( 

888 prop_transition is not None 

889 and self.m_init_cond.m_BroodTransitionDRV.is_enabled() 

890 ): 

891 self.capdrn.set_prop_transition(prop_transition / 100) 

892 self.capwkr.set_prop_transition(prop_transition / 100) 

893 else: 

894 self.capdrn.set_prop_transition(1.0) 

895 self.capwkr.set_prop_transition(1.0) 

896 # Adults Transition Rate 

897 prop_transition = self.m_init_cond.m_AdultTransitionDRV.get_active_value( 

898 the_date 

899 ) 

900 if ( 

901 prop_transition is not None 

902 and self.m_init_cond.m_AdultTransitionDRV.is_enabled() 

903 ): 

904 self.dadl.set_prop_transition(prop_transition / 100) 

905 self.wadl.set_prop_transition(prop_transition / 100) 

906 else: 

907 self.dadl.set_prop_transition(1.0) 

908 self.wadl.set_prop_transition(1.0) 

909 # Adults Lifespan Change 

910 adult_age_limit = self.m_init_cond.m_AdultLifespanDRV.get_active_value( 

911 the_date 

912 ) 

913 if ( 

914 adult_age_limit is not None 

915 and self.m_init_cond.m_AdultLifespanDRV.is_enabled() 

916 ): 

917 if self.wadl.get_length() != int(adult_age_limit): 

918 self.wadl.update_length(int(adult_age_limit)) 

919 else: 

920 if self.wadl.get_length() != WADLLIFE: 

921 self.wadl.update_length(WADLLIFE) 

922 # Foragers Lifespan Change 

923 forager_lifespan = self.m_init_cond.m_ForagerLifespanDRV.get_active_value( 

924 the_date 

925 ) 

926 if ( 

927 forager_lifespan is not None 

928 and self.m_init_cond.m_ForagerLifespanDRV.is_enabled() 

929 ): 

930 self.m_CurrentForagerLifespan = int(forager_lifespan) 

931 else: 

932 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan 

933 self.foragers.set_length(self.m_CurrentForagerLifespan) 

934 else: 

935 self.deggs.set_prop_transition(1.0) 

936 self.weggs.set_prop_transition(1.0) 

937 self.dlarv.set_prop_transition(1.0) 

938 self.wlarv.set_prop_transition(1.0) 

939 self.capdrn.set_prop_transition(1.0) 

940 self.capwkr.set_prop_transition(1.0) 

941 self.dadl.set_prop_transition(1.0) 

942 self.wadl.set_prop_transition(1.0) 

943 

944 # Reset output data struct for algorithm intermediate results 

945 self.m_InOutEvent.reset() 

946 

947 # Queen lays eggs 

948 self.queen.lay_eggs( 

949 day_num, 

950 event.get_temp(), 

951 event.get_daylight_hours(), 

952 self.foragers.get_quantity(), 

953 larv_per_bee, 

954 ) 

955 

956 # Simulate cold storage 

957 cold_storage = self.get_cold_storage_simulator() 

958 today = event.get_date_stg() 

959 if cold_storage.is_enabled(): 

960 cs_state_stg = f"On {today} Cold Storage is ENABLED" 

961 cold_storage.update(event, self) 

962 if cold_storage.is_active_now(): 

963 cs_state_stg += " and ACTIVE" 

964 if cold_storage.is_starting_now(): 

965 cs_state_stg += " and STARTING" 

966 if cold_storage.is_ending_now(): 

967 cs_state_stg += "and ENDING" 

968 if cold_storage.is_on(): 

969 cs_state_stg += " and ON" 

970 if self.m_p_session.is_info_reporting_enabled(): 

971 self.m_p_session.add_to_info_list(cs_state_stg) 

972 

973 l_DEggs = Egg(self.queen.get_deggs()) 

974 l_WEggs = Egg(self.queen.get_weggs()) 

975 

976 # At the beginning of cold storage all eggs are lost 

977 if cold_storage.is_starting_now(): 

978 if self.m_p_session.is_info_reporting_enabled(): 

979 self.m_p_session.add_to_info_list( 

980 f"On {today} Cold Storage is STARTING" 

981 ) 

982 l_DEggs.set_number(0) 

983 l_WEggs.set_number(0) 

984 self.deggs.kill_all() 

985 self.weggs.kill_all() 

986 

987 # Update stats for new eggs 

988 self.m_InOutEvent.m_NewWEggs = l_WEggs.get_number() 

989 self.m_InOutEvent.m_NewDEggs = l_DEggs.get_number() 

990 

991 self.deggs.update(l_DEggs) 

992 self.weggs.update(l_WEggs) 

993 

994 # At the beginning of cold storage no eggs become larvae 

995 if cold_storage.is_starting_now(): 

996 self.weggs.get_caboose().reset() 

997 self.deggs.get_caboose().reset() 

998 

999 # Update stats for new larvae 

1000 self.m_InOutEvent.m_WEggsToLarv = self.weggs.get_caboose().get_number() 

1001 self.m_InOutEvent.m_DEggsToLarv = self.deggs.get_caboose().get_number() 

1002 

1003 self.dlarv.update(self.deggs.get_caboose()) 

1004 self.wlarv.update(self.weggs.get_caboose()) 

1005 

1006 # At the beginning of cold storage no larvae become brood 

1007 if cold_storage.is_starting_now(): 

1008 self.wlarv.get_caboose().reset() 

1009 self.dlarv.get_caboose().reset() 

1010 self.wlarv.kill_all() 

1011 self.dlarv.kill_all() 

1012 

1013 # Update stats for new brood 

1014 self.m_InOutEvent.m_WLarvToBrood = self.wlarv.get_caboose().get_number() 

1015 self.m_InOutEvent.m_DLarvToBrood = self.dlarv.get_caboose().get_number() 

1016 

1017 self.capdrn.update(self.dlarv.get_caboose()) 

1018 self.capwkr.update(self.wlarv.get_caboose()) 

1019 

1020 # Update stats for new adults 

1021 self.m_InOutEvent.m_WBroodToAdult = self.capwkr.get_caboose().get_number() 

1022 self.m_InOutEvent.m_DBroodToAdult = self.capdrn.get_caboose().get_number() 

1023 

1024 number_of_non_adults = ( 

1025 self.wlarv.get_quantity() 

1026 + self.dlarv.get_quantity() 

1027 + self.capdrn.get_quantity() 

1028 + self.capwkr.get_quantity() 

1029 ) 

1030 

1031 # ForageInc validity 

1032 global_options = GlobalOptions.get() 

1033 forage_inc_is_valid = ( 

1034 global_options.should_forage_day_election_based_on_temperatures 

1035 or event.get_forage_inc() > 0.0 

1036 ) 

1037 

1038 if (number_of_non_adults > 0) or ( 

1039 event.is_forage_day() and forage_inc_is_valid 

1040 ): 

1041 # Foragers killed due to pesticide 

1042 foragers_to_be_killed = self.quantity_pesticide_to_kill( 

1043 self.foragers, 

1044 self.m_epadata.m_D_C_Foragers, 

1045 0, 

1046 self.m_epadata.m_AI_AdultLD50_Contact, 

1047 self.m_epadata.m_AI_AdultSlope_Contact, 

1048 ) 

1049 foragers_to_be_killed += self.quantity_pesticide_to_kill( 

1050 self.foragers, 

1051 self.m_epadata.m_D_D_Foragers, 

1052 0, 

1053 self.m_epadata.m_AI_AdultLD50, 

1054 self.m_epadata.m_AI_AdultSlope, 

1055 ) 

1056 min_age_to_forager = 14 

1057 self.wadl.move_to_end(foragers_to_be_killed, min_age_to_forager) 

1058 if foragers_to_be_killed > 0: 

1059 notification = f"{foragers_to_be_killed} Foragers killed by pesticide - recruiting workers" 

1060 self.add_event_notification( 

1061 event.get_date_stg("%m/%d/%Y"), notification 

1062 ) 

1063 self.m_InOutEvent.m_ForagersKilledByPesticide = foragers_to_be_killed 

1064 

1065 # Aging adults 

1066 aging_adults = not cold_storage.is_active_now() and ( 

1067 not global_options.should_adults_age_based_laid_eggs 

1068 or self.queen.compute_L(event.get_daylight_hours()) > 0 

1069 ) 

1070 if self.is_adult_aging_delay_active(): 

1071 pass # Corresponds to C++: CString stgDate = pEvent->GetDateStg(); 

1072 aging_adults = ( 

1073 aging_adults 

1074 and not self.is_adult_aging_delay_active() 

1075 and not self.is_adult_aging_delay_armed() 

1076 ) 

1077 if aging_adults: 

1078 self.dadl.update(self.capdrn.get_caboose(), self, event, False) 

1079 wkr_adl_caboose_number = self.wadl.get_caboose().get_number() 

1080 self.wadl.update(self.capwkr.get_caboose(), self, event, True) 

1081 drn_number_from_caboose = self.capwkr.get_caboose().get_number() 

1082 wkr_adl_caboose_number = self.wadl.get_caboose().get_number() 

1083 self.m_InOutEvent.m_WAdultToForagers = ( 

1084 self.wadl.get_caboose().get_number() 

1085 ) 

1086 self.foragers.update(self.wadl.get_caboose(), self, event) 

1087 else: 

1088 if ( 

1089 number_of_non_adults > 0 

1090 and global_options.should_adults_age_based_laid_eggs 

1091 ): 

1092 self.dadl.add(self.capdrn.get_caboose(), self, event, False) 

1093 self.wadl.add(self.capwkr.get_caboose(), self, event, True) 

1094 self.m_InOutEvent.m_WAdultToForagers = 0 

1095 reset_adult = self.dadl.get_bee_class()() # CAdult reset 

1096 reset_adult.reset() 

1097 self.foragers.update(reset_adult, self, event) 

1098 self.m_InOutEvent.m_DeadForagers = max( 

1099 0, self.foragers.get_caboose().get_number() 

1100 ) 

1101 

1102 # Apply pesticide mortality impacts 

1103 self.consume_food(event, day_num) 

1104 self.determine_foliar_dose(day_num) 

1105 self.apply_pesticide_mortality() 

1106 

1107 def get_eggs_today(self): 

1108 # Returns the total eggs today (worker + drone) from Queen 

1109 return self.queen.get_teggs() 

1110 

1111 def get_dd_today(self): 

1112 # Returns DD value for today from Queen 

1113 return self.queen.get_DD() 

1114 

1115 def get_daylight_hrs_today(self, event=None): 

1116 # Returns daylight hours for today from Queen 

1117 return self.queen.get_L() 

1118 

1119 def get_l_today(self): 

1120 # Returns L value for today from Queen 

1121 return self.queen.get_L() 

1122 

1123 def get_n_today(self): 

1124 # Returns N value for today from Queen 

1125 return self.queen.get_N() 

1126 

1127 def get_p_today(self): 

1128 # Returns P value for today from Queen 

1129 return self.queen.get_P() 

1130 

1131 def get_dd_today_lower(self): 

1132 # Returns dd value for today (lowercase) from Queen 

1133 return self.queen.get_dd() 

1134 

1135 def get_l_today_lower(self): 

1136 # Returns l value for today (lowercase) from Queen 

1137 return self.queen.get_l() 

1138 

1139 def get_n_today_lower(self): 

1140 # Returns n value for today (lowercase) from Queen 

1141 return self.queen.get_n() 

1142 

1143 def add_mites(self, new_mites): 

1144 # Assume new mites are "virgins" (port of CColony::AddMites) 

1145 virgins = self.run_mite * self.prop_rm_virgins + new_mites 

1146 self.run_mite += new_mites 

1147 if self.run_mite.get_total() <= 0: 

1148 self.prop_rm_virgins = 1.0 

1149 else: 

1150 total_run = self.run_mite.get_total() 

1151 # avoid division by zero though handled above 

1152 self.prop_rm_virgins = ( 

1153 virgins.get_total() / total_run if total_run > 0 else 1.0 

1154 ) 

1155 # Constrain proportion to be [0..1] 

1156 self.prop_rm_virgins = min(self.prop_rm_virgins, 1.0) 

1157 self.prop_rm_virgins = max(self.prop_rm_virgins, 0.0) 

1158 

1159 def initialize_mites(self): 

1160 # Initial condition infestation of capped brood (port of CColony::InitializeMites) 

1161 w_count = int( 

1162 (self.capwkr.get_quantity() * self.m_init_cond.m_workerBroodInfestField) 

1163 / 100.0 

1164 ) 

1165 d_count = int( 

1166 (self.capdrn.get_quantity() * self.m_init_cond.m_droneBroodInfestField) 

1167 / 100.0 

1168 ) 

1169 w_mites = Mite(0, w_count) 

1170 d_mites = Mite(0, d_count) 

1171 # distribute_mites propagates the resistant proportion into each boxcar 

1172 w_mites.set_pct_resistant(self.m_InitMitePctResistant) 

1173 d_mites.set_pct_resistant(self.m_InitMitePctResistant) 

1174 # Distribute mites into capped brood 

1175 self.capwkr.distribute_mites(w_mites) 

1176 self.capdrn.distribute_mites(d_mites) 

1177 

1178 # Initial condition mites on adult bees i.e. Running Mites 

1179 run_w_count = int( 

1180 ( 

1181 self.wadl.get_quantity() * self.m_init_cond.m_workerAdultInfestField 

1182 + self.foragers.get_quantity() 

1183 * self.m_init_cond.m_workerAdultInfestField 

1184 ) 

1185 / 100.0 

1186 ) 

1187 run_d_count = int( 

1188 (self.dadl.get_quantity() * self.m_init_cond.m_droneAdultInfestField) 

1189 / 100.0 

1190 ) 

1191 run_mite_w = Mite(0, run_w_count) 

1192 run_mite_d = Mite(0, run_d_count) 

1193 run_mite_w.set_pct_resistant(self.m_InitMitePctResistant) 

1194 run_mite_d.set_pct_resistant(self.m_InitMitePctResistant) 

1195 

1196 self.run_mite = run_mite_d + run_mite_w 

1197 

1198 self.prop_rm_virgins = 1.0 

1199 

1200 self.m_mites_dying_today = 0.0 

1201 self.m_mites_dying_this_period = 0.0 

1202 

1203 def update_mites(self, event, day_num): 

1204 # Port of CColony::UpdateMites 

1205 # 

1206 # Assume UpdateMites is called after UpdateBees. This means the 

1207 # last Larva boxcar has been moved to the first Brood boxcar and the last 

1208 # Brood boxcar has been moved to the first Adult boxcar. Therefore, we infest 

1209 # the first Brood boxcar with mites and we have mites emerging from the 

1210 # first boxcar in the appropriate Adult list. 

1211 # 

1212 # The proportion of running mites that have not infested before (prop_rm_virgins) 

1213 # is maintained and updated each day. Mites with that proportion infest each day 

1214 # and the proportion is updated at the end of this function. 

1215 

1216 # Reset today's mite death counter 

1217 self.m_mites_dying_today = 0.0 

1218 

1219 # The cells being infested this cycle are the head (index 0) of capped brood lists 

1220 WkrBrood = ( 

1221 self.capwkr.bees[0] 

1222 if getattr(self.capwkr, "bees", None) and len(self.capwkr.bees) > 0 

1223 else None 

1224 ) 

1225 DrnBrood = ( 

1226 self.capdrn.bees[0] 

1227 if getattr(self.capdrn, "bees", None) and len(self.capdrn.bees) > 0 

1228 else None 

1229 ) 

1230 

1231 if WkrBrood is None: 

1232 # Nothing to do without brood 

1233 return 

1234 if DrnBrood is None: 

1235 # create an empty brood-like object for math, fallback 

1236 class _EmptyBrood: 

1237 def get_number(self): 

1238 return 0 

1239 

1240 DrnBrood = _EmptyBrood() 

1241 

1242 # Calculate proportion of RunMites that can invade cells (per Calis) 

1243 B = self.get_colony_size() * 0.125 # Weight in grams of colony 

1244 if B > 0.0: 

1245 rD = 6.49 * (DrnBrood.get_number() / B) 

1246 rW = 0.56 * (WkrBrood.get_number() / B) 

1247 I = 1 - math.exp(-(rD + rW)) 

1248 I = max(I, 0.0) 

1249 else: 

1250 I = 0.0 

1251 

1252 # WMites = RunMite * (I * PROPINFSTW) 

1253 WMites = self.run_mite * (I * PROPINFSTW) 

1254 

1255 # Likelihood of finding drone cell 

1256 if WkrBrood.get_number() > 0: 

1257 Likelihood = float(DrnBrood.get_number()) / float(WkrBrood.get_number()) 

1258 Likelihood = min(Likelihood, 1.0) 

1259 else: 

1260 Likelihood = 1.0 

1261 

1262 # DMites = RunMite * (I * PROPINFSTD * Likelihood) 

1263 DMites = self.run_mite * (I * PROPINFSTD * Likelihood) 

1264 

1265 # If no worker targets, send WMites to drone candidates 

1266 if WkrBrood.get_number() == 0: 

1267 DMites += WMites 

1268 WMites.set_resistant(0) 

1269 WMites.set_non_resistant(0) 

1270 

1271 # OverflowLikelihood = RunMite * (I * PROPINFSTD * (1.0 - Likelihood)); 

1272 OverflowLikelihood = self.run_mite * (I * PROPINFSTD * (1.0 - Likelihood)) 

1273 # Preserve pct resistant of DMites 

1274 try: 

1275 OverflowLikelihood.set_pct_resistant(DMites.get_pct_resistant()) 

1276 except Exception: 

1277 pass 

1278 

1279 # Determine if too many mites/drone cell. If so send excess to worker cells 

1280 OverflowMax = Mite(0, 0) 

1281 max_allowed = MAXMITES_PER_DRONE_CELL * DrnBrood.get_number() 

1282 if DMites.get_total() > max_allowed: 

1283 # Don't truncate to int - preserve floating point precision like C++ 

1284 overflow_count = DMites.get_total() - max_allowed 

1285 OverflowMax = Mite(0, overflow_count) 

1286 try: 

1287 OverflowMax.set_pct_resistant(DMites.get_pct_resistant()) 

1288 except Exception: 

1289 pass 

1290 # DMites -= OverflowMax 

1291 DMites = DMites - OverflowMax 

1292 

1293 # Add overflow mites to those available to infest worker brood 

1294 WMites = WMites + OverflowMax + OverflowLikelihood 

1295 

1296 # Limit worker mites per cell 

1297 max_w = MAXMITES_PER_WORKER_CELL * WkrBrood.get_number() 

1298 if WMites.get_total() > max_w: 

1299 pr = WMites.get_pct_resistant() 

1300 # Don't truncate to int - preserve floating point precision like C++ 

1301 WMites = Mite(0, max_w) 

1302 try: 

1303 WMites.set_pct_resistant(pr) 

1304 except Exception: 

1305 pass 

1306 

1307 # Remove the mites used to infest from running mites 

1308 self.run_mite = self.run_mite - WMites - DMites 

1309 if self.run_mite.get_total() < 0: 

1310 self.run_mite = Mite(0, 0) 

1311 

1312 # Assign mites to the brood head and set prop virgins 

1313 WkrBrood.set_mites(WMites) 

1314 if hasattr(WkrBrood, "set_prop_virgins"): 

1315 WkrBrood.set_prop_virgins(self.prop_rm_virgins) 

1316 DrnBrood.set_mites(DMites) 

1317 if hasattr(DrnBrood, "set_prop_virgins"): 

1318 DrnBrood.set_prop_virgins(self.prop_rm_virgins) 

1319 

1320 # Emerging mites from first adult boxcar 

1321 # Prepare emerge records - USE BROOD OBJECTS LIKE C++ 

1322 WkrHead = ( 

1323 self.wadl.bees[0] 

1324 if getattr(self.wadl, "bees", None) and len(self.wadl.bees) > 0 

1325 else None 

1326 ) 

1327 DrnHead = ( 

1328 self.dadl.bees[0] 

1329 if getattr(self.dadl, "bees", None) and len(self.dadl.bees) > 0 

1330 else None 

1331 ) 

1332 

1333 # Use actual Brood objects like C++ CBrood WkrEmerge; CBrood DrnEmerge; 

1334 WkrEmerge = Brood() 

1335 DrnEmerge = Brood() 

1336 

1337 if WkrHead: 

1338 WkrEmerge.number = int(WkrHead.get_number()) # Ensure integer type 

1339 WkrEmerge.set_prop_virgins( 

1340 WkrHead.get_prop_virgins() 

1341 if hasattr(WkrHead, "get_prop_virgins") 

1342 else 0.0 

1343 ) 

1344 if WkrHead.have_mites_been_counted(): 

1345 WkrEmerge.mites = Mite(0, 0) 

1346 else: 

1347 m = WkrHead.get_mites() if hasattr(WkrHead, "get_mites") else 0 

1348 if isinstance(m, Mite): 

1349 WkrEmerge.mites = m 

1350 else: 

1351 # Don't truncate to int - preserve floating point precision like C++ 

1352 WkrEmerge.mites = Mite(0, m) 

1353 WkrHead.set_mites_counted(True) 

1354 

1355 if DrnHead: 

1356 DrnEmerge.number = int(DrnHead.get_number()) # Ensure integer type 

1357 DrnEmerge.set_prop_virgins( 

1358 DrnHead.get_prop_virgins() 

1359 if hasattr(DrnHead, "get_prop_virgins") 

1360 else 0.0 

1361 ) 

1362 if DrnHead.have_mites_been_counted(): 

1363 DrnEmerge.mites = Mite(0, 0) 

1364 else: 

1365 m = DrnHead.get_mites() if hasattr(DrnHead, "get_mites") else 0 

1366 if isinstance(m, Mite): 

1367 DrnEmerge.mites = m 

1368 else: 

1369 # Don't truncate to int - preserve floating point precision like C++ 

1370 DrnEmerge.mites = Mite(0, m) 

1371 DrnHead.set_mites_counted(True) 

1372 

1373 # Mites per cell - USE DIRECT ACCESS LIKE C++ 

1374 MitesPerCellW = ( 

1375 WkrEmerge.mites.get_total() / WkrEmerge.number 

1376 if WkrEmerge.number > 0 

1377 else 0.0 

1378 ) 

1379 MitesPerCellD = ( 

1380 DrnEmerge.mites.get_total() / DrnEmerge.number 

1381 if DrnEmerge.number > 0 

1382 else 0.0 

1383 ) 

1384 

1385 # Survivorship 

1386 PropSurviveMiteW = self.m_init_cond.m_workerMiteSurvivorship / 100.0 

1387 PropSurviveMiteD = self.m_init_cond.m_droneMiteSurvivorshipField / 100.0 

1388 

1389 # Reproduction rates per mite per cell 

1390 if MitesPerCellW <= 1.0: 

1391 ReproMitePerCellW = self.m_init_cond.m_workerMiteOffspring 

1392 else: 

1393 ReproMitePerCellW = (1.15 * MitesPerCellW) - ( 

1394 0.233 * MitesPerCellW * MitesPerCellW 

1395 ) 

1396 if ReproMitePerCellW < 0: 

1397 ReproMitePerCellW = 0.0 

1398 

1399 if MitesPerCellD <= 2.0: 

1400 ReproMitePerCellD = self.m_init_cond.m_droneMiteOffspringField 

1401 else: 

1402 ReproMitePerCellD = ( 

1403 1.734 

1404 - (0.0755 * MitesPerCellD) 

1405 - (0.0069 * MitesPerCellD * MitesPerCellD) 

1406 ) 

1407 if ReproMitePerCellD < 0: 

1408 ReproMitePerCellD = 0.0 

1409 

1410 PROPRUNMITE2 = 0.6 

1411 

1412 SurviveMitesW = WkrEmerge.mites * PropSurviveMiteW 

1413 SurviveMitesD = DrnEmerge.mites * PropSurviveMiteD 

1414 

1415 NumEmergingMites = SurviveMitesW.get_total() + SurviveMitesD.get_total() 

1416 

1417 NewMitesW = SurviveMitesW * ReproMitePerCellW 

1418 NewMitesD = SurviveMitesD * ReproMitePerCellD 

1419 

1420 # Only mites which hadn't previously infested can survive to infest again. 

1421 SurviveMitesW = SurviveMitesW * WkrEmerge.get_prop_virgins() 

1422 SurviveMitesD = SurviveMitesD * DrnEmerge.get_prop_virgins() 

1423 

1424 NumVirgins = SurviveMitesW.get_total() + SurviveMitesD.get_total() 

1425 

1426 RunMiteVirgins = self.run_mite * self.prop_rm_virgins 

1427 RunMiteW = NewMitesW + (SurviveMitesW * PROPRUNMITE2) 

1428 RunMiteD = NewMitesD + (SurviveMitesD * PROPRUNMITE2) 

1429 

1430 # Mites dying today are the number which originally emerged from brood minus the ones that eventually became running mites 

1431 self.m_mites_dying_today = ( 

1432 WkrEmerge.mites.get_total() + DrnEmerge.mites.get_total() 

1433 ) 

1434 self.m_mites_dying_today = max(0.0, self.m_mites_dying_today) 

1435 

1436 # Add new running mites 

1437 self.run_mite = self.run_mite + RunMiteD + RunMiteW 

1438 

1439 # Update proportion of virgins 

1440 if self.run_mite.get_total() <= 0: 

1441 self.prop_rm_virgins = 1.0 

1442 else: 

1443 numerator = ( 

1444 RunMiteVirgins.get_total() 

1445 + NewMitesW.get_total() 

1446 + NewMitesD.get_total() 

1447 ) 

1448 self.prop_rm_virgins = ( 

1449 numerator / self.run_mite.get_total() 

1450 if self.run_mite.get_total() > 0 

1451 else 1.0 

1452 ) 

1453 # Clamp 

1454 self.prop_rm_virgins = min(self.prop_rm_virgins, 1.0) 

1455 self.prop_rm_virgins = max(self.prop_rm_virgins, 0.0) 

1456 

1457 # Kill NonResistant Running Mites if Treatment Enabled 

1458 if self.m_vt_enable and hasattr(self.m_mite_treatment_info, "get_active_item"): 

1459 the_date = self.get_day_num_date(day_num) 

1460 the_item = None 

1461 the_item = self.m_mite_treatment_info.get_active_item(the_date) 

1462 has_item = the_item is not None 

1463 if has_item and the_item: 

1464 Quan = self.run_mite.get_total() 

1465 # Only susceptible mites die; the resistant subpopulation survives, so 

1466 # repeated treatments select for resistance. 

1467 if hasattr(self.run_mite, "get_non_resistant") and hasattr( 

1468 self.run_mite, "set_non_resistant" 

1469 ): 

1470 new_nonres = ( 

1471 self.run_mite.get_non_resistant() 

1472 * (100.0 - the_item.pct_mortality) 

1473 / 100.0 

1474 ) 

1475 self.run_mite.set_non_resistant(new_nonres) 

1476 self.m_mites_dying_today += Quan - self.run_mite.get_total() 

1477 

1478 self.m_mites_dying_this_period += self.m_mites_dying_today 

1479 

1480 def requeen_if_needed( 

1481 self, 

1482 sim_day_num, 

1483 event, 

1484 egg_laying_delay, 

1485 wkr_drn_ratio, 

1486 enable_requeen, 

1487 scheduled, 

1488 queen_strength, 

1489 rq_once, 

1490 requeen_date, 

1491 ): 

1492 """ 

1493 Port of CColony::ReQueenIfNeeded 

1494 

1495 Two modes: 

1496 - Scheduled: trigger on ReQueenDate (initial exact year match, subsequent annual matches) 

1497 - Automatic: trigger when proportion of unfertilized (drone) eggs > 0.15 during Apr-Sep (months 4..9) 

1498 

1499 When requeening occurs, a strength may be popped from m_RQQueenStrengthArray (if present). 

1500 After requeening, egg laying is delayed by egg_laying_delay days (queen.requeen handles this). 

1501 """ 

1502 applied_strength = queen_strength 

1503 

1504 if not enable_requeen: 

1505 return 

1506 

1507 try: 

1508 if scheduled == 0: 

1509 # Scheduled re-queening: 

1510 # initial: year, month, day must match 

1511 # subsequent annual: year < current year and month/day match and rq_once != 0 

1512 ev_time = event.get_time() 

1513 try: 

1514 rd_year = requeen_date.year 

1515 rd_month = requeen_date.month 

1516 rd_day = requeen_date.day 

1517 except Exception: 

1518 # If requeen_date doesn't expose year/month/day, bail out (no scheduled requeen) 

1519 return 

1520 

1521 if ( 

1522 rd_year == ev_time.year 

1523 and rd_month == ev_time.month 

1524 and rd_day == ev_time.day 

1525 ) or ( 

1526 (rd_year < ev_time.year) 

1527 and (rd_month == ev_time.month) 

1528 and (rd_day == ev_time.day) 

1529 and (rq_once != 0) 

1530 ): 

1531 if self.m_RQQueenStrengthArray: 

1532 applied_strength = self.m_RQQueenStrengthArray.pop(0) 

1533 notification = f"Scheduled Requeening Occurred, Strength {applied_strength:5.1f}" 

1534 self.add_event_notification( 

1535 event.get_date_stg("%m/%d/%Y"), notification 

1536 ) 

1537 self.queen.requeen(egg_laying_delay, applied_strength, sim_day_num) 

1538 else: 

1539 # Automatic re-queening 

1540 month = event.get_time().month 

1541 if ( 

1542 (self.queen.get_prop_drone_eggs() > 0.15) 

1543 and (month > 3) 

1544 and (month < 10) 

1545 ): 

1546 if self.m_RQQueenStrengthArray: 

1547 applied_strength = self.m_RQQueenStrengthArray.pop(0) 

1548 notification = f"Automatic Requeening Occurred, Strength {applied_strength:5.1f}" 

1549 self.add_event_notification( 

1550 event.get_date_stg("%m/%d/%Y"), notification 

1551 ) 

1552 self.queen.requeen(egg_laying_delay, applied_strength, sim_day_num) 

1553 except Exception: 

1554 # On unexpected errors, do not requeen 

1555 return 

1556 

1557 # def set_miticide_treatment(self, start_day_num, duration, mortality, enable): 

1558 # pass 

1559 

1560 # def set_miticide_treatment_from_treatments(self, treatments, enable): 

1561 # pass 

1562 

1563 def set_spore_treatment(self, start_day_num, enable): 

1564 # Port of CColony::SetSporeTreatment 

1565 if enable: 

1566 self.m_SPStart = start_day_num 

1567 self.m_SPEnable = True 

1568 else: 

1569 self.m_SPEnable = False 

1570 self.m_SPTreatmentActive = False 

1571 

1572 def remove_drone_comb(self, pct): 

1573 # Port of CColony::RemoveDroneComb 

1574 # Simulates the removal of drone comb. The variable pct is the amount to be removed 

1575 # possible bug: when multiplying by the percentages, likely need to divide by 100 to convert to fraction 

1576 if pct > 100: 

1577 pct = 100.0 

1578 if pct < 0: 

1579 pct = 0.0 

1580 

1581 # Apply to drone eggs 

1582 for egg in getattr(self.deggs, "bees", []): 

1583 if hasattr(egg, "number"): 

1584 egg.number *= int( 

1585 100.0 - pct 

1586 ) # should this be *= (100.0 - pct) / 100.0 ? 

1587 

1588 # Apply to drone larvae 

1589 for larva in getattr(self.dlarv, "bees", []): 

1590 if hasattr(larva, "number"): 

1591 larva.number *= int( 

1592 100.0 - pct 

1593 ) # should this be *= (100.0 - pct) / 100.0 ? 

1594 

1595 # Apply to drone capped brood 

1596 for brood in getattr(self.capdrn, "bees", []): 

1597 if hasattr(brood, "number"): 

1598 brood.number *= int( 

1599 100.0 - pct 

1600 ) # should this be *= (100.0 - pct) / 100.0 ? 

1601 if hasattr(brood, "mites"): 

1602 # brood.m_Mites = brood.m_Mites * (100.0 - pct); 

1603 if hasattr(brood.mites, "__mul__"): 

1604 # Follow C++ logic: mites multiplied by floating point, not int 

1605 brood.mites *= 100.0 - pct # should this be (100.0 - pct) / 100 ?? 

1606 if hasattr(brood, "set_prop_virgins"): 

1607 brood.set_prop_virgins(0.0) 

1608 

1609 def add_discrete_event(self, date_stg, event_id): 

1610 # Port of CColony::AddDiscreteEvent 

1611 if date_stg in self.m_event_map: 

1612 # Date already exists, add a new event to the array 

1613 self.m_event_map[date_stg].append(event_id) 

1614 else: 

1615 # Create new map element 

1616 self.m_event_map[date_stg] = [event_id] 

1617 

1618 def remove_discrete_event(self, date_stg, event_id): 

1619 # Port of CColony::RemoveDiscreteEvent 

1620 if date_stg in self.m_event_map: 

1621 # Date exists 

1622 event_array = self.m_event_map[date_stg] 

1623 # Remove all occurrences of event_id 

1624 self.m_event_map[date_stg] = [x for x in event_array if x != event_id] 

1625 

1626 if len(self.m_event_map[date_stg]) == 0: 

1627 del self.m_event_map[date_stg] 

1628 

1629 def get_discrete_events(self, key): 

1630 # Port of CColony::GetDiscreteEvents 

1631 # Returns the event array for the given key, or None if not found 

1632 return self.m_event_map.get(key, None) 

1633 

1634 def do_pending_events(self, weather_event, current_sim_day): 

1635 # Port of CColony::DoPendingEvents 

1636 # DoPendingEvents is used when running WebBeePop. The predefined events from a legacy program are 

1637 # mapped into VarroaPop parameters and this is executed as part of the main simulation loop. A much 

1638 # simplified set of features for use by elementary school students. 

1639 

1640 event_array = self.get_discrete_events(weather_event.get_date_stg("%m/%d/%Y")) 

1641 if not event_array: 

1642 return 

1643 

1644 for event_id in event_array: 

1645 # TRACE("A Discrete Event on %s\n",pWeatherEvent->GetDateStg("%m/%d/%Y")); 

1646 EggLayDelay = 17 

1647 Strength = 5 

1648 

1649 if event_id == DE_SWARM: # Swarm 

1650 self.add_event_notification( 

1651 weather_event.get_date_stg("%m/%d/%Y"), 

1652 "Detected SWARM Discrete Event", 

1653 ) 

1654 if hasattr(self.foragers, "factor_quantity"): 

1655 self.foragers.factor_quantity(0.75) 

1656 if hasattr(self.wadl, "factor_quantity"): 

1657 self.wadl.factor_quantity(0.75) 

1658 if hasattr(self.dadl, "factor_quantity"): 

1659 self.dadl.factor_quantity(0.75) 

1660 

1661 elif event_id == DE_CHALKBROOD: # Chalk Brood 

1662 # All Larvae Die 

1663 self.add_event_notification( 

1664 weather_event.get_date_stg("%m/%d/%Y"), 

1665 "Detected CHALKBROOD Discrete Event", 

1666 ) 

1667 if hasattr(self.dlarv, "factor_quantity"): 

1668 self.dlarv.factor_quantity(0.0) 

1669 if hasattr(self.wlarv, "factor_quantity"): 

1670 self.wlarv.factor_quantity(0.0) 

1671 

1672 elif event_id == DE_RESOURCEDEP: # Resource Depletion 

1673 # Forager Lifespan = minimum 

1674 self.add_event_notification( 

1675 weather_event.get_date_stg("%m/%d/%Y"), 

1676 "Detected RESOURCEDEPLETION Discrete Event", 

1677 ) 

1678 self.m_init_cond.m_ForagerLifespan = 4 

1679 

1680 elif event_id == DE_SUPERCEDURE: # Supercedure of Queen 

1681 # New queen = 17 days before egg laying starts 

1682 self.add_event_notification( 

1683 weather_event.get_date_stg("%m/%d/%Y"), 

1684 "Detected SUPERCEDURE Discrete Event", 

1685 ) 

1686 if hasattr(self.queen, "requeen"): 

1687 self.queen.requeen(EggLayDelay, Strength, current_sim_day) 

1688 

1689 elif event_id == DE_PESTICIDE: # Death of foragers by pesticide 

1690 # 25% of foragers die 

1691 self.add_event_notification( 

1692 weather_event.get_date_stg("%m/%d/%Y"), 

1693 "Detected PESTICIDE Discrete Event", 

1694 ) 

1695 if hasattr(self.foragers, "factor_quantity"): 

1696 self.foragers.factor_quantity(0.75) 

1697 

1698 def get_mites_dying_today(self): 

1699 # Port of CColony::GetMitesDyingToday 

1700 return self.m_mites_dying_today 

1701 

1702 def get_nurse_bees(self): 

1703 # Port of CColony::GetNurseBees 

1704 # Number of nurse bees is defined as # larvae/2. Implication is that a nurse bee is needed for each two larvae 

1705 total_larvae = self.wlarv.get_quantity() + self.dlarv.get_quantity() 

1706 return total_larvae // 2 

1707 

1708 def get_total_mite_count(self): 

1709 # Port of CColony::GetTotalMiteCount 

1710 # return ( RunMite.GetTotal() + CapDrn.GetMiteCount() + CapWkr.GetMiteCount() ); 

1711 run_mite_total = ( 

1712 self.run_mite.get_total() if hasattr(self.run_mite, "get_total") else 0 

1713 ) 

1714 capdrn_mites = ( 

1715 self.capdrn.get_mite_count() 

1716 if hasattr(self.capdrn, "get_mite_count") 

1717 else 0 

1718 ) 

1719 capwkr_mites = ( 

1720 self.capwkr.get_mite_count() 

1721 if hasattr(self.capwkr, "get_mite_count") 

1722 else 0 

1723 ) 

1724 return run_mite_total + capdrn_mites + capwkr_mites 

1725 

1726 def set_start_sample_period(self): 

1727 # Port of CColony::SetStartSamplePeriod 

1728 # Notifies CColony that it is the beginning of a sample period. Since we gather either weekly or 

1729 # daily data this is used to reset accumulators. 

1730 self.m_mites_dying_this_period = 0.0 

1731 

1732 def get_mites_dying_this_period(self): 

1733 # Port of CColony::GetMitesDyingThisPeriod 

1734 return self.m_mites_dying_this_period 

1735 

1736 def apply_pesticide_mortality(self): 

1737 """ 

1738 Port of CColony::ApplyPesticideMortality 

1739 

1740 Applies pesticide mortality to different bee populations based on their current doses 

1741 compared to previously seen maximum doses. Updates mortality tracking variables. 

1742 

1743 Constraint: Bee quantities are not reduced unless the current pesticide dose is > previous maximum dose. But, 

1744 for bees just getting into Larva4 or Adult1, this is the first time they have had a dose. 

1745 

1746 """ 

1747 # Worker Larvae 4 

1748 # if (m_EPAData.m_D_L4 > m_EPAData.m_D_L4_Max) // IED - only reduce if current dose greater than previous maximum dose 

1749 # { 

1750 self.m_dead_worker_larvae_pesticide = self.apply_pesticide_to_bees( 

1751 self.wlarv, 

1752 3, 

1753 3, 

1754 self.m_epadata.m_D_L4, 

1755 0, 

1756 self.m_epadata.m_AI_LarvaLD50, 

1757 self.m_epadata.m_AI_LarvaSlope, 

1758 ) 

1759 self.m_epadata.m_D_L4_Max = max( 

1760 self.m_epadata.m_D_L4_Max, self.m_epadata.m_D_L4 

1761 ) 

1762 # } 

1763 

1764 # Worker Larvae 5 

1765 if self.m_epadata.m_D_L5 > self.m_epadata.m_D_L5_Max: 

1766 self.m_dead_worker_larvae_pesticide += self.apply_pesticide_to_bees( 

1767 self.wlarv, 

1768 4, 

1769 4, 

1770 self.m_epadata.m_D_L5, 

1771 self.m_epadata.m_D_L5_Max, 

1772 self.m_epadata.m_AI_LarvaLD50, 

1773 self.m_epadata.m_AI_LarvaSlope, 

1774 ) 

1775 self.m_epadata.m_D_L5_Max = self.m_epadata.m_D_L5 

1776 

1777 # Drone Larvae 

1778 self.m_dead_drone_larvae_pesticide = self.apply_pesticide_to_bees( 

1779 self.dlarv, 

1780 3, 

1781 3, 

1782 self.m_epadata.m_D_LD, 

1783 0, 

1784 self.m_epadata.m_AI_LarvaLD50, 

1785 self.m_epadata.m_AI_LarvaSlope, 

1786 ) # New L4 drones 

1787 if self.m_epadata.m_D_LD > self.m_epadata.m_D_LD_Max: 

1788 self.m_dead_drone_larvae_pesticide += self.apply_pesticide_to_bees( 

1789 self.dlarv, 

1790 4, 

1791 DLARVLIFE - 1, 

1792 self.m_epadata.m_D_LD, 

1793 self.m_epadata.m_D_LD_Max, 

1794 self.m_epadata.m_AI_LarvaLD50, 

1795 self.m_epadata.m_AI_LarvaSlope, 

1796 ) 

1797 self.m_epadata.m_D_LD_Max = self.m_epadata.m_D_LD 

1798 

1799 # Worker Adults 1-3 

1800 self.m_dead_worker_adults_pesticide = self.apply_pesticide_to_bees( 

1801 self.wadl, 

1802 0, 

1803 0, 

1804 self.m_epadata.m_D_A13, 

1805 0, 

1806 self.m_epadata.m_AI_AdultLD50, 

1807 self.m_epadata.m_AI_AdultSlope, 

1808 ) # New adults 

1809 if self.m_epadata.m_D_A13 > self.m_epadata.m_D_A13_Max: 

1810 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1811 self.wadl, 

1812 1, 

1813 2, 

1814 self.m_epadata.m_D_A13, 

1815 self.m_epadata.m_D_A13_Max, 

1816 self.m_epadata.m_AI_AdultLD50, 

1817 self.m_epadata.m_AI_AdultSlope, 

1818 ) 

1819 self.m_epadata.m_D_A13_Max = self.m_epadata.m_D_A13 

1820 

1821 # Worker Adults 4-10 

1822 if self.m_epadata.m_D_A410 > self.m_epadata.m_D_A410_Max: 

1823 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1824 self.wadl, 

1825 3, 

1826 9, 

1827 self.m_epadata.m_D_A410, 

1828 self.m_epadata.m_D_A410_Max, 

1829 self.m_epadata.m_AI_AdultLD50, 

1830 self.m_epadata.m_AI_AdultSlope, 

1831 ) 

1832 self.m_epadata.m_D_A410_Max = self.m_epadata.m_D_A410 

1833 

1834 # Worker Adults 11-20 

1835 if self.m_epadata.m_D_A1120 > self.m_epadata.m_D_A1120_Max: 

1836 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1837 self.wadl, 

1838 10, 

1839 WADLLIFE - 1, 

1840 self.m_epadata.m_D_A1120, 

1841 self.m_epadata.m_D_A1120_Max, 

1842 self.m_epadata.m_AI_AdultLD50, 

1843 self.m_epadata.m_AI_AdultSlope, 

1844 ) 

1845 self.m_epadata.m_D_A1120_Max = self.m_epadata.m_D_A1120 

1846 

1847 # Worker Drones 

1848 self.m_dead_drone_adults_pesticide = self.apply_pesticide_to_bees( 

1849 self.dadl, 

1850 0, 

1851 0, 

1852 self.m_epadata.m_D_AD, 

1853 0, 

1854 self.m_epadata.m_AI_AdultLD50, 

1855 self.m_epadata.m_AI_AdultSlope, 

1856 ) 

1857 if self.m_epadata.m_D_AD > self.m_epadata.m_D_AD_Max: 

1858 self.m_dead_drone_adults_pesticide += self.apply_pesticide_to_bees( 

1859 self.dadl, 

1860 1, 

1861 DADLLIFE - 1, 

1862 self.m_epadata.m_D_AD, 

1863 self.m_epadata.m_D_AD_Max, 

1864 self.m_epadata.m_AI_AdultLD50, 

1865 self.m_epadata.m_AI_AdultSlope, 

1866 ) 

1867 self.m_epadata.m_D_AD_Max = self.m_epadata.m_D_AD 

1868 

1869 # Foragers - Contact Mortality 

1870 self.m_dead_foragers_pesticide = self.apply_pesticide_to_bees( 

1871 self.foragers, 

1872 0, 

1873 0, 

1874 self.m_epadata.m_D_C_Foragers, 

1875 0, 

1876 self.m_epadata.m_AI_AdultLD50_Contact, 

1877 self.m_epadata.m_AI_AdultSlope_Contact, 

1878 ) 

1879 if self.m_epadata.m_D_C_Foragers > self.m_epadata.m_D_C_Foragers_Max: 

1880 # Use get_length() method if available, otherwise assume reasonable default 

1881 forager_length = getattr(self.foragers, "get_length", lambda: 21)() - 1 

1882 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1883 self.foragers, 

1884 1, 

1885 forager_length, 

1886 self.m_epadata.m_D_C_Foragers, 

1887 self.m_epadata.m_D_C_Foragers_Max, 

1888 self.m_epadata.m_AI_AdultLD50_Contact, 

1889 self.m_epadata.m_AI_AdultSlope_Contact, 

1890 ) 

1891 self.m_epadata.m_D_C_Foragers_Max = self.m_epadata.m_D_C_Foragers 

1892 

1893 # Foragers - Diet Mortality 

1894 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1895 self.foragers, 

1896 0, 

1897 0, 

1898 self.m_epadata.m_D_D_Foragers, 

1899 0, 

1900 self.m_epadata.m_AI_AdultLD50, 

1901 self.m_epadata.m_AI_AdultSlope, 

1902 ) 

1903 if self.m_epadata.m_D_D_Foragers > self.m_epadata.m_D_D_Foragers_Max: 

1904 # Use get_length() method if available, otherwise assume reasonable default 

1905 forager_length = getattr(self.foragers, "get_length", lambda: 21)() - 1 

1906 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1907 self.foragers, 

1908 1, 

1909 forager_length, 

1910 self.m_epadata.m_D_D_Foragers, 

1911 self.m_epadata.m_D_D_Foragers_Max, 

1912 self.m_epadata.m_AI_AdultLD50, 

1913 self.m_epadata.m_AI_AdultSlope, 

1914 ) 

1915 self.m_epadata.m_D_D_Foragers_Max = self.m_epadata.m_D_D_Foragers 

1916 

1917 if self.m_dead_foragers_pesticide > 0: 

1918 # Debug breakpoint placeholder (equivalent to int i = 0; in C++) 

1919 pass 

1920 

1921 # Reset the current doses to zero after mortality is applied. 

1922 self.m_epadata.m_D_L4 = 0 

1923 self.m_epadata.m_D_L5 = 0 

1924 self.m_epadata.m_D_LD = 0 

1925 self.m_epadata.m_D_A13 = 0 

1926 self.m_epadata.m_D_A410 = 0 

1927 self.m_epadata.m_D_A1120 = 0 

1928 self.m_epadata.m_D_AD = 0 

1929 self.m_epadata.m_D_C_Foragers = 0 

1930 self.m_epadata.m_D_D_Foragers = 0 

1931 

1932 def quantity_pesticide_to_kill(self, bee_list, current_dose, max_dose, ld50, slope): 

1933 """ 

1934 Port of CColony::QuantityPesticideToKill 

1935 

1936 This just calculates the number of bees in the list that would be killed by the pesticide and dose. 

1937 

1938 Args: 

1939 bee_list: The bee list to calculate mortality for 

1940 current_dose: Current pesticide dose 

1941 max_dose: Previously seen maximum dose 

1942 ld50: Lethal dose 50 value 

1943 slope: Dose-response slope parameter 

1944 

1945 Returns: 

1946 Number of bees that would be killed by pesticide 

1947 """ 

1948 bee_quant = bee_list.get_quantity() 

1949 

1950 # Calculate dose response for current and maximum doses 

1951 redux_current = self.m_epadata.dose_response(current_dose, ld50, slope) 

1952 redux_max = self.m_epadata.dose_response(max_dose, ld50, slope) 

1953 

1954 # Less than max already seen - no additional mortality 

1955 if redux_current <= redux_max: 

1956 return 0 

1957 

1958 # Calculate new bee quantity after mortality 

1959 new_bee_quant = int(bee_quant * (1 - (redux_current - redux_max))) 

1960 

1961 # Return the number killed by pesticide 

1962 return bee_quant - new_bee_quant 

1963 

1964 def apply_pesticide_to_bees( 

1965 self, bee_list, from_idx, to_idx, current_dose, max_dose, ld50, slope 

1966 ): 

1967 """ 

1968 Port of CColony::ApplyPesticideToBees 

1969 

1970 This calculates the number of bees to kill then reduces that number from all age groups 

1971 between "from_idx" and "to_idx" in the list. 

1972 

1973 Args: 

1974 bee_list: The bee list to apply mortality to 

1975 from_idx: Starting age index 

1976 to_idx: Ending age index 

1977 current_dose: Current pesticide dose 

1978 max_dose: Previously seen maximum dose 

1979 ld50: Lethal dose 50 value 

1980 slope: Dose-response slope parameter 

1981 

1982 Returns: 

1983 Number of bees killed by pesticide 

1984 """ 

1985 # Get bee quantity in the specified age range 

1986 bee_quant = int(bee_list.get_quantity_at_range(from_idx, to_idx)) 

1987 if bee_quant <= 0: 

1988 return 0 

1989 

1990 # Calculate dose response for current and maximum doses 

1991 redux_current = self.m_epadata.dose_response(current_dose, ld50, slope) 

1992 redux_max = self.m_epadata.dose_response(max_dose, ld50, slope) 

1993 

1994 # Less than max already seen - no additional mortality 

1995 if redux_current <= redux_max: 

1996 return 0 

1997 

1998 # Calculate new bee quantity after mortality 

1999 new_bee_quant = int(bee_quant * (1 - (redux_current - redux_max))) 

2000 prop_redux = new_bee_quant / bee_quant if bee_quant > 0 else 0 

2001 

2002 # Apply proportional reduction to the specified age range 

2003 bee_list.set_quantity_at_proportional(from_idx, to_idx, prop_redux) 

2004 

2005 # Return the number killed by pesticide 

2006 return int(bee_quant - new_bee_quant) 

2007 

2008 def determine_foliar_dose(self, day_num): 

2009 """ 

2010 Port of CColony::DetermineFoliarDose 

2011 

2012 If we are in a date range with Dose, this routine adds to the Dose rate variables. 

2013 

2014 Args: 

2015 day_num: The simulation day number 

2016 """ 

2017 # Jump out if Foliar is not enabled 

2018 if not self.m_epadata.m_FoliarEnabled: 

2019 return 

2020 

2021 # Get the current date (matches C++ COleDateTime* pDate = GetDayNumDate(DayNum)) 

2022 current_date = self.get_day_num_date(day_num) 

2023 

2024 # In order to expose, must be after the application date and inside the forage window 

2025 if ( 

2026 current_date >= self.m_epadata.m_FoliarAppDate 

2027 and current_date >= self.m_epadata.m_FoliarForageBegin 

2028 and current_date < self.m_epadata.m_FoliarForageEnd 

2029 ): 

2030 # Calculate days since application (matches C++ LONG DaysSinceApplication) 

2031 days_since_application = ( 

2032 current_date - self.m_epadata.m_FoliarAppDate 

2033 ).days 

2034 

2035 # Foliar Dose is related to AI application rate and Contact Exposure factor 

2036 # (See Kris Garber's EFED Training Insect Exposure.pptx for a summary) 

2037 dose = ( 

2038 self.m_epadata.m_E_AppRate 

2039 * self.m_epadata.m_AI_ContactFactor 

2040 / 1000000.0 

2041 ) # convert to Grams AI/bee 

2042 

2043 # Dose reduced due to active ingredient half-life 

2044 if self.m_epadata.m_AI_HalfLife > 0: 

2045 import math 

2046 

2047 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife 

2048 dose *= math.exp(-k * days_since_application) 

2049 

2050 # Adds to any diet-based exposure. Only foragers impacted. 

2051 self.m_epadata.m_D_C_Foragers += dose 

2052 

2053 def consume_food(self, event, day_num): 

2054 """ 

2055 Calculate colony food consumption and pesticide exposure. 

2056 

2057 Args: 

2058 event: Current event with date and forage information 

2059 day_num: Current day number in simulation 

2060 """ 

2061 # Skip food consumption on day 1 

2062 if day_num == 1: 

2063 return 

2064 

2065 # Calculate colony needs for pollen and nectar 

2066 pollen_need = self.get_pollen_needs(event) # grams 

2067 nectar_need = self.get_nectar_needs(event) # grams 

2068 

2069 # Get incoming resources from foraging 

2070 incoming_pollen = 0.0 

2071 incoming_nectar = 0.0 

2072 

2073 # Get incoming pesticide concentrations 

2074 c_ai_p = 0.0 # Pesticide concentration in incoming pollen 

2075 c_ai_n = 0.0 # Pesticide concentration in incoming nectar 

2076 

2077 if event.is_forage_day(): 

2078 incoming_pollen = self.get_incoming_pollen_quant() 

2079 incoming_nectar = self.get_incoming_nectar_quant() 

2080 c_ai_p = self.get_incoming_pollen_pesticide_concentration(day_num) 

2081 c_ai_n = self.get_incoming_nectar_pesticide_concentration(day_num) 

2082 

2083 # Process pollen consumption 

2084 c_actual_p = 0.0 

2085 

2086 # First check if supplemental pollen feeding is available 

2087 in_p = incoming_pollen 

2088 if self.is_pollen_feeding_day(event): 

2089 # C++ logic: All pollen needs are met by supplemental feeding 

2090 if self.m_SuppPollen.m_CurrentAmount >= pollen_need: 

2091 self.m_SuppPollen.m_CurrentAmount -= pollen_need 

2092 pollen_need = 0 # All needs met by supplemental feeding 

2093 c_ai_p = 0 # No pesticide in supplemental feed 

2094 

2095 if in_p >= pollen_need: 

2096 # Sufficient incoming pollen 

2097 c_actual_p = c_ai_p 

2098 # Add remaining pollen to resources 

2099 remaining_pollen = in_p - pollen_need 

2100 if remaining_pollen > 0: 

2101 pollen_resource = ResourceItem( 

2102 resource_quantity=remaining_pollen, 

2103 pesticide_quantity=remaining_pollen * c_ai_p, 

2104 ) 

2105 self.add_pollen_to_resources(pollen_resource) 

2106 else: 

2107 # Need to use stored pollen 

2108 shortfall = pollen_need - in_p 

2109 

2110 # Calculate resultant concentration [C1*Q1 + C2*Q2]/[Q1 + Q2] 

2111 stored_conc = self.resources.get_pollen_pesticide_concentration() 

2112 c_actual_p = ((c_ai_p * in_p) + (stored_conc * shortfall)) / ( 

2113 in_p + shortfall 

2114 ) 

2115 

2116 # Check if we have enough stored pollen 

2117 if self.resources.get_pollen_quantity() < shortfall: 

2118 if self.m_NoResourceKillsColony: 

2119 self.kill_colony() 

2120 date_str = event.get_date_stg() 

2121 self.add_event_notification( 

2122 date_str, "Colony Died - Lack of Pollen Stores" 

2123 ) 

2124 

2125 # Remove pollen from stores 

2126 self.resources.remove_pollen(shortfall) 

2127 

2128 # Process nectar consumption 

2129 c_actual_n = 0.0 

2130 

2131 # First check if supplemental nectar feeding is available 

2132 in_n = incoming_nectar 

2133 if self.is_nectar_feeding_day(event): 

2134 # C++ logic: Add daily nectar amount to resources 

2135 if ( 

2136 hasattr(self.m_SuppNectar, "m_StartingAmount") 

2137 and hasattr(self.m_SuppNectar, "m_BeginDate") 

2138 and hasattr(self.m_SuppNectar, "m_EndDate") 

2139 and self.m_SuppNectar.m_BeginDate is not None 

2140 and self.m_SuppNectar.m_EndDate is not None 

2141 ): 

2142 days_in_period = ( 

2143 self.m_SuppNectar.m_EndDate - self.m_SuppNectar.m_BeginDate 

2144 ).days 

2145 if days_in_period > 0: 

2146 daily_nectar_amount = ( 

2147 self.m_SuppNectar.m_StartingAmount / days_in_period 

2148 ) 

2149 if self.m_SuppNectar.m_CurrentAmount >= daily_nectar_amount: 

2150 nectar_resource = ResourceItem( 

2151 resource_quantity=daily_nectar_amount, 

2152 pesticide_quantity=0.0, # No pesticide in supplemental feed 

2153 ) 

2154 self.add_nectar_to_resources(nectar_resource) 

2155 self.m_SuppNectar.m_CurrentAmount -= daily_nectar_amount 

2156 c_ai_n = 0 # No pesticide in supplemental nectar 

2157 

2158 if in_n >= nectar_need: 

2159 # Sufficient incoming nectar 

2160 c_actual_n = c_ai_n 

2161 # Add remaining nectar to resources 

2162 remaining_nectar = in_n - nectar_need 

2163 if remaining_nectar > 0: 

2164 nectar_resource = ResourceItem( 

2165 resource_quantity=remaining_nectar, 

2166 pesticide_quantity=remaining_nectar * c_ai_n, 

2167 ) 

2168 self.add_nectar_to_resources(nectar_resource) 

2169 else: 

2170 # Need to use stored nectar 

2171 shortfall = nectar_need - in_n 

2172 

2173 # Calculate resultant concentration [C1*Q1 + C2*Q2]/[Q1 + Q2] 

2174 stored_conc = self.resources.get_nectar_pesticide_concentration() 

2175 c_actual_n = ((c_ai_n * in_n) + (stored_conc * shortfall)) / ( 

2176 in_n + shortfall 

2177 ) 

2178 

2179 # Check if we have enough stored nectar 

2180 if self.resources.get_nectar_quantity() < shortfall: 

2181 if self.m_NoResourceKillsColony: 

2182 self.kill_colony() 

2183 date_str = event.get_date_stg() 

2184 self.add_event_notification( 

2185 date_str, "Colony Died - Lack of Nectar Stores" 

2186 ) 

2187 

2188 # Remove nectar from stores 

2189 self.resources.remove_nectar(shortfall) 

2190 

2191 # Calculate diet doses for each life stage based on actual consumed concentrations 

2192 # Diet dose = concentration * consumption rate / 1000 (convert mg to g) 

2193 self.m_epadata.m_D_L4 = ( 

2194 c_actual_p * self.m_epadata.m_C_L4_Pollen / 1000.0 

2195 + c_actual_n * self.m_epadata.m_C_L4_Nectar / 1000.0 

2196 ) 

2197 self.m_epadata.m_D_L5 = ( 

2198 c_actual_p * self.m_epadata.m_C_L5_Pollen / 1000.0 

2199 + c_actual_n * self.m_epadata.m_C_L5_Nectar / 1000.0 

2200 ) 

2201 self.m_epadata.m_D_LD = ( 

2202 c_actual_p * self.m_epadata.m_C_LD_Pollen / 1000.0 

2203 + c_actual_n * self.m_epadata.m_C_LD_Nectar / 1000.0 

2204 ) 

2205 self.m_epadata.m_D_A13 = ( 

2206 c_actual_p * self.m_epadata.m_C_A13_Pollen / 1000.0 

2207 + c_actual_n * self.m_epadata.m_C_A13_Nectar / 1000.0 

2208 ) 

2209 self.m_epadata.m_D_A410 = ( 

2210 c_actual_p * self.m_epadata.m_C_A410_Pollen / 1000.0 

2211 + c_actual_n * self.m_epadata.m_C_A410_Nectar / 1000.0 

2212 ) 

2213 self.m_epadata.m_D_A1120 = ( 

2214 c_actual_p * self.m_epadata.m_C_A1120_Pollen / 1000.0 

2215 + c_actual_n * self.m_epadata.m_C_A1120_Nectar / 1000.0 

2216 ) 

2217 self.m_epadata.m_D_AD = ( 

2218 c_actual_p * self.m_epadata.m_C_AD_Pollen / 1000.0 

2219 + c_actual_n * self.m_epadata.m_C_AD_Nectar / 1000.0 

2220 ) 

2221 self.m_epadata.m_D_D_Foragers = ( 

2222 c_actual_p * self.m_epadata.m_C_Forager_Pollen / 1000.0 

2223 + c_actual_n * self.m_epadata.m_C_Forager_Nectar / 1000.0 

2224 ) 

2225 

2226 def get_pollen_needs(self, event): 

2227 """ 

2228 Calculate pollen needs in grams based on colony composition and season. 

2229 

2230 Args: 

2231 event: Current event with date and temperature information 

2232 

2233 Returns: 

2234 float: Pollen needs in grams 

2235 """ 

2236 need = 0.0 

2237 

2238 if event.is_winter_day(): 

2239 # Winter consumption - nurse bees have different rates 

2240 wadl_ag = [ 

2241 self.wadl.get_quantity_at_range(0, 2), # Ages 0-2 

2242 self.wadl.get_quantity_at_range(3, 9), # Ages 3-9 

2243 self.wadl.get_quantity_at_range(10, 19), # Ages 10-19 

2244 ] 

2245 

2246 consumption = [ 

2247 self.m_epadata.m_C_A13_Pollen / 1000.0, 

2248 self.m_epadata.m_C_A410_Pollen / 1000.0, 

2249 self.m_epadata.m_C_A1120_Pollen / 1000.0, 

2250 ] 

2251 

2252 nurse_bee_quantity = self.get_nurse_bees() 

2253 moved_nurse_bees = 0 

2254 

2255 # Allocate nurse bees from youngest age groups first 

2256 for i in range(3): 

2257 if wadl_ag[i] <= nurse_bee_quantity - moved_nurse_bees: 

2258 moved_nurse_bees += wadl_ag[i] 

2259 need += wadl_ag[i] * consumption[i] 

2260 else: 

2261 # Match C++ bug: update moved_nurse_bees first, then calculate need 

2262 # This causes (nurse_bee_quantity - moved_nurse_bees) to be 0 

2263 moved_nurse_bees += nurse_bee_quantity - moved_nurse_bees 

2264 need += (nurse_bee_quantity - moved_nurse_bees) * consumption[i] 

2265 

2266 if moved_nurse_bees >= nurse_bee_quantity: 

2267 break 

2268 

2269 # Non-nurse bees consume 2 mg per day 

2270 non_nurse_bees = self.get_colony_size() - moved_nurse_bees 

2271 need += non_nurse_bees * 0.002 

2272 

2273 # Add forager need 

2274 forager_need = 0.0 

2275 if event.is_forage_day(): 

2276 forager_need = ( 

2277 self.foragers.get_active_quantity() 

2278 * self.m_epadata.m_C_Forager_Pollen 

2279 / 1000.0 

2280 ) 

2281 forager_need += ( 

2282 (self.foragers.get_quantity() - self.foragers.get_active_quantity()) 

2283 * self.m_epadata.m_C_A1120_Pollen 

2284 / 1000.0 

2285 ) 

2286 else: 

2287 forager_need = self.foragers.get_quantity() * 0.002 

2288 

2289 need += forager_need # Already in grams 

2290 else: 

2291 # Non-winter day - calculate based on larvae and adult needs 

2292 # Larvae needs 

2293 l_needs = ( 

2294 self.wlarv.get_quantity_at(3) * self.m_epadata.m_C_L4_Pollen 

2295 + self.wlarv.get_quantity_at(4) * self.m_epadata.m_C_L5_Pollen 

2296 + self.dlarv.get_quantity() * self.m_epadata.m_C_LD_Pollen 

2297 ) 

2298 

2299 # Adult needs 

2300 if event.is_forage_day(): 

2301 a_needs = ( 

2302 self.wadl.get_quantity_at_range(0, 2) 

2303 * self.m_epadata.m_C_A13_Pollen 

2304 + self.wadl.get_quantity_at_range(3, 9) 

2305 * self.m_epadata.m_C_A410_Pollen 

2306 + self.wadl.get_quantity_at_range(10, 19) 

2307 * self.m_epadata.m_C_A1120_Pollen 

2308 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Pollen 

2309 + self.foragers.get_active_quantity() 

2310 * self.m_epadata.m_C_Forager_Pollen 

2311 + self.foragers.get_unemployed_quantity() 

2312 * self.m_epadata.m_C_A1120_Pollen 

2313 ) 

2314 else: 

2315 # All foragers consume like mature adults on non-forage days 

2316 a_needs = ( 

2317 self.wadl.get_quantity_at_range(0, 2) 

2318 * self.m_epadata.m_C_A13_Pollen 

2319 + self.wadl.get_quantity_at_range(3, 9) 

2320 * self.m_epadata.m_C_A410_Pollen 

2321 + ( 

2322 self.wadl.get_quantity_at_range(10, 19) 

2323 + self.foragers.get_quantity() 

2324 ) 

2325 * self.m_epadata.m_C_A1120_Pollen 

2326 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Pollen 

2327 ) 

2328 

2329 need = (l_needs + a_needs) / 1000.0 # Convert to grams 

2330 

2331 return need 

2332 

2333 def get_nectar_needs(self, event): 

2334 """ 

2335 Calculate nectar needs in grams based on colony composition and season. 

2336 

2337 Args: 

2338 event: Current event with date and temperature information 

2339 

2340 Returns: 

2341 float: Nectar needs in grams 

2342 """ 

2343 need = 0.0 

2344 

2345 if event.is_winter_day(): 

2346 colony_size = self.get_colony_size() 

2347 if colony_size > 0: 

2348 if event.get_temp() <= 8.5: 

2349 # See K. Garber's Winter Failure logic 

2350 need = 0.3121 * colony_size * pow(0.128 * colony_size, -0.48) 

2351 else: 

2352 # 8.5 < AveTemp < 18.0 

2353 if event.is_forage_day(): 

2354 # Foragers need normal forager nutrition 

2355 non_foragers = colony_size - self.foragers.get_active_quantity() 

2356 need = ( 

2357 self.foragers.get_active_quantity() 

2358 * self.m_epadata.m_C_Forager_Nectar 

2359 ) / 1000.0 + 0.05419 * non_foragers * pow( 

2360 0.128 * non_foragers, -0.27 

2361 ) 

2362 else: 

2363 # All bees consume at winter rates 

2364 need = 0.05419 * colony_size * pow(0.128 * colony_size, -0.27) 

2365 else: 

2366 # Summer day 

2367 # Larvae needs 

2368 l_needs = ( 

2369 self.wlarv.get_quantity_at(3) * self.m_epadata.m_C_L4_Nectar 

2370 + self.wlarv.get_quantity_at(4) * self.m_epadata.m_C_L5_Nectar 

2371 + self.dlarv.get_quantity() * self.m_epadata.m_C_LD_Nectar 

2372 ) 

2373 

2374 # Adult needs 

2375 if event.is_forage_day(): 

2376 a_needs = ( 

2377 self.wadl.get_quantity_at_range(0, 2) 

2378 * self.m_epadata.m_C_A13_Nectar 

2379 + self.wadl.get_quantity_at_range(3, 9) 

2380 * self.m_epadata.m_C_A410_Nectar 

2381 + self.wadl.get_quantity_at_range(10, 19) 

2382 * self.m_epadata.m_C_A1120_Nectar 

2383 + self.foragers.get_unemployed_quantity() 

2384 * self.m_epadata.m_C_A1120_Nectar 

2385 + self.foragers.get_active_quantity() 

2386 * self.m_epadata.m_C_Forager_Nectar 

2387 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Nectar 

2388 ) 

2389 else: 

2390 # Foragers consume like mature adults 

2391 a_needs = ( 

2392 self.wadl.get_quantity_at_range(0, 2) 

2393 * self.m_epadata.m_C_A13_Nectar 

2394 + self.wadl.get_quantity_at_range(3, 9) 

2395 * self.m_epadata.m_C_A410_Nectar 

2396 + ( 

2397 self.wadl.get_quantity_at_range(10, 19) 

2398 + self.foragers.get_quantity() 

2399 ) 

2400 * self.m_epadata.m_C_A1120_Nectar 

2401 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Nectar 

2402 ) 

2403 

2404 need = (l_needs + a_needs) / 1000.0 # Convert to grams 

2405 

2406 return need 

2407 

2408 def get_incoming_pollen_quant(self): 

2409 """ 

2410 Calculate incoming pollen quantity in grams from foraging. 

2411 

2412 Returns: 

2413 float: Incoming pollen in grams 

2414 """ 

2415 pollen = 0.0 

2416 # Only bring in pollen if there are larvae 

2417 if (self.wlarv.get_quantity() + self.dlarv.get_quantity()) > 0: 

2418 pollen = ( 

2419 self.foragers.get_active_quantity() 

2420 * self.m_epadata.m_I_PollenTrips 

2421 * self.m_epadata.m_I_PollenLoad 

2422 / 1000.0 

2423 ) 

2424 return pollen 

2425 

2426 def get_incoming_nectar_quant(self): 

2427 """ 

2428 Calculate incoming nectar quantity in grams from foraging. 

2429 

2430 Returns: 

2431 float: Incoming nectar in grams 

2432 """ 

2433 nectar = ( 

2434 self.foragers.get_active_quantity() 

2435 * self.m_epadata.m_I_NectarTrips 

2436 * self.m_epadata.m_I_NectarLoad 

2437 / 1000.0 

2438 ) 

2439 

2440 # If there are no larvae, all pollen foraging trips become nectar trips 

2441 if (self.wlarv.get_quantity() + self.dlarv.get_quantity()) <= 0: 

2442 nectar += ( 

2443 self.foragers.get_active_quantity() 

2444 * self.m_epadata.m_I_PollenTrips 

2445 * self.m_epadata.m_I_NectarLoad 

2446 / 1000.0 

2447 ) 

2448 

2449 return nectar 

2450 

2451 def get_incoming_pollen_pesticide_concentration(self, day_num): 

2452 """ 

2453 Calculate incoming pollen pesticide concentration accounting for decay. 

2454 

2455 Args: 

2456 day_num: Current day number in simulation 

2457 

2458 Returns: 

2459 float: Pesticide concentration in grams AI per gram pollen 

2460 """ 

2461 incoming_concentration = 0.0 

2462 cur_date = self.get_day_num_date(day_num) 

2463 

2464 # Check if using nutrient contamination table 

2465 if self.nutrient_ct.is_enabled(): 

2466 nectar_conc, pollen_conc = self.nutrient_ct.get_contaminant_conc(cur_date) 

2467 incoming_concentration = pollen_conc 

2468 else: 

2469 # Normal foliar spray process 

2470 if ( 

2471 self.m_epadata.m_FoliarEnabled 

2472 and cur_date >= self.m_epadata.m_FoliarAppDate 

2473 and cur_date >= self.m_epadata.m_FoliarForageBegin 

2474 and cur_date < self.m_epadata.m_FoliarForageEnd 

2475 ): 

2476 # Base concentration from foliar spray 

2477 incoming_concentration = 110.0 * self.m_epadata.m_E_AppRate / 1000000.0 

2478 # Apply decay due to active ingredient half-life 

2479 days_since_application = ( 

2480 cur_date - self.m_epadata.m_FoliarAppDate 

2481 ).days 

2482 if self.m_epadata.m_AI_HalfLife > 0: 

2483 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife 

2484 incoming_concentration *= math.exp(-k * days_since_application) 

2485 

2486 self.add_event_notification( 

2487 cur_date.strftime("%m/%d/%Y"), 

2488 "Incoming Foliar Spray Pollen Pesticide", 

2489 ) 

2490 

2491 # Seed treatment exposure 

2492 if ( 

2493 cur_date >= self.m_epadata.m_SeedForageBegin 

2494 and cur_date < self.m_epadata.m_SeedForageEnd 

2495 and self.m_epadata.m_SeedEnabled 

2496 ): 

2497 incoming_concentration += ( 

2498 self.m_epadata.m_E_SeedAppRate * 18e-9 

2499 ) # 18 ng ai/g per (mg ai/seed) 

2500 self.add_event_notification( 

2501 cur_date.strftime("%m/%d/%Y"), "Incoming Seed Pollen Pesticide" 

2502 ) 

2503 

2504 # Soil contamination exposure 

2505 if ( 

2506 cur_date >= self.m_epadata.m_SoilForageBegin 

2507 and cur_date < self.m_epadata.m_SoilForageEnd 

2508 and self.m_epadata.m_SoilEnabled 

2509 ): 

2510 if self.m_epadata.m_AI_KOW > 0 or self.m_epadata.m_E_SoilTheta != 0: 

2511 log_kow = math.log10(self.m_epadata.m_AI_KOW) 

2512 tscf = -0.0648 * (log_kow * log_kow) + 0.241 * log_kow + 0.5822 

2513 soil_conc = ( 

2514 tscf 

2515 * (pow(10, (0.95 * log_kow - 2.05)) + 0.82) 

2516 * self.m_epadata.m_E_SoilConcentration 

2517 * ( 

2518 self.m_epadata.m_E_SoilP 

2519 / ( 

2520 self.m_epadata.m_E_SoilTheta 

2521 + self.m_epadata.m_E_SoilP 

2522 * self.m_epadata.m_AI_KOC 

2523 * self.m_epadata.m_E_SoilFoc 

2524 ) 

2525 ) 

2526 ) 

2527 incoming_concentration += soil_conc / 1000000.0 

2528 self.add_event_notification( 

2529 cur_date.strftime("%m/%d/%Y"), "Incoming Soil Pollen Pesticide" 

2530 ) 

2531 

2532 return incoming_concentration 

2533 

2534 def get_incoming_nectar_pesticide_concentration(self, day_num): 

2535 """ 

2536 Calculate incoming nectar pesticide concentration accounting for decay. 

2537 

2538 Args: 

2539 day_num: Current day number in simulation 

2540 

2541 Returns: 

2542 float: Pesticide concentration in grams AI per gram nectar 

2543 """ 

2544 incoming_concentration = 0.0 

2545 cur_date = self.get_day_num_date(day_num) 

2546 

2547 # Check if using nutrient contamination table 

2548 if self.nutrient_ct.is_enabled(): 

2549 nectar_conc, pollen_conc = self.nutrient_ct.get_contaminant_conc(cur_date) 

2550 incoming_concentration = nectar_conc 

2551 else: 

2552 # Normal foliar spray process 

2553 if ( 

2554 self.m_epadata.m_FoliarEnabled 

2555 and cur_date >= self.m_epadata.m_FoliarAppDate 

2556 and cur_date >= self.m_epadata.m_FoliarForageBegin 

2557 and cur_date < self.m_epadata.m_FoliarForageEnd 

2558 ): 

2559 # Base concentration from foliar spray 

2560 incoming_concentration = 110.0 * self.m_epadata.m_E_AppRate / 1000000.0 

2561 

2562 # Apply decay due to active ingredient half-life 

2563 days_since_application = ( 

2564 cur_date - self.m_epadata.m_FoliarAppDate 

2565 ).days 

2566 if self.m_epadata.m_AI_HalfLife > 0: 

2567 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife 

2568 incoming_concentration *= math.exp(-k * days_since_application) 

2569 

2570 self.add_event_notification( 

2571 cur_date.strftime("%m/%d/%Y"), 

2572 "Incoming Foliar Spray Nectar Pesticide", 

2573 ) 

2574 

2575 # Seed treatment exposure 

2576 if ( 

2577 cur_date >= self.m_epadata.m_SeedForageBegin 

2578 and cur_date < self.m_epadata.m_SeedForageEnd 

2579 and self.m_epadata.m_SeedEnabled 

2580 ): 

2581 incoming_concentration += ( 

2582 self.m_epadata.m_E_SeedAppRate * 45e-9 

2583 ) # 45 ng ai/g per (mg ai/seed) 

2584 self.add_event_notification( 

2585 cur_date.strftime("%m/%d/%Y"), "Incoming Seed Nectar Pesticide" 

2586 ) 

2587 

2588 # Soil contamination exposure 

2589 if ( 

2590 cur_date >= self.m_epadata.m_SoilForageBegin 

2591 and cur_date < self.m_epadata.m_SoilForageEnd 

2592 and self.m_epadata.m_SoilEnabled 

2593 ): 

2594 if self.m_epadata.m_AI_KOW > 0 or self.m_epadata.m_E_SoilTheta != 0: 

2595 log_kow = math.log10(self.m_epadata.m_AI_KOW) 

2596 tscf = -0.0648 * (log_kow * log_kow) + 0.241 * log_kow + 0.5822 

2597 soil_conc = ( 

2598 tscf 

2599 * (pow(10, (0.95 * log_kow - 2.05)) + 0.82) 

2600 * self.m_epadata.m_E_SoilConcentration 

2601 * ( 

2602 self.m_epadata.m_E_SoilP 

2603 / ( 

2604 self.m_epadata.m_E_SoilTheta 

2605 + self.m_epadata.m_E_SoilP 

2606 * self.m_epadata.m_AI_KOC 

2607 * self.m_epadata.m_E_SoilFoc 

2608 ) 

2609 ) 

2610 ) 

2611 incoming_concentration += soil_conc / 1000000.0 

2612 self.add_event_notification( 

2613 cur_date.strftime("%m/%d/%Y"), "Incoming Soil Nectar Pesticide" 

2614 ) 

2615 

2616 return incoming_concentration 

2617 

2618 def is_pollen_feeding_day(self, event): 

2619 """ 

2620 Check if supplemental pollen feeding should occur. 

2621 

2622 Args: 

2623 event: Current event with date information 

2624 

2625 Returns: 

2626 bool: True if pollen feeding should occur 

2627 """ 

2628 feeding_day = False 

2629 

2630 if self.m_SuppPollenEnabled and self.get_colony_size() > 100: 

2631 if self.m_SuppPollenAnnual: 

2632 # Annual feeding - check within year 

2633 test_begin = event.get_time().replace( 

2634 month=self.m_SuppPollen.m_BeginDate.month, 

2635 day=self.m_SuppPollen.m_BeginDate.day, 

2636 ) 

2637 test_end = event.get_time().replace( 

2638 month=self.m_SuppPollen.m_EndDate.month, 

2639 day=self.m_SuppPollen.m_EndDate.day, 

2640 ) 

2641 

2642 feeding_day = ( 

2643 self.m_SuppPollen.m_CurrentAmount > 0.0 

2644 and test_begin < event.get_time() 

2645 and test_end >= event.get_time() 

2646 ) 

2647 else: 

2648 # Specific date range 

2649 feeding_day = ( 

2650 self.m_SuppPollen.m_CurrentAmount > 0.0 

2651 and self.m_SuppPollen.m_BeginDate < event.get_time() 

2652 and self.m_SuppPollen.m_EndDate >= event.get_time() 

2653 ) 

2654 

2655 return feeding_day 

2656 

2657 def is_nectar_feeding_day(self, event): 

2658 """ 

2659 Check if supplemental nectar feeding should occur. 

2660 

2661 Args: 

2662 event: Current event with date information 

2663 

2664 Returns: 

2665 bool: True if nectar feeding should occur 

2666 """ 

2667 feeding_day = False 

2668 

2669 if self.m_SuppNectarEnabled and self.get_colony_size() > 100: 

2670 if self.m_SuppNectarAnnual: 

2671 # Annual feeding - check within year 

2672 test_begin = event.get_time().replace( 

2673 month=self.m_SuppNectar.m_BeginDate.month, 

2674 day=self.m_SuppNectar.m_BeginDate.day, 

2675 ) 

2676 test_end = event.get_time().replace( 

2677 month=self.m_SuppNectar.m_EndDate.month, 

2678 day=self.m_SuppNectar.m_EndDate.day, 

2679 ) 

2680 feeding_day = ( 

2681 self.m_SuppNectar.m_CurrentAmount > 0.0 

2682 and test_begin < event.get_time() 

2683 and test_end >= event.get_time() 

2684 ) 

2685 else: 

2686 # Specific date range 

2687 feeding_day = ( 

2688 self.m_SuppNectar.m_CurrentAmount > 0.0 

2689 and self.m_SuppNectar.m_BeginDate < event.get_time() 

2690 and self.m_SuppNectar.m_EndDate >= event.get_time() 

2691 ) 

2692 

2693 return feeding_day 

2694 

2695 def add_pollen_to_resources(self, resource): 

2696 """ 

2697 Add pollen to colony resources with storage limits. 

2698 

2699 Args: 

2700 resource: ResourceItem object with resource_quantity and pesticide_quantity 

2701 """ 

2702 if self.m_ColonyPolMaxAmount <= 0: 

2703 self.m_ColonyPolMaxAmount = 5000 # Default max 

2704 

2705 prop_full = self.resources.get_pollen_quantity() / self.m_ColonyPolMaxAmount 

2706 reduction = 1 - prop_full 

2707 

2708 if prop_full > 0.9: 

2709 resource.resource_quantity *= reduction 

2710 resource.pesticide_quantity *= reduction 

2711 

2712 self.resources.add_pollen(resource) 

2713 

2714 def add_nectar_to_resources(self, resource): 

2715 """ 

2716 Add nectar to colony resources with storage limits. 

2717 

2718 Args: 

2719 resource: ResourceItem object with resource_quantity and pesticide_quantity 

2720 """ 

2721 if self.m_ColonyNecMaxAmount <= 0: 

2722 self.m_ColonyNecMaxAmount = 5000 # Default max 

2723 

2724 prop_full = self.resources.get_nectar_quantity() / self.m_ColonyNecMaxAmount 

2725 reduction = 1 - prop_full 

2726 

2727 reduction = max(reduction, 0) # Don't exceed max value 

2728 

2729 if prop_full > 0.9: 

2730 resource.resource_quantity *= reduction 

2731 resource.pesticide_quantity *= reduction 

2732 

2733 self.resources.add_nectar(resource) 

2734 

2735 def initialize_colony_resources(self): 

2736 """ 

2737 Port of CColony::InitializeColonyResources 

2738 

2739 Initialize colony resources to zero values. 

2740 TODO: This should ultimately be pre-settable at the beginning of a simulation. 

2741 For now, initialize everything to 0.0. 

2742 """ 

2743 self.resources.set_pollen_quantity(0) 

2744 self.resources.set_nectar_quantity(0) 

2745 self.resources.set_pollen_pesticide_quantity(0) 

2746 self.resources.set_nectar_pesticide_quantity(0)