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

1153 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 13:34 +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 

60from pybeepop.beepop.epadata import EPAData 

61from pybeepop.beepop.colonyresource import ColonyResource, ResourceItem 

62from pybeepop.beepop.queen import Queen 

63from pybeepop.beepop.nutrientcontaminationtable import NutrientContaminationTable 

64from pybeepop.beepop.beelist import ( 

65 ForagerListA, 

66 AdultList, 

67 BroodList, 

68 LarvaList, 

69 EggList, 

70) 

71from pybeepop.beepop.mite import Mite 

72from pybeepop.beepop.brood import Brood 

73from pybeepop.beepop.mitetreatments import MiteTreatments 

74from pybeepop.beepop.daterangevalues import DateRangeValues 

75from pybeepop.beepop.egg import Egg 

76from pybeepop.beepop.coldstoragesimulator import ColdStorageSimulator 

77import math 

78from pybeepop.beepop.globaloptions import GlobalOptions 

79from pybeepop.beepop.spores import Spores 

80from types import SimpleNamespace 

81from datetime import datetime, timedelta 

82 

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_VTStart = 0 

298 self.m_SPStart = 0 

299 self.m_VTDuration = 0 

300 self.m_VTMortality = 0 

301 self.m_SPEnable = False 

302 self.m_SPTreatmentActive = False 

303 self.m_InitMitePctResistant = 0.0 

304 self.m_CurrentForagerLifespan = 0 

305 self.m_RQQueenStrengthArray = [] 

306 self.m_NutrientContEnabled = False 

307 self.m_SuppPollenEnabled = False 

308 self.m_SuppNectarEnabled = False 

309 self.m_SuppPollenAnnual = False 

310 self.m_SuppNectarAnnual = False 

311 

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

313 self.m_SuppPollen = SimpleNamespace() 

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

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

316 self.m_SuppPollen.m_CurrentAmount = 0.0 

317 self.m_SuppPollen.m_StartingAmount = 0.0 

318 

319 self.m_SuppNectar = SimpleNamespace() 

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

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

322 self.m_SuppNectar.m_CurrentAmount = 0.0 

323 self.m_SuppNectar.m_StartingAmount = 0.0 

324 

325 self.m_InOutEvent = InOutEvent() 

326 

327 # Property aliases for consistent naming in consume_food methods 

328 @property 

329 def epa_data(self): 

330 return self.m_epadata 

331 

332 @property 

333 def nutrient_ct(self): 

334 return self.m_nutrient_ct 

335 

336 # Methods from colony.h not yet implemented 

337 def get_adult_aging_delay(self): 

338 return self.m_adult_age_delay_limit 

339 

340 def set_adult_aging_delay(self, delay): 

341 self.m_adult_age_delay_limit = delay 

342 

343 def get_adult_aging_delay_egg_threshold(self): 

344 return self.m_adult_aging_delay_egg_threshold 

345 

346 def set_adult_aging_delay_egg_threshold(self, threshold): 

347 self.m_adult_aging_delay_egg_threshold = threshold 

348 

349 def is_adult_aging_delay_armed(self): 

350 return self.adult_aging_delay_armed 

351 

352 def set_adult_aging_delay_armed(self, armed_state): 

353 self.adult_aging_delay_armed = armed_state 

354 

355 def set_initialized(self, val): 

356 self.has_been_initialized = val 

357 

358 def is_initialized(self): 

359 return self.has_been_initialized 

360 

361 def get_forager_lifespan(self): 

362 return self.m_init_cond.m_ForagerLifespan 

363 

364 def get_cold_storage_simulator(self): 

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

366 return ColdStorageSimulator.get() 

367 

368 def get_adult_drones(self): 

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

370 return self.dadl.get_quantity() 

371 

372 def get_adult_workers(self): 

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

374 return self.wadl.get_quantity() 

375 

376 def get_foragers(self): 

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

378 return self.foragers.get_quantity() 

379 

380 def get_active_foragers(self): 

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

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

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

384 return self.foragers.get_active_quantity() 

385 

386 def get_drone_brood(self): 

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

388 return self.capdrn.get_quantity() 

389 

390 def get_worker_brood(self): 

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

392 return self.capwkr.get_quantity() 

393 

394 def get_drone_larvae(self): 

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

396 return self.dlarv.get_quantity() 

397 

398 def get_worker_larvae(self): 

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

400 return self.wlarv.get_quantity() 

401 

402 def get_drone_eggs(self): 

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

404 return self.deggs.get_quantity() 

405 

406 def get_worker_eggs(self): 

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

408 return self.weggs.get_quantity() 

409 

410 def get_total_eggs_laid_today(self): 

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

412 return self.queen.get_teggs() 

413 

414 def get_free_mites(self): 

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

416 return self.run_mite.get_total() 

417 

418 def get_drone_brood_mites(self): 

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

420 return self.capdrn.get_mite_count() 

421 

422 def get_worker_brood_mites(self): 

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

424 return self.capwkr.get_mite_count() 

425 

426 def get_mites_per_drone_brood(self): 

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

428 return self.capdrn.get_mites_per_cell() 

429 

430 def get_mites_per_worker_brood(self): 

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

432 return self.capwkr.get_mites_per_cell() 

433 

434 def get_prop_mites_dying(self): 

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

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

437 proportion_dying = self.get_mites_dying_this_period() / ( 

438 self.get_mites_dying_this_period() + self.get_total_mite_count() 

439 ) 

440 return proportion_dying 

441 else: 

442 return 0.0 

443 

444 def get_col_pollen(self): 

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

446 return self.resources.get_pollen_quantity() 

447 

448 def get_pollen_pest_conc(self): 

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

450 return self.resources.get_pollen_pesticide_concentration() * 1000000.0 

451 

452 def get_col_nectar(self): 

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

454 return self.resources.get_nectar_quantity() 

455 

456 def get_nectar_pest_conc(self): 

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

458 return self.resources.get_nectar_pesticide_concentration() * 1000000.0 

459 

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

461 def get_dead_drone_larvae_pesticide(self): 

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

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

464 

465 def get_dead_worker_larvae_pesticide(self): 

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

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

468 

469 def get_dead_drone_adults_pesticide(self): 

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

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

472 

473 def get_dead_worker_adults_pesticide(self): 

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

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

476 

477 def get_dead_foragers_pesticide(self): 

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

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

480 

481 def get_queen_strength(self): 

482 """Get the queen strength.""" 

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

484 

485 def get_dd_lower(self): 

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

487 return self.get_dd_today_lower() 

488 

489 def get_l_lower(self): 

490 """Get the lower L value.""" 

491 return self.get_l_today_lower() 

492 

493 def get_n_lower(self): 

494 """Get the lower N value.""" 

495 return self.get_n_today_lower() 

496 

497 def set_mite_pct_resistance(self, pct): 

498 self.m_InitMitePctResistant = pct 

499 

500 def set_vt_enable(self, value): 

501 self.m_vt_enable = value 

502 

503 def initialize_colony(self): 

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

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

506 self.deggs.set_length(EGGLIFE) 

507 self.deggs.set_prop_transition(1.0) 

508 self.weggs.set_length(EGGLIFE) 

509 self.weggs.set_prop_transition(1.0) 

510 self.dlarv.set_length(DLARVLIFE) 

511 self.dlarv.set_prop_transition(1.0) 

512 self.wlarv.set_length(WLARVLIFE) 

513 self.wlarv.set_prop_transition(1.0) 

514 self.capdrn.set_length(DBROODLIFE) 

515 self.capdrn.set_prop_transition(1.0) 

516 self.capwkr.set_length(WBROODLIFE) 

517 self.capwkr.set_prop_transition(1.0) 

518 self.dadl.set_length(DADLLIFE) 

519 self.dadl.set_prop_transition(1.0) 

520 self.wadl.set_length(WADLLIFE) 

521 self.wadl.set_prop_transition(1.0) 

522 self.wadl.set_colony(self) 

523 self.foragers.set_length(self.m_CurrentForagerLifespan) 

524 self.foragers.set_colony(self) 

525 

526 self.initialize_bees() 

527 self.initialize_mites() 

528 # Set pesticide Dose rate to 0 

529 if self.m_epadata: 

530 for attr in [ 

531 "m_D_L4", 

532 "m_D_L5", 

533 "m_D_LD", 

534 "m_D_A13", 

535 "m_D_A410", 

536 "m_D_A1120", 

537 "m_D_AD", 

538 "m_D_C_Foragers", 

539 "m_D_D_Foragers", 

540 "m_D_L4_Max", 

541 "m_D_L5_Max", 

542 "m_D_LD_Max", 

543 "m_D_A13_Max", 

544 "m_D_A410_Max", 

545 "m_D_A1120_Max", 

546 "m_D_AD_Max", 

547 "m_D_C_Foragers_Max", 

548 "m_D_D_Foragers_Max", 

549 ]: 

550 setattr(self.m_epadata, attr, 0) 

551 # Set resources 

552 if self.resources: 

553 self.resources.initialize( 

554 self.m_ColonyPolInitAmount, self.m_ColonyNecInitAmount 

555 ) 

556 if self.m_SuppPollen: 

557 self.m_SuppPollen.m_CurrentAmount = self.m_SuppPollen.m_StartingAmount 

558 if self.m_SuppNectar: 

559 self.m_SuppNectar.m_CurrentAmount = self.m_SuppNectar.m_StartingAmount 

560 # Set pesticide mortality trackers to zero 

561 self.m_dead_worker_larvae_pesticide = 0 

562 self.m_dead_drone_larvae_pesticide = 0 

563 self.m_dead_worker_adults_pesticide = 0 

564 self.m_dead_drone_adults_pesticide = 0 

565 self.m_dead_foragers_pesticide = 0 

566 self.m_colony_event_list.clear() 

567 # Nutrient contamination table logic (if enabled) 

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

569 # rather than loading from file during initialization 

570 if ( 

571 self.m_nutrient_ct 

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

573 ): 

574 # Contamination table is already loaded via set_contamination_table 

575 pass 

576 # Set initial state of AdultAgingDelayArming 

577 if self.m_p_session: 

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

579 # Ported logic for arming AdultAgingDelay 

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

581 if 1 <= monthnum < 3: 

582 self.adult_aging_delay_armed = True 

583 else: 

584 self.adult_aging_delay_armed = False 

585 self.has_been_initialized = True 

586 

587 def add_event_notification(self, date_stg, msg): 

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

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

590 self.m_colony_event_list.append(event_string) 

591 

592 def get_day_num_date(self, day_num): 

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

594 if not self.m_p_session: 

595 return None 

596 sim_start = self.m_p_session.get_sim_start() 

597 # Use timedelta to add days to datetime object 

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

599 

600 def kill_colony(self): 

601 # Set queen strength to 1 (minimum) 

602 if self.queen: 

603 self.queen.set_strength(1) 

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

605 for attr in [ 

606 "deggs", 

607 "weggs", 

608 "dlarv", 

609 "wlarv", 

610 "capdrn", 

611 "capwkr", 

612 "dadl", 

613 "wadl", 

614 "foragers", 

615 ]: 

616 bee_list = getattr(self, attr, None) 

617 if bee_list: 

618 bee_list.kill_all() 

619 if hasattr(self, "foragers"): 

620 self.foragers.clear_pending_foragers() 

621 

622 def create(self): 

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

624 

625 # Set lengths and prop transitions for all bee lists 

626 self.deggs.set_length(EGGLIFE) 

627 self.deggs.set_prop_transition(1.0) 

628 self.weggs.set_length(EGGLIFE) 

629 self.weggs.set_prop_transition(1.0) 

630 self.dlarv.set_length(DLARVLIFE) 

631 self.dlarv.set_prop_transition(1.0) 

632 self.wlarv.set_length(WLARVLIFE) 

633 self.wlarv.set_prop_transition(1.0) 

634 self.capdrn.set_length(DBROODLIFE) 

635 self.capdrn.set_prop_transition(1.0) 

636 self.capwkr.set_length(WBROODLIFE) 

637 self.capwkr.set_prop_transition(1.0) 

638 self.dadl.set_length(DADLLIFE) 

639 self.dadl.set_prop_transition(1.0) 

640 self.wadl.set_length(WADLLIFE) 

641 self.wadl.set_prop_transition(1.0) 

642 

643 # Set colony reference for lists that need it 

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

645 self.wadl.set_colony(self) 

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

647 self.foragers.set_length( 

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

649 if hasattr(self, "m_init_cond") 

650 else 12 

651 ) 

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

653 self.foragers.set_colony(self) 

654 

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

656 self.set_default_init_conditions() 

657 

658 def clear(self): 

659 # Clear all lists and simulation state 

660 for attr in [ 

661 "deggs", 

662 "weggs", 

663 "dlarv", 

664 "wlarv", 

665 "capdrn", 

666 "capwkr", 

667 "dadl", 

668 "wadl", 

669 "foragers", 

670 ]: 

671 bee_list = getattr(self, attr, None) 

672 if bee_list: 

673 bee_list.kill_all() 

674 self.m_colony_event_list.clear() 

675 

676 def is_adult_aging_delay_active(self): 

677 """ 

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

679 Logic matches C++ implementation exactly. 

680 """ 

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

682 egg_quant_threshold = self.get_adult_aging_delay_egg_threshold() 

683 

684 if self.is_adult_aging_delay_armed(): 

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

686 self.set_adult_aging_delay_armed( 

687 False 

688 ) # Disarm when eggs exceed threshold 

689 self.m_days_since_egg_laying_began = 0 # Reset counter 

690 

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

692 active = ( 

693 self.m_days_since_egg_laying_began < self.m_adult_age_delay_limit 

694 ) and not self.is_adult_aging_delay_armed() 

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

696 

697 return active 

698 

699 def set_default_init_conditions(self): 

700 """ 

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

702 Resets bee lists and initial state variables. 

703 """ 

704 # Reset bee lists 

705 for bee_list in [ 

706 self.deggs, 

707 self.weggs, 

708 self.dlarv, 

709 self.wlarv, 

710 self.capdrn, 

711 self.capwkr, 

712 self.dadl, 

713 self.wadl, 

714 self.foragers, 

715 ]: 

716 if hasattr(bee_list, "clear"): 

717 bee_list.clear() 

718 # Reset initial conditions 

719 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit 

720 self.adult_aging_delay_armed = False 

721 self.m_dead_worker_larvae_pesticide = 0 

722 self.m_dead_drone_larvae_pesticide = 0 

723 self.m_dead_worker_adults_pesticide = 0 

724 self.m_dead_drone_adults_pesticide = 0 

725 self.m_dead_foragers_pesticide = 0 

726 self.m_colony_event_list.clear() 

727 # Optionally reset other state variables as needed 

728 

729 def initialize_bees(self): 

730 """ 

731 Ported from CColony::InitializeBees. 

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

733 """ 

734 # Set current forager lifespan and adult aging delay 

735 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan 

736 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit 

737 

738 # Initialize Queen 

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

740 

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

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

743 self.foragers.set_length(self.m_CurrentForagerLifespan) 

744 self.foragers.set_colony(self) 

745 

746 # Helper to distribute bees into boxcars 

747 def distribute_bees(init_count, bee_list, bee_class): 

748 boxcar_len = bee_list.get_length() 

749 if boxcar_len == 0: 

750 return 

751 avg = init_count // boxcar_len 

752 remainder = init_count - avg * boxcar_len 

753 for i in range(boxcar_len): 

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

755 bee = bee_class(count) 

756 bee_list.add_head(bee) 

757 

758 # Eggs 

759 distribute_bees( 

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

761 ) 

762 distribute_bees( 

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

764 ) 

765 

766 # Larvae 

767 distribute_bees( 

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

769 ) 

770 distribute_bees( 

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

772 ) 

773 

774 # Capped Brood 

775 distribute_bees( 

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

777 ) 

778 distribute_bees( 

779 self.m_init_cond.m_workerBroodField, 

780 self.capwkr, 

781 self.capwkr.get_bee_class(), 

782 ) 

783 

784 # Drone Adults 

785 boxcar_len = self.dadl.get_length() 

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

787 remainder = ( 

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

789 ) 

790 for i in range(boxcar_len): 

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

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

793 drone.set_lifespan(DADLLIFE) 

794 self.dadl.add_head(drone) 

795 

796 # Worker Adults and Foragers 

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

798 avg = ( 

799 self.m_init_cond.m_workerAdultsField // total_boxcars 

800 if total_boxcars 

801 else 0 

802 ) 

803 remainder = ( 

804 self.m_init_cond.m_workerAdultsField - avg * total_boxcars 

805 if total_boxcars 

806 else 0 

807 ) 

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

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

810 worker.set_lifespan(WADLLIFE) 

811 self.wadl.add_head(worker) 

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

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

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

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

816 self.foragers.add_head(forager) 

817 

818 # Set queen day one and egg laying delay 

819 self.queen.set_day_one(1) 

820 self.queen.set_egg_laying_delay(0) 

821 

822 def get_colony_size(self): 

823 """ 

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

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

826 """ 

827 return int( 

828 self.dadl.get_quantity() 

829 + self.wadl.get_quantity() 

830 + self.foragers.get_quantity() 

831 ) 

832 

833 def update_bees(self, event, day_num): 

834 """ 

835 Ported from CColony::UpdateBees. 

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

837 """ 

838 # Calculate larvae per bee 

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

840 total_adults = ( 

841 self.wadl.get_quantity() 

842 + self.dadl.get_quantity() 

843 + self.foragers.get_quantity() 

844 ) 

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

846 if total_adults == 0: 

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

848 else: 

849 larv_per_bee = float(total_larvae) / total_adults 

850 

851 # Arm Adult Aging Delay on Jan 1 

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

853 self.set_adult_aging_delay_armed(True) 

854 

855 # Apply date range values 

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

857 the_date = event.parse_date(date_stg) 

858 if the_date: 

859 # Eggs Transition Rate 

860 prop_transition = self.m_init_cond.m_EggTransitionDRV.get_active_value( 

861 the_date 

862 ) 

863 if ( 

864 prop_transition is not None 

865 and self.m_init_cond.m_EggTransitionDRV.is_enabled() 

866 ): 

867 self.deggs.set_prop_transition(prop_transition / 100) 

868 self.weggs.set_prop_transition(prop_transition / 100) 

869 else: 

870 self.deggs.set_prop_transition(1.0) 

871 self.weggs.set_prop_transition(1.0) 

872 # Larvae Transition Rate 

873 prop_transition = self.m_init_cond.m_LarvaeTransitionDRV.get_active_value( 

874 the_date 

875 ) 

876 if ( 

877 prop_transition is not None 

878 and self.m_init_cond.m_LarvaeTransitionDRV.is_enabled() 

879 ): 

880 self.dlarv.set_prop_transition(prop_transition / 100) 

881 self.wlarv.set_prop_transition(prop_transition / 100) 

882 else: 

883 self.dlarv.set_prop_transition(1.0) 

884 self.wlarv.set_prop_transition(1.0) 

885 # Brood Transition Rate 

886 prop_transition = self.m_init_cond.m_BroodTransitionDRV.get_active_value( 

887 the_date 

888 ) 

889 if ( 

890 prop_transition is not None 

891 and self.m_init_cond.m_BroodTransitionDRV.is_enabled() 

892 ): 

893 self.capdrn.set_prop_transition(prop_transition / 100) 

894 self.capwkr.set_prop_transition(prop_transition / 100) 

895 else: 

896 self.capdrn.set_prop_transition(1.0) 

897 self.capwkr.set_prop_transition(1.0) 

898 # Adults Transition Rate 

899 prop_transition = self.m_init_cond.m_AdultTransitionDRV.get_active_value( 

900 the_date 

901 ) 

902 if ( 

903 prop_transition is not None 

904 and self.m_init_cond.m_AdultTransitionDRV.is_enabled() 

905 ): 

906 self.dadl.set_prop_transition(prop_transition / 100) 

907 self.wadl.set_prop_transition(prop_transition / 100) 

908 else: 

909 self.dadl.set_prop_transition(1.0) 

910 self.wadl.set_prop_transition(1.0) 

911 # Adults Lifespan Change 

912 adult_age_limit = self.m_init_cond.m_AdultLifespanDRV.get_active_value( 

913 the_date 

914 ) 

915 if ( 

916 adult_age_limit is not None 

917 and self.m_init_cond.m_AdultLifespanDRV.is_enabled() 

918 ): 

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

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

921 else: 

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

923 self.wadl.update_length(WADLLIFE) 

924 # Foragers Lifespan Change 

925 forager_lifespan = self.m_init_cond.m_ForagerLifespanDRV.get_active_value( 

926 the_date 

927 ) 

928 if ( 

929 forager_lifespan is not None 

930 and self.m_init_cond.m_ForagerLifespanDRV.is_enabled() 

931 ): 

932 self.m_CurrentForagerLifespan = int(forager_lifespan) 

933 else: 

934 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan 

935 self.foragers.set_length(self.m_CurrentForagerLifespan) 

936 else: 

937 self.deggs.set_prop_transition(1.0) 

938 self.weggs.set_prop_transition(1.0) 

939 self.dlarv.set_prop_transition(1.0) 

940 self.wlarv.set_prop_transition(1.0) 

941 self.capdrn.set_prop_transition(1.0) 

942 self.capwkr.set_prop_transition(1.0) 

943 self.dadl.set_prop_transition(1.0) 

944 self.wadl.set_prop_transition(1.0) 

945 

946 # Reset output data struct for algorithm intermediate results 

947 self.m_InOutEvent.reset() 

948 

949 # Queen lays eggs 

950 self.queen.lay_eggs( 

951 day_num, 

952 event.get_temp(), 

953 event.get_daylight_hours(), 

954 self.foragers.get_quantity(), 

955 larv_per_bee, 

956 ) 

957 

958 # Simulate cold storage 

959 cold_storage = self.get_cold_storage_simulator() 

960 today = event.get_date_stg() 

961 if cold_storage.is_enabled(): 

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

963 cold_storage.update(event, self) 

964 if cold_storage.is_active_now(): 

965 cs_state_stg += " and ACTIVE" 

966 if cold_storage.is_starting_now(): 

967 cs_state_stg += " and STARTING" 

968 if cold_storage.is_ending_now(): 

969 cs_state_stg += "and ENDING" 

970 if cold_storage.is_on(): 

971 cs_state_stg += " and ON" 

972 if self.m_p_session.is_info_reporting_enabled(): 

973 self.m_p_session.add_to_info_list(cs_state_stg) 

974 

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

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

977 

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

979 if cold_storage.is_starting_now(): 

980 if self.m_p_session.is_info_reporting_enabled(): 

981 self.m_p_session.add_to_info_list( 

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

983 ) 

984 l_DEggs.set_number(0) 

985 l_WEggs.set_number(0) 

986 self.deggs.kill_all() 

987 self.weggs.kill_all() 

988 

989 # Update stats for new eggs 

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

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

992 

993 self.deggs.update(l_DEggs) 

994 self.weggs.update(l_WEggs) 

995 

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

997 if cold_storage.is_starting_now(): 

998 self.weggs.get_caboose().reset() 

999 self.deggs.get_caboose().reset() 

1000 

1001 # Update stats for new larvae 

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

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

1004 

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

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

1007 

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

1009 if cold_storage.is_starting_now(): 

1010 self.wlarv.get_caboose().reset() 

1011 self.dlarv.get_caboose().reset() 

1012 self.wlarv.kill_all() 

1013 self.dlarv.kill_all() 

1014 

1015 # Update stats for new brood 

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

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

1018 

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

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

1021 

1022 # Update stats for new adults 

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

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

1025 

1026 number_of_non_adults = ( 

1027 self.wlarv.get_quantity() 

1028 + self.dlarv.get_quantity() 

1029 + self.capdrn.get_quantity() 

1030 + self.capwkr.get_quantity() 

1031 ) 

1032 

1033 # ForageInc validity 

1034 global_options = GlobalOptions.get() 

1035 forage_inc_is_valid = ( 

1036 global_options.should_forage_day_election_based_on_temperatures 

1037 or event.get_forage_inc() > 0.0 

1038 ) 

1039 

1040 if (number_of_non_adults > 0) or ( 

1041 event.is_forage_day() and forage_inc_is_valid 

1042 ): 

1043 # Foragers killed due to pesticide 

1044 foragers_to_be_killed = self.quantity_pesticide_to_kill( 

1045 self.foragers, 

1046 self.m_epadata.m_D_C_Foragers, 

1047 0, 

1048 self.m_epadata.m_AI_AdultLD50_Contact, 

1049 self.m_epadata.m_AI_AdultSlope_Contact, 

1050 ) 

1051 foragers_to_be_killed += self.quantity_pesticide_to_kill( 

1052 self.foragers, 

1053 self.m_epadata.m_D_D_Foragers, 

1054 0, 

1055 self.m_epadata.m_AI_AdultLD50, 

1056 self.m_epadata.m_AI_AdultSlope, 

1057 ) 

1058 min_age_to_forager = 14 

1059 self.wadl.move_to_end(foragers_to_be_killed, min_age_to_forager) 

1060 if foragers_to_be_killed > 0: 

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

1062 self.add_event_notification( 

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

1064 ) 

1065 self.m_InOutEvent.m_ForagersKilledByPesticide = foragers_to_be_killed 

1066 

1067 # Aging adults 

1068 aging_adults = not cold_storage.is_active_now() and ( 

1069 not global_options.should_adults_age_based_laid_eggs 

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

1071 ) 

1072 if self.is_adult_aging_delay_active(): 

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

1074 aging_adults = ( 

1075 aging_adults 

1076 and not self.is_adult_aging_delay_active() 

1077 and not self.is_adult_aging_delay_armed() 

1078 ) 

1079 if aging_adults: 

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

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

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

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

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

1085 self.m_InOutEvent.m_WAdultToForagers = ( 

1086 self.wadl.get_caboose().get_number() 

1087 ) 

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

1089 else: 

1090 if ( 

1091 number_of_non_adults > 0 

1092 and global_options.should_adults_age_based_laid_eggs 

1093 ): 

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

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

1096 self.m_InOutEvent.m_WAdultToForagers = 0 

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

1098 reset_adult.reset() 

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

1100 self.m_InOutEvent.m_DeadForagers = ( 

1101 self.foragers.get_caboose().get_number() 

1102 if self.foragers.get_caboose().get_number() > 0 

1103 else 0 

1104 ) 

1105 

1106 # Apply pesticide mortality impacts 

1107 self.consume_food(event, day_num) 

1108 self.determine_foliar_dose(day_num) 

1109 self.apply_pesticide_mortality() 

1110 

1111 def get_eggs_today(self): 

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

1113 return self.queen.get_teggs() 

1114 

1115 def get_dd_today(self): 

1116 # Returns DD value for today from Queen 

1117 return self.queen.get_DD() 

1118 

1119 def get_daylight_hrs_today(self, event=None): 

1120 # Returns daylight hours for today from Queen 

1121 return self.queen.get_L() 

1122 

1123 def get_l_today(self): 

1124 # Returns L value for today from Queen 

1125 return self.queen.get_L() 

1126 

1127 def get_n_today(self): 

1128 # Returns N value for today from Queen 

1129 return self.queen.get_N() 

1130 

1131 def get_p_today(self): 

1132 # Returns P value for today from Queen 

1133 return self.queen.get_P() 

1134 

1135 def get_dd_today_lower(self): 

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

1137 return self.queen.get_dd() 

1138 

1139 def get_l_today_lower(self): 

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

1141 return self.queen.get_l() 

1142 

1143 def get_n_today_lower(self): 

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

1145 return self.queen.get_n() 

1146 

1147 def add_mites(self, new_mites): 

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

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

1150 self.run_mite += new_mites 

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

1152 self.prop_rm_virgins = 1.0 

1153 else: 

1154 total_run = self.run_mite.get_total() 

1155 # avoid division by zero though handled above 

1156 self.prop_rm_virgins = ( 

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

1158 ) 

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

1160 if self.prop_rm_virgins > 1.0: 

1161 self.prop_rm_virgins = 1.0 

1162 if self.prop_rm_virgins < 0.0: 

1163 self.prop_rm_virgins = 0.0 

1164 

1165 def initialize_mites(self): 

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

1167 w_count = int( 

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

1169 / 100.0 

1170 ) 

1171 d_count = int( 

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

1173 / 100.0 

1174 ) 

1175 w_mites = Mite(0, w_count) 

1176 d_mites = Mite(0, d_count) 

1177 # Distribute mites into capped brood 

1178 self.capwkr.distribute_mites(w_mites) 

1179 self.capdrn.distribute_mites(d_mites) 

1180 

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

1182 run_w_count = int( 

1183 ( 

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

1185 + self.foragers.get_quantity() 

1186 * self.m_init_cond.m_workerAdultInfestField 

1187 ) 

1188 / 100.0 

1189 ) 

1190 run_d_count = int( 

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

1192 / 100.0 

1193 ) 

1194 run_mite_w = Mite(0, run_w_count) 

1195 run_mite_d = Mite(0, run_d_count) 

1196 

1197 self.run_mite = run_mite_d + run_mite_w 

1198 

1199 self.prop_rm_virgins = 1.0 

1200 

1201 self.m_mites_dying_today = 0.0 

1202 self.m_mites_dying_this_period = 0.0 

1203 

1204 def update_mites(self, event, day_num): 

1205 # Port of CColony::UpdateMites 

1206 # 

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

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

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

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

1211 # first boxcar in the appropriate Adult list. 

1212 # 

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

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

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

1216 

1217 # Reset today's mite death counter 

1218 self.m_mites_dying_today = 0.0 

1219 

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

1221 WkrBrood = ( 

1222 self.capwkr.bees[0] 

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

1224 else None 

1225 ) 

1226 DrnBrood = ( 

1227 self.capdrn.bees[0] 

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

1229 else None 

1230 ) 

1231 

1232 if WkrBrood is None: 

1233 # Nothing to do without brood 

1234 return 

1235 if DrnBrood is None: 

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

1237 class _EmptyBrood: 

1238 def get_number(self): 

1239 return 0 

1240 

1241 DrnBrood = _EmptyBrood() 

1242 

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

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

1245 if B > 0.0: 

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

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

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

1249 if I < 0.0: 

1250 I = 0.0 

1251 else: 

1252 I = 0.0 

1253 

1254 # WMites = RunMite * (I * PROPINFSTW) 

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

1256 

1257 # Likelihood of finding drone cell 

1258 if WkrBrood.get_number() > 0: 

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

1260 if Likelihood > 1.0: 

1261 Likelihood = 1.0 

1262 else: 

1263 Likelihood = 1.0 

1264 

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

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

1267 

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

1269 if WkrBrood.get_number() == 0: 

1270 DMites += WMites 

1271 WMites.set_resistant(0) 

1272 WMites.set_non_resistant(0) 

1273 

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

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

1276 # Preserve pct resistant of DMites 

1277 try: 

1278 OverflowLikelihood.set_pct_resistant(DMites.get_pct_resistant()) 

1279 except Exception: 

1280 pass 

1281 

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

1283 OverflowMax = Mite(0, 0) 

1284 max_allowed = MAXMITES_PER_DRONE_CELL * DrnBrood.get_number() 

1285 if DMites.get_total() > max_allowed: 

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

1287 overflow_count = DMites.get_total() - max_allowed 

1288 OverflowMax = Mite(0, overflow_count) 

1289 try: 

1290 OverflowMax.set_pct_resistant(DMites.get_pct_resistant()) 

1291 except Exception: 

1292 pass 

1293 # DMites -= OverflowMax 

1294 DMites = DMites - OverflowMax 

1295 

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

1297 WMites = WMites + OverflowMax + OverflowLikelihood 

1298 

1299 # Limit worker mites per cell 

1300 max_w = MAXMITES_PER_WORKER_CELL * WkrBrood.get_number() 

1301 if WMites.get_total() > max_w: 

1302 pr = WMites.get_pct_resistant() 

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

1304 WMites = Mite(0, max_w) 

1305 try: 

1306 WMites.set_pct_resistant(pr) 

1307 except Exception: 

1308 pass 

1309 

1310 # Remove the mites used to infest from running mites 

1311 self.run_mite = self.run_mite - WMites - DMites 

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

1313 self.run_mite = Mite(0, 0) 

1314 

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

1316 WkrBrood.set_mites(WMites) 

1317 if hasattr(WkrBrood, "set_prop_virgins"): 

1318 WkrBrood.set_prop_virgins(self.prop_rm_virgins) 

1319 DrnBrood.set_mites(DMites) 

1320 if hasattr(DrnBrood, "set_prop_virgins"): 

1321 DrnBrood.set_prop_virgins(self.prop_rm_virgins) 

1322 

1323 # Emerging mites from first adult boxcar 

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

1325 WkrHead = ( 

1326 self.wadl.bees[0] 

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

1328 else None 

1329 ) 

1330 DrnHead = ( 

1331 self.dadl.bees[0] 

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

1333 else None 

1334 ) 

1335 

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

1337 WkrEmerge = Brood() 

1338 DrnEmerge = Brood() 

1339 

1340 if WkrHead: 

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

1342 WkrEmerge.set_prop_virgins( 

1343 WkrHead.get_prop_virgins() 

1344 if hasattr(WkrHead, "get_prop_virgins") 

1345 else 0.0 

1346 ) 

1347 if WkrHead.have_mites_been_counted(): 

1348 WkrEmerge.mites = Mite(0, 0) 

1349 else: 

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

1351 if isinstance(m, Mite): 

1352 WkrEmerge.mites = m 

1353 else: 

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

1355 WkrEmerge.mites = Mite(0, m) 

1356 WkrHead.set_mites_counted(True) 

1357 

1358 if DrnHead: 

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

1360 DrnEmerge.set_prop_virgins( 

1361 DrnHead.get_prop_virgins() 

1362 if hasattr(DrnHead, "get_prop_virgins") 

1363 else 0.0 

1364 ) 

1365 if DrnHead.have_mites_been_counted(): 

1366 DrnEmerge.mites = Mite(0, 0) 

1367 else: 

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

1369 if isinstance(m, Mite): 

1370 DrnEmerge.mites = m 

1371 else: 

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

1373 DrnEmerge.mites = Mite(0, m) 

1374 DrnHead.set_mites_counted(True) 

1375 

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

1377 MitesPerCellW = ( 

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

1379 if WkrEmerge.number > 0 

1380 else 0.0 

1381 ) 

1382 MitesPerCellD = ( 

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

1384 if DrnEmerge.number > 0 

1385 else 0.0 

1386 ) 

1387 

1388 # Survivorship 

1389 PropSurviveMiteW = self.m_init_cond.m_workerMiteSurvivorship / 100.0 

1390 PropSurviveMiteD = self.m_init_cond.m_droneMiteSurvivorshipField / 100.0 

1391 

1392 # Reproduction rates per mite per cell 

1393 if MitesPerCellW <= 1.0: 

1394 ReproMitePerCellW = self.m_init_cond.m_workerMiteOffspring 

1395 else: 

1396 ReproMitePerCellW = (1.15 * MitesPerCellW) - ( 

1397 0.233 * MitesPerCellW * MitesPerCellW 

1398 ) 

1399 if ReproMitePerCellW < 0: 

1400 ReproMitePerCellW = 0.0 

1401 

1402 if MitesPerCellD <= 2.0: 

1403 ReproMitePerCellD = self.m_init_cond.m_droneMiteOffspringField 

1404 else: 

1405 ReproMitePerCellD = ( 

1406 1.734 

1407 - (0.0755 * MitesPerCellD) 

1408 - (0.0069 * MitesPerCellD * MitesPerCellD) 

1409 ) 

1410 if ReproMitePerCellD < 0: 

1411 ReproMitePerCellD = 0.0 

1412 

1413 PROPRUNMITE2 = 0.6 

1414 

1415 SurviveMitesW = WkrEmerge.mites * PropSurviveMiteW 

1416 SurviveMitesD = DrnEmerge.mites * PropSurviveMiteD 

1417 

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

1419 

1420 NewMitesW = SurviveMitesW * ReproMitePerCellW 

1421 NewMitesD = SurviveMitesD * ReproMitePerCellD 

1422 

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

1424 SurviveMitesW = SurviveMitesW * WkrEmerge.get_prop_virgins() 

1425 SurviveMitesD = SurviveMitesD * DrnEmerge.get_prop_virgins() 

1426 

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

1428 

1429 RunMiteVirgins = self.run_mite * self.prop_rm_virgins 

1430 RunMiteW = NewMitesW + (SurviveMitesW * PROPRUNMITE2) 

1431 RunMiteD = NewMitesD + (SurviveMitesD * PROPRUNMITE2) 

1432 

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

1434 self.m_mites_dying_today = ( 

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

1436 ) 

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

1438 

1439 # Add new running mites 

1440 self.run_mite = self.run_mite + RunMiteD + RunMiteW 

1441 

1442 # Update proportion of virgins 

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

1444 self.prop_rm_virgins = 1.0 

1445 else: 

1446 numerator = ( 

1447 RunMiteVirgins.get_total() 

1448 + NewMitesW.get_total() 

1449 + NewMitesD.get_total() 

1450 ) 

1451 self.prop_rm_virgins = ( 

1452 numerator / self.run_mite.get_total() 

1453 if self.run_mite.get_total() > 0 

1454 else 1.0 

1455 ) 

1456 # Clamp 

1457 if self.prop_rm_virgins > 1.0: 

1458 self.prop_rm_virgins = 1.0 

1459 if self.prop_rm_virgins < 0.0: 

1460 self.prop_rm_virgins = 0.0 

1461 

1462 # Kill NonResistant Running Mites if Treatment Enabled 

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

1464 the_date = self.get_day_num_date(day_num) 

1465 the_item = None 

1466 the_item = self.m_mite_treatment_info.get_active_item(the_date) 

1467 has_item = the_item is not None 

1468 if has_item and the_item: 

1469 Quan = self.run_mite.get_total() 

1470 # Reduce non-resistant proportion 

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

1472 self.run_mite, "set_non_resistant" 

1473 ): 

1474 new_nonres = ( 

1475 self.run_mite.get_non_resistant() 

1476 * (100.0 - the_item.pct_mortality) 

1477 / 100.0 

1478 ) 

1479 self.run_mite.set_non_resistant(new_nonres) 

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

1481 

1482 self.m_mites_dying_this_period += self.m_mites_dying_today 

1483 

1484 def requeen_if_needed( 

1485 self, 

1486 sim_day_num, 

1487 event, 

1488 egg_laying_delay, 

1489 wkr_drn_ratio, 

1490 enable_requeen, 

1491 scheduled, 

1492 queen_strength, 

1493 rq_once, 

1494 requeen_date, 

1495 ): 

1496 """ 

1497 Port of CColony::ReQueenIfNeeded 

1498 

1499 Two modes: 

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

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

1502 

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

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

1505 """ 

1506 applied_strength = queen_strength 

1507 

1508 if not enable_requeen: 

1509 return 

1510 

1511 try: 

1512 if scheduled == 0: 

1513 # Scheduled re-queening: 

1514 # initial: year, month, day must match 

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

1516 ev_time = event.get_time() 

1517 try: 

1518 rd_year = requeen_date.year 

1519 rd_month = requeen_date.month 

1520 rd_day = requeen_date.day 

1521 except Exception: 

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

1523 return 

1524 

1525 if ( 

1526 rd_year == ev_time.year 

1527 and rd_month == ev_time.month 

1528 and rd_day == ev_time.day 

1529 ) or ( 

1530 (rd_year < ev_time.year) 

1531 and (rd_month == ev_time.month) 

1532 and (rd_day == ev_time.day) 

1533 and (rq_once != 0) 

1534 ): 

1535 if self.m_RQQueenStrengthArray: 

1536 applied_strength = self.m_RQQueenStrengthArray.pop(0) 

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

1538 self.add_event_notification( 

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

1540 ) 

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

1542 else: 

1543 # Automatic re-queening 

1544 month = event.get_time().month 

1545 if ( 

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

1547 and (month > 3) 

1548 and (month < 10) 

1549 ): 

1550 if self.m_RQQueenStrengthArray: 

1551 applied_strength = self.m_RQQueenStrengthArray.pop(0) 

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

1553 self.add_event_notification( 

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

1555 ) 

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

1557 except Exception: 

1558 # On unexpected errors, do not requeen 

1559 return 

1560 

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

1562 # pass 

1563 

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

1565 # pass 

1566 

1567 def set_spore_treatment(self, start_day_num, enable): 

1568 # Port of CColony::SetSporeTreatment 

1569 if enable: 

1570 self.m_SPStart = start_day_num 

1571 self.m_SPEnable = True 

1572 else: 

1573 self.m_SPEnable = False 

1574 self.m_SPTreatmentActive = False 

1575 

1576 def remove_drone_comb(self, pct): 

1577 # Port of CColony::RemoveDroneComb 

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

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

1580 if pct > 100: 

1581 pct = 100.0 

1582 if pct < 0: 

1583 pct = 0.0 

1584 

1585 # Apply to drone eggs 

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

1587 if hasattr(egg, "number"): 

1588 egg.number *= int( 

1589 100.0 - pct 

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

1591 

1592 # Apply to drone larvae 

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

1594 if hasattr(larva, "number"): 

1595 larva.number *= int( 

1596 100.0 - pct 

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

1598 

1599 # Apply to drone capped brood 

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

1601 if hasattr(brood, "number"): 

1602 brood.number *= int( 

1603 100.0 - pct 

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

1605 if hasattr(brood, "mites"): 

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

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

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

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

1610 if hasattr(brood, "set_prop_virgins"): 

1611 brood.set_prop_virgins(0.0) 

1612 

1613 def add_discrete_event(self, date_stg, event_id): 

1614 # Port of CColony::AddDiscreteEvent 

1615 if date_stg in self.m_event_map: 

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

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

1618 else: 

1619 # Create new map element 

1620 self.m_event_map[date_stg] = [event_id] 

1621 

1622 def remove_discrete_event(self, date_stg, event_id): 

1623 # Port of CColony::RemoveDiscreteEvent 

1624 if date_stg in self.m_event_map: 

1625 # Date exists 

1626 event_array = self.m_event_map[date_stg] 

1627 # Remove all occurrences of event_id 

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

1629 

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

1631 del self.m_event_map[date_stg] 

1632 

1633 def get_discrete_events(self, key): 

1634 # Port of CColony::GetDiscreteEvents 

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

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

1637 

1638 def do_pending_events(self, weather_event, current_sim_day): 

1639 # Port of CColony::DoPendingEvents 

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

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

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

1643 

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

1645 if not event_array: 

1646 return 

1647 

1648 for event_id in event_array: 

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

1650 EggLayDelay = 17 

1651 Strength = 5 

1652 

1653 if event_id == DE_SWARM: # Swarm 

1654 self.add_event_notification( 

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

1656 "Detected SWARM Discrete Event", 

1657 ) 

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

1659 self.foragers.factor_quantity(0.75) 

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

1661 self.wadl.factor_quantity(0.75) 

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

1663 self.dadl.factor_quantity(0.75) 

1664 

1665 elif event_id == DE_CHALKBROOD: # Chalk Brood 

1666 # All Larvae Die 

1667 self.add_event_notification( 

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

1669 "Detected CHALKBROOD Discrete Event", 

1670 ) 

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

1672 self.dlarv.factor_quantity(0.0) 

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

1674 self.wlarv.factor_quantity(0.0) 

1675 

1676 elif event_id == DE_RESOURCEDEP: # Resource Depletion 

1677 # Forager Lifespan = minimum 

1678 self.add_event_notification( 

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

1680 "Detected RESOURCEDEPLETION Discrete Event", 

1681 ) 

1682 self.m_init_cond.m_ForagerLifespan = 4 

1683 

1684 elif event_id == DE_SUPERCEDURE: # Supercedure of Queen 

1685 # New queen = 17 days before egg laying starts 

1686 self.add_event_notification( 

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

1688 "Detected SUPERCEDURE Discrete Event", 

1689 ) 

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

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

1692 

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

1694 # 25% of foragers die 

1695 self.add_event_notification( 

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

1697 "Detected PESTICIDE Discrete Event", 

1698 ) 

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

1700 self.foragers.factor_quantity(0.75) 

1701 

1702 def get_mites_dying_today(self): 

1703 # Port of CColony::GetMitesDyingToday 

1704 return self.m_mites_dying_today 

1705 

1706 def get_nurse_bees(self): 

1707 # Port of CColony::GetNurseBees 

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

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

1710 return total_larvae // 2 

1711 

1712 def get_total_mite_count(self): 

1713 # Port of CColony::GetTotalMiteCount 

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

1715 run_mite_total = ( 

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

1717 ) 

1718 capdrn_mites = ( 

1719 self.capdrn.get_mite_count() 

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

1721 else 0 

1722 ) 

1723 capwkr_mites = ( 

1724 self.capwkr.get_mite_count() 

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

1726 else 0 

1727 ) 

1728 return run_mite_total + capdrn_mites + capwkr_mites 

1729 

1730 def set_start_sample_period(self): 

1731 # Port of CColony::SetStartSamplePeriod 

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

1733 # daily data this is used to reset accumulators. 

1734 self.m_mites_dying_this_period = 0.0 

1735 

1736 def get_mites_dying_this_period(self): 

1737 # Port of CColony::GetMitesDyingThisPeriod 

1738 return self.m_mites_dying_this_period 

1739 

1740 def apply_pesticide_mortality(self): 

1741 """ 

1742 Port of CColony::ApplyPesticideMortality 

1743 

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

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

1746 

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

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

1749 

1750 """ 

1751 # Worker Larvae 4 

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

1753 # { 

1754 self.m_dead_worker_larvae_pesticide = self.apply_pesticide_to_bees( 

1755 self.wlarv, 

1756 3, 

1757 3, 

1758 self.m_epadata.m_D_L4, 

1759 0, 

1760 self.m_epadata.m_AI_LarvaLD50, 

1761 self.m_epadata.m_AI_LarvaSlope, 

1762 ) 

1763 if self.m_epadata.m_D_L4 > self.m_epadata.m_D_L4_Max: 

1764 self.m_epadata.m_D_L4_Max = self.m_epadata.m_D_L4 

1765 # } 

1766 

1767 # Worker Larvae 5 

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

1769 self.m_dead_worker_larvae_pesticide += self.apply_pesticide_to_bees( 

1770 self.wlarv, 

1771 4, 

1772 4, 

1773 self.m_epadata.m_D_L5, 

1774 self.m_epadata.m_D_L5_Max, 

1775 self.m_epadata.m_AI_LarvaLD50, 

1776 self.m_epadata.m_AI_LarvaSlope, 

1777 ) 

1778 self.m_epadata.m_D_L5_Max = self.m_epadata.m_D_L5 

1779 

1780 # Drone Larvae 

1781 self.m_dead_drone_larvae_pesticide = self.apply_pesticide_to_bees( 

1782 self.dlarv, 

1783 3, 

1784 3, 

1785 self.m_epadata.m_D_LD, 

1786 0, 

1787 self.m_epadata.m_AI_LarvaLD50, 

1788 self.m_epadata.m_AI_LarvaSlope, 

1789 ) # New L4 drones 

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

1791 self.m_dead_drone_larvae_pesticide += self.apply_pesticide_to_bees( 

1792 self.dlarv, 

1793 4, 

1794 DLARVLIFE - 1, 

1795 self.m_epadata.m_D_LD, 

1796 self.m_epadata.m_D_LD_Max, 

1797 self.m_epadata.m_AI_LarvaLD50, 

1798 self.m_epadata.m_AI_LarvaSlope, 

1799 ) 

1800 self.m_epadata.m_D_LD_Max = self.m_epadata.m_D_LD 

1801 

1802 # Worker Adults 1-3 

1803 self.m_dead_worker_adults_pesticide = self.apply_pesticide_to_bees( 

1804 self.wadl, 

1805 0, 

1806 0, 

1807 self.m_epadata.m_D_A13, 

1808 0, 

1809 self.m_epadata.m_AI_AdultLD50, 

1810 self.m_epadata.m_AI_AdultSlope, 

1811 ) # New adults 

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

1813 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1814 self.wadl, 

1815 1, 

1816 2, 

1817 self.m_epadata.m_D_A13, 

1818 self.m_epadata.m_D_A13_Max, 

1819 self.m_epadata.m_AI_AdultLD50, 

1820 self.m_epadata.m_AI_AdultSlope, 

1821 ) 

1822 self.m_epadata.m_D_A13_Max = self.m_epadata.m_D_A13 

1823 

1824 # Worker Adults 4-10 

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

1826 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1827 self.wadl, 

1828 3, 

1829 9, 

1830 self.m_epadata.m_D_A410, 

1831 self.m_epadata.m_D_A410_Max, 

1832 self.m_epadata.m_AI_AdultLD50, 

1833 self.m_epadata.m_AI_AdultSlope, 

1834 ) 

1835 self.m_epadata.m_D_A410_Max = self.m_epadata.m_D_A410 

1836 

1837 # Worker Adults 11-20 

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

1839 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees( 

1840 self.wadl, 

1841 10, 

1842 WADLLIFE - 1, 

1843 self.m_epadata.m_D_A1120, 

1844 self.m_epadata.m_D_A1120_Max, 

1845 self.m_epadata.m_AI_AdultLD50, 

1846 self.m_epadata.m_AI_AdultSlope, 

1847 ) 

1848 self.m_epadata.m_D_A1120_Max = self.m_epadata.m_D_A1120 

1849 

1850 # Worker Drones 

1851 self.m_dead_drone_adults_pesticide = self.apply_pesticide_to_bees( 

1852 self.dadl, 

1853 0, 

1854 0, 

1855 self.m_epadata.m_D_AD, 

1856 0, 

1857 self.m_epadata.m_AI_AdultLD50, 

1858 self.m_epadata.m_AI_AdultSlope, 

1859 ) 

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

1861 self.m_dead_drone_adults_pesticide += self.apply_pesticide_to_bees( 

1862 self.dadl, 

1863 1, 

1864 DADLLIFE - 1, 

1865 self.m_epadata.m_D_AD, 

1866 self.m_epadata.m_D_AD_Max, 

1867 self.m_epadata.m_AI_AdultLD50, 

1868 self.m_epadata.m_AI_AdultSlope, 

1869 ) 

1870 self.m_epadata.m_D_AD_Max = self.m_epadata.m_D_AD 

1871 

1872 # Foragers - Contact Mortality 

1873 self.m_dead_foragers_pesticide = self.apply_pesticide_to_bees( 

1874 self.foragers, 

1875 0, 

1876 0, 

1877 self.m_epadata.m_D_C_Foragers, 

1878 0, 

1879 self.m_epadata.m_AI_AdultLD50_Contact, 

1880 self.m_epadata.m_AI_AdultSlope_Contact, 

1881 ) 

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

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

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

1885 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1886 self.foragers, 

1887 1, 

1888 forager_length, 

1889 self.m_epadata.m_D_C_Foragers, 

1890 self.m_epadata.m_D_C_Foragers_Max, 

1891 self.m_epadata.m_AI_AdultLD50_Contact, 

1892 self.m_epadata.m_AI_AdultSlope_Contact, 

1893 ) 

1894 self.m_epadata.m_D_C_Foragers_Max = self.m_epadata.m_D_C_Foragers 

1895 

1896 # Foragers - Diet Mortality 

1897 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1898 self.foragers, 

1899 0, 

1900 0, 

1901 self.m_epadata.m_D_D_Foragers, 

1902 0, 

1903 self.m_epadata.m_AI_AdultLD50, 

1904 self.m_epadata.m_AI_AdultSlope, 

1905 ) 

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

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

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

1909 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees( 

1910 self.foragers, 

1911 1, 

1912 forager_length, 

1913 self.m_epadata.m_D_D_Foragers, 

1914 self.m_epadata.m_D_D_Foragers_Max, 

1915 self.m_epadata.m_AI_AdultLD50, 

1916 self.m_epadata.m_AI_AdultSlope, 

1917 ) 

1918 self.m_epadata.m_D_D_Foragers_Max = self.m_epadata.m_D_D_Foragers 

1919 

1920 if self.m_dead_foragers_pesticide > 0: 

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

1922 pass 

1923 

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

1925 self.m_epadata.m_D_L4 = 0 

1926 self.m_epadata.m_D_L5 = 0 

1927 self.m_epadata.m_D_LD = 0 

1928 self.m_epadata.m_D_A13 = 0 

1929 self.m_epadata.m_D_A410 = 0 

1930 self.m_epadata.m_D_A1120 = 0 

1931 self.m_epadata.m_D_AD = 0 

1932 self.m_epadata.m_D_C_Foragers = 0 

1933 self.m_epadata.m_D_D_Foragers = 0 

1934 

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

1936 """ 

1937 Port of CColony::QuantityPesticideToKill 

1938 

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

1940 

1941 Args: 

1942 bee_list: The bee list to calculate mortality for 

1943 current_dose: Current pesticide dose 

1944 max_dose: Previously seen maximum dose 

1945 ld50: Lethal dose 50 value 

1946 slope: Dose-response slope parameter 

1947 

1948 Returns: 

1949 Number of bees that would be killed by pesticide 

1950 """ 

1951 bee_quant = bee_list.get_quantity() 

1952 

1953 # Calculate dose response for current and maximum doses 

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

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

1956 

1957 # Less than max already seen - no additional mortality 

1958 if redux_current <= redux_max: 

1959 return 0 

1960 

1961 # Calculate new bee quantity after mortality 

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

1963 

1964 # Return the number killed by pesticide 

1965 return bee_quant - new_bee_quant 

1966 

1967 def apply_pesticide_to_bees( 

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

1969 ): 

1970 """ 

1971 Port of CColony::ApplyPesticideToBees 

1972 

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

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

1975 

1976 Args: 

1977 bee_list: The bee list to apply mortality to 

1978 from_idx: Starting age index 

1979 to_idx: Ending age index 

1980 current_dose: Current pesticide dose 

1981 max_dose: Previously seen maximum dose 

1982 ld50: Lethal dose 50 value 

1983 slope: Dose-response slope parameter 

1984 

1985 Returns: 

1986 Number of bees killed by pesticide 

1987 """ 

1988 # Get bee quantity in the specified age range 

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

1990 if bee_quant <= 0: 

1991 return 0 

1992 

1993 # Calculate dose response for current and maximum doses 

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

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

1996 

1997 # Less than max already seen - no additional mortality 

1998 if redux_current <= redux_max: 

1999 return 0 

2000 

2001 # Calculate new bee quantity after mortality 

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

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

2004 

2005 # Apply proportional reduction to the specified age range 

2006 bee_list.set_quantity_at_proportional(from_idx, to_idx, prop_redux) 

2007 

2008 # Return the number killed by pesticide 

2009 return int(bee_quant - new_bee_quant) 

2010 

2011 def determine_foliar_dose(self, day_num): 

2012 """ 

2013 Port of CColony::DetermineFoliarDose 

2014 

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

2016 

2017 Args: 

2018 day_num: The simulation day number 

2019 """ 

2020 # Jump out if Foliar is not enabled 

2021 if not self.m_epadata.m_FoliarEnabled: 

2022 return 

2023 

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

2025 current_date = self.get_day_num_date(day_num) 

2026 

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

2028 if ( 

2029 current_date >= self.m_epadata.m_FoliarAppDate 

2030 and current_date >= self.m_epadata.m_FoliarForageBegin 

2031 and current_date < self.m_epadata.m_FoliarForageEnd 

2032 ): 

2033 

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

2035 days_since_application = ( 

2036 current_date - self.m_epadata.m_FoliarAppDate 

2037 ).days 

2038 

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

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

2041 dose = ( 

2042 self.m_epadata.m_E_AppRate 

2043 * self.m_epadata.m_AI_ContactFactor 

2044 / 1000000.0 

2045 ) # convert to Grams AI/bee 

2046 

2047 # Dose reduced due to active ingredient half-life 

2048 if self.m_epadata.m_AI_HalfLife > 0: 

2049 import math 

2050 

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

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

2053 

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

2055 self.m_epadata.m_D_C_Foragers += dose 

2056 

2057 def consume_food(self, event, day_num): 

2058 """ 

2059 Calculate colony food consumption and pesticide exposure. 

2060 

2061 Args: 

2062 event: Current event with date and forage information 

2063 day_num: Current day number in simulation 

2064 """ 

2065 # Skip food consumption on day 1 

2066 if day_num == 1: 

2067 return 

2068 

2069 # Calculate colony needs for pollen and nectar 

2070 pollen_need = self.get_pollen_needs(event) # grams 

2071 nectar_need = self.get_nectar_needs(event) # grams 

2072 

2073 # Get incoming resources from foraging 

2074 incoming_pollen = 0.0 

2075 incoming_nectar = 0.0 

2076 

2077 # Get incoming pesticide concentrations 

2078 c_ai_p = 0.0 # Pesticide concentration in incoming pollen 

2079 c_ai_n = 0.0 # Pesticide concentration in incoming nectar 

2080 

2081 if event.is_forage_day(): 

2082 incoming_pollen = self.get_incoming_pollen_quant() 

2083 incoming_nectar = self.get_incoming_nectar_quant() 

2084 c_ai_p = self.get_incoming_pollen_pesticide_concentration(day_num) 

2085 c_ai_n = self.get_incoming_nectar_pesticide_concentration(day_num) 

2086 

2087 # Process pollen consumption 

2088 c_actual_p = 0.0 

2089 

2090 # First check if supplemental pollen feeding is available 

2091 in_p = incoming_pollen 

2092 if self.is_pollen_feeding_day(event): 

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

2094 if self.m_SuppPollen.m_CurrentAmount >= pollen_need: 

2095 self.m_SuppPollen.m_CurrentAmount -= pollen_need 

2096 pollen_need = 0 # All needs met by supplemental feeding 

2097 c_ai_p = 0 # No pesticide in supplemental feed 

2098 

2099 if in_p >= pollen_need: 

2100 # Sufficient incoming pollen 

2101 c_actual_p = c_ai_p 

2102 # Add remaining pollen to resources 

2103 remaining_pollen = in_p - pollen_need 

2104 if remaining_pollen > 0: 

2105 pollen_resource = ResourceItem( 

2106 resource_quantity=remaining_pollen, 

2107 pesticide_quantity=remaining_pollen * c_ai_p, 

2108 ) 

2109 self.add_pollen_to_resources(pollen_resource) 

2110 else: 

2111 # Need to use stored pollen 

2112 shortfall = pollen_need - in_p 

2113 

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

2115 stored_conc = self.resources.get_pollen_pesticide_concentration() 

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

2117 in_p + shortfall 

2118 ) 

2119 

2120 # Check if we have enough stored pollen 

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

2122 if self.m_NoResourceKillsColony: 

2123 self.kill_colony() 

2124 date_str = event.get_date_stg() 

2125 self.add_event_notification( 

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

2127 ) 

2128 

2129 # Remove pollen from stores 

2130 self.resources.remove_pollen(shortfall) 

2131 

2132 # Process nectar consumption 

2133 c_actual_n = 0.0 

2134 

2135 # First check if supplemental nectar feeding is available 

2136 in_n = incoming_nectar 

2137 if self.is_nectar_feeding_day(event): 

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

2139 if ( 

2140 hasattr(self.m_SuppNectar, "m_StartingAmount") 

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

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

2143 and self.m_SuppNectar.m_BeginDate is not None 

2144 and self.m_SuppNectar.m_EndDate is not None 

2145 ): 

2146 days_in_period = ( 

2147 self.m_SuppNectar.m_EndDate - self.m_SuppNectar.m_BeginDate 

2148 ).days 

2149 if days_in_period > 0: 

2150 daily_nectar_amount = ( 

2151 self.m_SuppNectar.m_StartingAmount / days_in_period 

2152 ) 

2153 if self.m_SuppNectar.m_CurrentAmount >= daily_nectar_amount: 

2154 nectar_resource = ResourceItem( 

2155 resource_quantity=daily_nectar_amount, 

2156 pesticide_quantity=0.0, # No pesticide in supplemental feed 

2157 ) 

2158 self.add_nectar_to_resources(nectar_resource) 

2159 self.m_SuppNectar.m_CurrentAmount -= daily_nectar_amount 

2160 c_ai_n = 0 # No pesticide in supplemental nectar 

2161 

2162 if in_n >= nectar_need: 

2163 # Sufficient incoming nectar 

2164 c_actual_n = c_ai_n 

2165 # Add remaining nectar to resources 

2166 remaining_nectar = in_n - nectar_need 

2167 if remaining_nectar > 0: 

2168 nectar_resource = ResourceItem( 

2169 resource_quantity=remaining_nectar, 

2170 pesticide_quantity=remaining_nectar * c_ai_n, 

2171 ) 

2172 self.add_nectar_to_resources(nectar_resource) 

2173 else: 

2174 # Need to use stored nectar 

2175 shortfall = nectar_need - in_n 

2176 

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

2178 stored_conc = self.resources.get_nectar_pesticide_concentration() 

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

2180 in_n + shortfall 

2181 ) 

2182 

2183 # Check if we have enough stored nectar 

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

2185 if self.m_NoResourceKillsColony: 

2186 self.kill_colony() 

2187 date_str = event.get_date_stg() 

2188 self.add_event_notification( 

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

2190 ) 

2191 

2192 # Remove nectar from stores 

2193 self.resources.remove_nectar(shortfall) 

2194 

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

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

2197 self.m_epadata.m_D_L4 = ( 

2198 c_actual_p * self.m_epadata.m_C_L4_Pollen / 1000.0 

2199 + c_actual_n * self.m_epadata.m_C_L4_Nectar / 1000.0 

2200 ) 

2201 self.m_epadata.m_D_L5 = ( 

2202 c_actual_p * self.m_epadata.m_C_L5_Pollen / 1000.0 

2203 + c_actual_n * self.m_epadata.m_C_L5_Nectar / 1000.0 

2204 ) 

2205 self.m_epadata.m_D_LD = ( 

2206 c_actual_p * self.m_epadata.m_C_LD_Pollen / 1000.0 

2207 + c_actual_n * self.m_epadata.m_C_LD_Nectar / 1000.0 

2208 ) 

2209 self.m_epadata.m_D_A13 = ( 

2210 c_actual_p * self.m_epadata.m_C_A13_Pollen / 1000.0 

2211 + c_actual_n * self.m_epadata.m_C_A13_Nectar / 1000.0 

2212 ) 

2213 self.m_epadata.m_D_A410 = ( 

2214 c_actual_p * self.m_epadata.m_C_A410_Pollen / 1000.0 

2215 + c_actual_n * self.m_epadata.m_C_A410_Nectar / 1000.0 

2216 ) 

2217 self.m_epadata.m_D_A1120 = ( 

2218 c_actual_p * self.m_epadata.m_C_A1120_Pollen / 1000.0 

2219 + c_actual_n * self.m_epadata.m_C_A1120_Nectar / 1000.0 

2220 ) 

2221 self.m_epadata.m_D_AD = ( 

2222 c_actual_p * self.m_epadata.m_C_AD_Pollen / 1000.0 

2223 + c_actual_n * self.m_epadata.m_C_AD_Nectar / 1000.0 

2224 ) 

2225 self.m_epadata.m_D_D_Foragers = ( 

2226 c_actual_p * self.m_epadata.m_C_Forager_Pollen / 1000.0 

2227 + c_actual_n * self.m_epadata.m_C_Forager_Nectar / 1000.0 

2228 ) 

2229 

2230 def get_pollen_needs(self, event): 

2231 """ 

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

2233 

2234 Args: 

2235 event: Current event with date and temperature information 

2236 

2237 Returns: 

2238 float: Pollen needs in grams 

2239 """ 

2240 need = 0.0 

2241 

2242 if event.is_winter_day(): 

2243 # Winter consumption - nurse bees have different rates 

2244 wadl_ag = [ 

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

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

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

2248 ] 

2249 

2250 consumption = [ 

2251 self.m_epadata.m_C_A13_Pollen / 1000.0, 

2252 self.m_epadata.m_C_A410_Pollen / 1000.0, 

2253 self.m_epadata.m_C_A1120_Pollen / 1000.0, 

2254 ] 

2255 

2256 nurse_bee_quantity = self.get_nurse_bees() 

2257 moved_nurse_bees = 0 

2258 

2259 # Allocate nurse bees from youngest age groups first 

2260 for i in range(3): 

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

2262 moved_nurse_bees += wadl_ag[i] 

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

2264 else: 

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

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

2267 moved_nurse_bees += nurse_bee_quantity - moved_nurse_bees 

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

2269 

2270 if moved_nurse_bees >= nurse_bee_quantity: 

2271 break 

2272 

2273 # Non-nurse bees consume 2 mg per day 

2274 non_nurse_bees = self.get_colony_size() - moved_nurse_bees 

2275 need += non_nurse_bees * 0.002 

2276 

2277 # Add forager need 

2278 forager_need = 0.0 

2279 if event.is_forage_day(): 

2280 forager_need = ( 

2281 self.foragers.get_active_quantity() 

2282 * self.m_epadata.m_C_Forager_Pollen 

2283 / 1000.0 

2284 ) 

2285 forager_need += ( 

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

2287 * self.m_epadata.m_C_A1120_Pollen 

2288 / 1000.0 

2289 ) 

2290 else: 

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

2292 

2293 need += forager_need # Already in grams 

2294 else: 

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

2296 # Larvae needs 

2297 l_needs = ( 

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

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

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

2301 ) 

2302 

2303 # Adult needs 

2304 if event.is_forage_day(): 

2305 a_needs = ( 

2306 self.wadl.get_quantity_at_range(0, 2) 

2307 * self.m_epadata.m_C_A13_Pollen 

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

2309 * self.m_epadata.m_C_A410_Pollen 

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

2311 * self.m_epadata.m_C_A1120_Pollen 

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

2313 + self.foragers.get_active_quantity() 

2314 * self.m_epadata.m_C_Forager_Pollen 

2315 + self.foragers.get_unemployed_quantity() 

2316 * self.m_epadata.m_C_A1120_Pollen 

2317 ) 

2318 else: 

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

2320 a_needs = ( 

2321 self.wadl.get_quantity_at_range(0, 2) 

2322 * self.m_epadata.m_C_A13_Pollen 

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

2324 * self.m_epadata.m_C_A410_Pollen 

2325 + ( 

2326 self.wadl.get_quantity_at_range(10, 19) 

2327 + self.foragers.get_quantity() 

2328 ) 

2329 * self.m_epadata.m_C_A1120_Pollen 

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

2331 ) 

2332 

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

2334 

2335 return need 

2336 

2337 def get_nectar_needs(self, event): 

2338 """ 

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

2340 

2341 Args: 

2342 event: Current event with date and temperature information 

2343 

2344 Returns: 

2345 float: Nectar needs in grams 

2346 """ 

2347 need = 0.0 

2348 

2349 if event.is_winter_day(): 

2350 colony_size = self.get_colony_size() 

2351 if colony_size > 0: 

2352 if event.get_temp() <= 8.5: 

2353 # See K. Garber's Winter Failure logic 

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

2355 else: 

2356 # 8.5 < AveTemp < 18.0 

2357 if event.is_forage_day(): 

2358 # Foragers need normal forager nutrition 

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

2360 need = ( 

2361 self.foragers.get_active_quantity() 

2362 * self.m_epadata.m_C_Forager_Nectar 

2363 ) / 1000.0 + 0.05419 * non_foragers * pow( 

2364 0.128 * non_foragers, -0.27 

2365 ) 

2366 else: 

2367 # All bees consume at winter rates 

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

2369 else: 

2370 # Summer day 

2371 # Larvae needs 

2372 l_needs = ( 

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

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

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

2376 ) 

2377 

2378 # Adult needs 

2379 if event.is_forage_day(): 

2380 a_needs = ( 

2381 self.wadl.get_quantity_at_range(0, 2) 

2382 * self.m_epadata.m_C_A13_Nectar 

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

2384 * self.m_epadata.m_C_A410_Nectar 

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

2386 * self.m_epadata.m_C_A1120_Nectar 

2387 + self.foragers.get_unemployed_quantity() 

2388 * self.m_epadata.m_C_A1120_Nectar 

2389 + self.foragers.get_active_quantity() 

2390 * self.m_epadata.m_C_Forager_Nectar 

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

2392 ) 

2393 else: 

2394 # Foragers consume like mature adults 

2395 a_needs = ( 

2396 self.wadl.get_quantity_at_range(0, 2) 

2397 * self.m_epadata.m_C_A13_Nectar 

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

2399 * self.m_epadata.m_C_A410_Nectar 

2400 + ( 

2401 self.wadl.get_quantity_at_range(10, 19) 

2402 + self.foragers.get_quantity() 

2403 ) 

2404 * self.m_epadata.m_C_A1120_Nectar 

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

2406 ) 

2407 

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

2409 

2410 return need 

2411 

2412 def get_incoming_pollen_quant(self): 

2413 """ 

2414 Calculate incoming pollen quantity in grams from foraging. 

2415 

2416 Returns: 

2417 float: Incoming pollen in grams 

2418 """ 

2419 pollen = 0.0 

2420 # Only bring in pollen if there are larvae 

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

2422 pollen = ( 

2423 self.foragers.get_active_quantity() 

2424 * self.m_epadata.m_I_PollenTrips 

2425 * self.m_epadata.m_I_PollenLoad 

2426 / 1000.0 

2427 ) 

2428 return pollen 

2429 

2430 def get_incoming_nectar_quant(self): 

2431 """ 

2432 Calculate incoming nectar quantity in grams from foraging. 

2433 

2434 Returns: 

2435 float: Incoming nectar in grams 

2436 """ 

2437 nectar = ( 

2438 self.foragers.get_active_quantity() 

2439 * self.m_epadata.m_I_NectarTrips 

2440 * self.m_epadata.m_I_NectarLoad 

2441 / 1000.0 

2442 ) 

2443 

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

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

2446 nectar += ( 

2447 self.foragers.get_active_quantity() 

2448 * self.m_epadata.m_I_PollenTrips 

2449 * self.m_epadata.m_I_NectarLoad 

2450 / 1000.0 

2451 ) 

2452 

2453 return nectar 

2454 

2455 def get_incoming_pollen_pesticide_concentration(self, day_num): 

2456 """ 

2457 Calculate incoming pollen pesticide concentration accounting for decay. 

2458 

2459 Args: 

2460 day_num: Current day number in simulation 

2461 

2462 Returns: 

2463 float: Pesticide concentration in grams AI per gram pollen 

2464 """ 

2465 incoming_concentration = 0.0 

2466 cur_date = self.get_day_num_date(day_num) 

2467 

2468 # Check if using nutrient contamination table 

2469 if self.nutrient_ct.is_enabled(): 

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

2471 incoming_concentration = pollen_conc 

2472 else: 

2473 # Normal foliar spray process 

2474 if ( 

2475 self.m_epadata.m_FoliarEnabled 

2476 and cur_date >= self.m_epadata.m_FoliarAppDate 

2477 and cur_date >= self.m_epadata.m_FoliarForageBegin 

2478 and cur_date < self.m_epadata.m_FoliarForageEnd 

2479 ): 

2480 

2481 # Base concentration from foliar spray 

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

2483 # Apply decay due to active ingredient half-life 

2484 days_since_application = ( 

2485 cur_date - self.m_epadata.m_FoliarAppDate 

2486 ).days 

2487 if self.m_epadata.m_AI_HalfLife > 0: 

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

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

2490 

2491 self.add_event_notification( 

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

2493 "Incoming Foliar Spray Pollen Pesticide", 

2494 ) 

2495 

2496 # Seed treatment exposure 

2497 if ( 

2498 cur_date >= self.m_epadata.m_SeedForageBegin 

2499 and cur_date < self.m_epadata.m_SeedForageEnd 

2500 and self.m_epadata.m_SeedEnabled 

2501 ): 

2502 incoming_concentration += ( 

2503 self.m_epadata.m_E_SeedConcentration / 1000000.0 

2504 ) 

2505 self.add_event_notification( 

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

2507 ) 

2508 

2509 # Soil contamination exposure 

2510 if ( 

2511 cur_date >= self.m_epadata.m_SoilForageBegin 

2512 and cur_date < self.m_epadata.m_SoilForageEnd 

2513 and self.m_epadata.m_SoilEnabled 

2514 ): 

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

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

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

2518 soil_conc = ( 

2519 tscf 

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

2521 * self.m_epadata.m_E_SoilConcentration 

2522 * ( 

2523 self.m_epadata.m_E_SoilP 

2524 / ( 

2525 self.m_epadata.m_E_SoilTheta 

2526 + self.m_epadata.m_E_SoilP 

2527 * self.m_epadata.m_AI_KOC 

2528 * self.m_epadata.m_E_SoilFoc 

2529 ) 

2530 ) 

2531 ) 

2532 incoming_concentration += soil_conc / 1000000.0 

2533 self.add_event_notification( 

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

2535 ) 

2536 

2537 return incoming_concentration 

2538 

2539 def get_incoming_nectar_pesticide_concentration(self, day_num): 

2540 """ 

2541 Calculate incoming nectar pesticide concentration accounting for decay. 

2542 

2543 Args: 

2544 day_num: Current day number in simulation 

2545 

2546 Returns: 

2547 float: Pesticide concentration in grams AI per gram nectar 

2548 """ 

2549 incoming_concentration = 0.0 

2550 cur_date = self.get_day_num_date(day_num) 

2551 

2552 # Check if using nutrient contamination table 

2553 if self.nutrient_ct.is_enabled(): 

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

2555 incoming_concentration = nectar_conc 

2556 else: 

2557 # Normal foliar spray process 

2558 if ( 

2559 self.m_epadata.m_FoliarEnabled 

2560 and cur_date >= self.m_epadata.m_FoliarAppDate 

2561 and cur_date >= self.m_epadata.m_FoliarForageBegin 

2562 and cur_date < self.m_epadata.m_FoliarForageEnd 

2563 ): 

2564 

2565 # Base concentration from foliar spray 

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

2567 

2568 # Apply decay due to active ingredient half-life 

2569 days_since_application = ( 

2570 cur_date - self.m_epadata.m_FoliarAppDate 

2571 ).days 

2572 if self.m_epadata.m_AI_HalfLife > 0: 

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

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

2575 

2576 self.add_event_notification( 

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

2578 "Incoming Foliar Spray Nectar Pesticide", 

2579 ) 

2580 

2581 # Seed treatment exposure 

2582 if ( 

2583 cur_date >= self.m_epadata.m_SeedForageBegin 

2584 and cur_date < self.m_epadata.m_SeedForageEnd 

2585 and self.m_epadata.m_SeedEnabled 

2586 ): 

2587 incoming_concentration += ( 

2588 self.m_epadata.m_E_SeedConcentration / 1000000.0 

2589 ) 

2590 self.add_event_notification( 

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

2592 ) 

2593 

2594 # Soil contamination exposure 

2595 if ( 

2596 cur_date >= self.m_epadata.m_SoilForageBegin 

2597 and cur_date < self.m_epadata.m_SoilForageEnd 

2598 and self.m_epadata.m_SoilEnabled 

2599 ): 

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

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

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

2603 soil_conc = ( 

2604 tscf 

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

2606 * self.m_epadata.m_E_SoilConcentration 

2607 * ( 

2608 self.m_epadata.m_E_SoilP 

2609 / ( 

2610 self.m_epadata.m_E_SoilTheta 

2611 + self.m_epadata.m_E_SoilP 

2612 * self.m_epadata.m_AI_KOC 

2613 * self.m_epadata.m_E_SoilFoc 

2614 ) 

2615 ) 

2616 ) 

2617 incoming_concentration += soil_conc / 1000000.0 

2618 self.add_event_notification( 

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

2620 ) 

2621 

2622 return incoming_concentration 

2623 

2624 def is_pollen_feeding_day(self, event): 

2625 """ 

2626 Check if supplemental pollen feeding should occur. 

2627 

2628 Args: 

2629 event: Current event with date information 

2630 

2631 Returns: 

2632 bool: True if pollen feeding should occur 

2633 """ 

2634 feeding_day = False 

2635 

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

2637 if self.m_SuppPollenAnnual: 

2638 # Annual feeding - check within year 

2639 test_begin = event.get_time().replace( 

2640 month=self.m_SuppPollen.m_BeginDate.month, 

2641 day=self.m_SuppPollen.m_BeginDate.day, 

2642 ) 

2643 test_end = event.get_time().replace( 

2644 month=self.m_SuppPollen.m_EndDate.month, 

2645 day=self.m_SuppPollen.m_EndDate.day, 

2646 ) 

2647 

2648 feeding_day = ( 

2649 self.m_SuppPollen.m_CurrentAmount > 0.0 

2650 and test_begin < event.get_time() 

2651 and test_end >= event.get_time() 

2652 ) 

2653 else: 

2654 # Specific date range 

2655 feeding_day = ( 

2656 self.m_SuppPollen.m_CurrentAmount > 0.0 

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

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

2659 ) 

2660 

2661 return feeding_day 

2662 

2663 def is_nectar_feeding_day(self, event): 

2664 """ 

2665 Check if supplemental nectar feeding should occur. 

2666 

2667 Args: 

2668 event: Current event with date information 

2669 

2670 Returns: 

2671 bool: True if nectar feeding should occur 

2672 """ 

2673 feeding_day = False 

2674 

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

2676 if self.m_SuppNectarAnnual: 

2677 # Annual feeding - check within year 

2678 test_begin = event.get_time().replace( 

2679 month=self.m_SuppNectar.m_BeginDate.month, 

2680 day=self.m_SuppNectar.m_BeginDate.day, 

2681 ) 

2682 test_end = event.get_time().replace( 

2683 month=self.m_SuppNectar.m_EndDate.month, 

2684 day=self.m_SuppNectar.m_EndDate.day, 

2685 ) 

2686 feeding_day = ( 

2687 self.m_SuppNectar.m_CurrentAmount > 0.0 

2688 and test_begin < event.get_time() 

2689 and test_end >= event.get_time() 

2690 ) 

2691 else: 

2692 # Specific date range 

2693 feeding_day = ( 

2694 self.m_SuppNectar.m_CurrentAmount > 0.0 

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

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

2697 ) 

2698 

2699 return feeding_day 

2700 

2701 def add_pollen_to_resources(self, resource): 

2702 """ 

2703 Add pollen to colony resources with storage limits. 

2704 

2705 Args: 

2706 resource: ResourceItem object with resource_quantity and pesticide_quantity 

2707 """ 

2708 if self.m_ColonyPolMaxAmount <= 0: 

2709 self.m_ColonyPolMaxAmount = 5000 # Default max 

2710 

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

2712 reduction = 1 - prop_full 

2713 

2714 if prop_full > 0.9: 

2715 resource.resource_quantity *= reduction 

2716 resource.pesticide_quantity *= reduction 

2717 

2718 self.resources.add_pollen(resource) 

2719 

2720 def add_nectar_to_resources(self, resource): 

2721 """ 

2722 Add nectar to colony resources with storage limits. 

2723 

2724 Args: 

2725 resource: ResourceItem object with resource_quantity and pesticide_quantity 

2726 """ 

2727 if self.m_ColonyNecMaxAmount <= 0: 

2728 self.m_ColonyNecMaxAmount = 5000 # Default max 

2729 

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

2731 reduction = 1 - prop_full 

2732 

2733 if reduction < 0: 

2734 reduction = 0 # Don't exceed max value 

2735 

2736 if prop_full > 0.9: 

2737 resource.resource_quantity *= reduction 

2738 resource.pesticide_quantity *= reduction 

2739 

2740 self.resources.add_nectar(resource) 

2741 

2742 def initialize_colony_resources(self): 

2743 """ 

2744 Port of CColony::InitializeColonyResources 

2745 

2746 Initialize colony resources to zero values. 

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

2748 For now, initialize everything to 0.0. 

2749 """ 

2750 self.resources.set_pollen_quantity(0) 

2751 self.resources.set_nectar_quantity(0) 

2752 self.resources.set_pollen_pesticide_quantity(0) 

2753 self.resources.set_nectar_pesticide_quantity(0)