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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 13:34 +0000
1"""BeePop+ Colony Simulation Module.
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.
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.
11Architecture:
12 The Colony class manages multiple interconnected subsystems:
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
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)
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"""
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
84# Life stage durations (from colony.h)
85EGGLIFE = 3
86DLARVLIFE = 7
87WLARVLIFE = 5
88DBROODLIFE = 14
89WBROODLIFE = 13
90DADLLIFE = 21
91WADLLIFE = 21
93# Mite attributes
94PROPINFSTW = 0.08
95PROPINFSTD = 0.92
96MAXMITES_PER_DRONE_CELL = 7
97MAXMITES_PER_WORKER_CELL = 4
99# Discrete event codes
100DE_NONE = 1
101DE_SWARM = 2
102DE_CHALKBROOD = 3
103DE_RESOURCEDEP = 4
104DE_SUPERCEDURE = 5
105DE_PESTICIDE = 6
108class InOutEvent:
109 """Additional statistics container for detailed colony simulation output.
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.
115 Note:
116 All counts are initialized to -1 to indicate unset values.
117 Call reset() to reinitialize all values to -1.
118 """
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
136 def reset(self):
137 """Reset all event counters to uninitialized state (-1).
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
159class Colony:
160 """Main simulation class for honey bee colony dynamics.
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.
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.
171 """
173 def __init__(self, session=None):
174 """Initialize a new Colony instance.
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.
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.
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
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
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
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
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
246 # Spore population
247 self.m_spores = Spores()
249 # Mite treatment info
250 self.m_mite_treatment_info = MiteTreatments()
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
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()
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
288 # Feeding day flags
289 self.m_pollen_feeding_day = False
290 self.m_nectar_feeding_day = False
292 # Sample period and mite death tracking
293 self.m_mites_dying_today = 0.0
294 self.m_mites_dying_this_period = 0.0
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
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
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
325 self.m_InOutEvent = InOutEvent()
327 # Property aliases for consistent naming in consume_food methods
328 @property
329 def epa_data(self):
330 return self.m_epadata
332 @property
333 def nutrient_ct(self):
334 return self.m_nutrient_ct
336 # Methods from colony.h not yet implemented
337 def get_adult_aging_delay(self):
338 return self.m_adult_age_delay_limit
340 def set_adult_aging_delay(self, delay):
341 self.m_adult_age_delay_limit = delay
343 def get_adult_aging_delay_egg_threshold(self):
344 return self.m_adult_aging_delay_egg_threshold
346 def set_adult_aging_delay_egg_threshold(self, threshold):
347 self.m_adult_aging_delay_egg_threshold = threshold
349 def is_adult_aging_delay_armed(self):
350 return self.adult_aging_delay_armed
352 def set_adult_aging_delay_armed(self, armed_state):
353 self.adult_aging_delay_armed = armed_state
355 def set_initialized(self, val):
356 self.has_been_initialized = val
358 def is_initialized(self):
359 return self.has_been_initialized
361 def get_forager_lifespan(self):
362 return self.m_init_cond.m_ForagerLifespan
364 def get_cold_storage_simulator(self):
365 """Return the singleton instance of the cold storage simulator."""
366 return ColdStorageSimulator.get()
368 def get_adult_drones(self):
369 """Get the total number of adult drones."""
370 return self.dadl.get_quantity()
372 def get_adult_workers(self):
373 """Get the total number of adult workers."""
374 return self.wadl.get_quantity()
376 def get_foragers(self):
377 """Get the total number of foragers."""
378 return self.foragers.get_quantity()
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()
386 def get_drone_brood(self):
387 """Get the total number of drone brood."""
388 return self.capdrn.get_quantity()
390 def get_worker_brood(self):
391 """Get the total number of worker brood."""
392 return self.capwkr.get_quantity()
394 def get_drone_larvae(self):
395 """Get the total number of drone larvae."""
396 return self.dlarv.get_quantity()
398 def get_worker_larvae(self):
399 """Get the total number of worker larvae."""
400 return self.wlarv.get_quantity()
402 def get_drone_eggs(self):
403 """Get the total number of drone eggs."""
404 return self.deggs.get_quantity()
406 def get_worker_eggs(self):
407 """Get the total number of worker eggs."""
408 return self.weggs.get_quantity()
410 def get_total_eggs_laid_today(self):
411 """Get the total number of all eggs laid today."""
412 return self.queen.get_teggs()
414 def get_free_mites(self):
415 """Get the number of free mites."""
416 return self.run_mite.get_total()
418 def get_drone_brood_mites(self):
419 """Get the number of mites in drone brood."""
420 return self.capdrn.get_mite_count()
422 def get_worker_brood_mites(self):
423 """Get the number of mites in worker brood."""
424 return self.capwkr.get_mite_count()
426 def get_mites_per_drone_brood(self):
427 """Get the mites per drone brood ratio."""
428 return self.capdrn.get_mites_per_cell()
430 def get_mites_per_worker_brood(self):
431 """Get the mites per worker brood ratio."""
432 return self.capwkr.get_mites_per_cell()
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
444 def get_col_pollen(self):
445 """Get colony pollen amount in grams."""
446 return self.resources.get_pollen_quantity()
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
452 def get_col_nectar(self):
453 """Get colony nectar amount in grams."""
454 return self.resources.get_nectar_quantity()
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
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)
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)
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)
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)
477 def get_dead_foragers_pesticide(self):
478 """Get number of foragers killed by pesticide."""
479 return getattr(self, "m_dead_foragers_pesticide", 0)
481 def get_queen_strength(self):
482 """Get the queen strength."""
483 return self.queen.get_strength() if self.queen else 0.0
485 def get_dd_lower(self):
486 """Get the lower degree day value."""
487 return self.get_dd_today_lower()
489 def get_l_lower(self):
490 """Get the lower L value."""
491 return self.get_l_today_lower()
493 def get_n_lower(self):
494 """Get the lower N value."""
495 return self.get_n_today_lower()
497 def set_mite_pct_resistance(self, pct):
498 self.m_InitMitePctResistant = pct
500 def set_vt_enable(self, value):
501 self.m_vt_enable = value
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)
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
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)
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)
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()
622 def create(self):
623 self.clear() # Clear all lists in case they have been built already
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)
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)
655 # Remove any current list boxcars in preparation for new initialization
656 self.set_default_init_conditions()
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()
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()
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
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 ++)
697 return active
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
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
738 # Initialize Queen
739 self.queen.set_strength(self.m_init_cond.m_QueenStrength)
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)
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)
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 )
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 )
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 )
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)
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)
818 # Set queen day one and egg laying delay
819 self.queen.set_day_one(1)
820 self.queen.set_egg_laying_delay(0)
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 )
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
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)
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)
946 # Reset output data struct for algorithm intermediate results
947 self.m_InOutEvent.reset()
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 )
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)
975 l_DEggs = Egg(self.queen.get_deggs())
976 l_WEggs = Egg(self.queen.get_weggs())
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()
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()
993 self.deggs.update(l_DEggs)
994 self.weggs.update(l_WEggs)
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()
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()
1005 self.dlarv.update(self.deggs.get_caboose())
1006 self.wlarv.update(self.weggs.get_caboose())
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()
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()
1019 self.capdrn.update(self.dlarv.get_caboose())
1020 self.capwkr.update(self.wlarv.get_caboose())
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()
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 )
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 )
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
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 )
1106 # Apply pesticide mortality impacts
1107 self.consume_food(event, day_num)
1108 self.determine_foliar_dose(day_num)
1109 self.apply_pesticide_mortality()
1111 def get_eggs_today(self):
1112 # Returns the total eggs today (worker + drone) from Queen
1113 return self.queen.get_teggs()
1115 def get_dd_today(self):
1116 # Returns DD value for today from Queen
1117 return self.queen.get_DD()
1119 def get_daylight_hrs_today(self, event=None):
1120 # Returns daylight hours for today from Queen
1121 return self.queen.get_L()
1123 def get_l_today(self):
1124 # Returns L value for today from Queen
1125 return self.queen.get_L()
1127 def get_n_today(self):
1128 # Returns N value for today from Queen
1129 return self.queen.get_N()
1131 def get_p_today(self):
1132 # Returns P value for today from Queen
1133 return self.queen.get_P()
1135 def get_dd_today_lower(self):
1136 # Returns dd value for today (lowercase) from Queen
1137 return self.queen.get_dd()
1139 def get_l_today_lower(self):
1140 # Returns l value for today (lowercase) from Queen
1141 return self.queen.get_l()
1143 def get_n_today_lower(self):
1144 # Returns n value for today (lowercase) from Queen
1145 return self.queen.get_n()
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
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)
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)
1197 self.run_mite = run_mite_d + run_mite_w
1199 self.prop_rm_virgins = 1.0
1201 self.m_mites_dying_today = 0.0
1202 self.m_mites_dying_this_period = 0.0
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.
1217 # Reset today's mite death counter
1218 self.m_mites_dying_today = 0.0
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 )
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
1241 DrnBrood = _EmptyBrood()
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
1254 # WMites = RunMite * (I * PROPINFSTW)
1255 WMites = self.run_mite * (I * PROPINFSTW)
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
1265 # DMites = RunMite * (I * PROPINFSTD * Likelihood)
1266 DMites = self.run_mite * (I * PROPINFSTD * Likelihood)
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)
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
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
1296 # Add overflow mites to those available to infest worker brood
1297 WMites = WMites + OverflowMax + OverflowLikelihood
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
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)
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)
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 )
1336 # Use actual Brood objects like C++ CBrood WkrEmerge; CBrood DrnEmerge;
1337 WkrEmerge = Brood()
1338 DrnEmerge = Brood()
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)
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)
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 )
1388 # Survivorship
1389 PropSurviveMiteW = self.m_init_cond.m_workerMiteSurvivorship / 100.0
1390 PropSurviveMiteD = self.m_init_cond.m_droneMiteSurvivorshipField / 100.0
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
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
1413 PROPRUNMITE2 = 0.6
1415 SurviveMitesW = WkrEmerge.mites * PropSurviveMiteW
1416 SurviveMitesD = DrnEmerge.mites * PropSurviveMiteD
1418 NumEmergingMites = SurviveMitesW.get_total() + SurviveMitesD.get_total()
1420 NewMitesW = SurviveMitesW * ReproMitePerCellW
1421 NewMitesD = SurviveMitesD * ReproMitePerCellD
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()
1427 NumVirgins = SurviveMitesW.get_total() + SurviveMitesD.get_total()
1429 RunMiteVirgins = self.run_mite * self.prop_rm_virgins
1430 RunMiteW = NewMitesW + (SurviveMitesW * PROPRUNMITE2)
1431 RunMiteD = NewMitesD + (SurviveMitesD * PROPRUNMITE2)
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)
1439 # Add new running mites
1440 self.run_mite = self.run_mite + RunMiteD + RunMiteW
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
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()
1482 self.m_mites_dying_this_period += self.m_mites_dying_today
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
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)
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
1508 if not enable_requeen:
1509 return
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
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
1561 # def set_miticide_treatment(self, start_day_num, duration, mortality, enable):
1562 # pass
1564 # def set_miticide_treatment_from_treatments(self, treatments, enable):
1565 # pass
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
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
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 ?
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 ?
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)
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]
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]
1630 if len(self.m_event_map[date_stg]) == 0:
1631 del self.m_event_map[date_stg]
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)
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.
1644 event_array = self.get_discrete_events(weather_event.get_date_stg("%m/%d/%Y"))
1645 if not event_array:
1646 return
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
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)
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)
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
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)
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)
1702 def get_mites_dying_today(self):
1703 # Port of CColony::GetMitesDyingToday
1704 return self.m_mites_dying_today
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
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
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
1736 def get_mites_dying_this_period(self):
1737 # Port of CColony::GetMitesDyingThisPeriod
1738 return self.m_mites_dying_this_period
1740 def apply_pesticide_mortality(self):
1741 """
1742 Port of CColony::ApplyPesticideMortality
1744 Applies pesticide mortality to different bee populations based on their current doses
1745 compared to previously seen maximum doses. Updates mortality tracking variables.
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.
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 # }
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
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
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
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
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
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
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
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
1920 if self.m_dead_foragers_pesticide > 0:
1921 # Debug breakpoint placeholder (equivalent to int i = 0; in C++)
1922 pass
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
1935 def quantity_pesticide_to_kill(self, bee_list, current_dose, max_dose, ld50, slope):
1936 """
1937 Port of CColony::QuantityPesticideToKill
1939 This just calculates the number of bees in the list that would be killed by the pesticide and dose.
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
1948 Returns:
1949 Number of bees that would be killed by pesticide
1950 """
1951 bee_quant = bee_list.get_quantity()
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)
1957 # Less than max already seen - no additional mortality
1958 if redux_current <= redux_max:
1959 return 0
1961 # Calculate new bee quantity after mortality
1962 new_bee_quant = int(bee_quant * (1 - (redux_current - redux_max)))
1964 # Return the number killed by pesticide
1965 return bee_quant - new_bee_quant
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
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.
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
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
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)
1997 # Less than max already seen - no additional mortality
1998 if redux_current <= redux_max:
1999 return 0
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
2005 # Apply proportional reduction to the specified age range
2006 bee_list.set_quantity_at_proportional(from_idx, to_idx, prop_redux)
2008 # Return the number killed by pesticide
2009 return int(bee_quant - new_bee_quant)
2011 def determine_foliar_dose(self, day_num):
2012 """
2013 Port of CColony::DetermineFoliarDose
2015 If we are in a date range with Dose, this routine adds to the Dose rate variables.
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
2024 # Get the current date (matches C++ COleDateTime* pDate = GetDayNumDate(DayNum))
2025 current_date = self.get_day_num_date(day_num)
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 ):
2034 # Calculate days since application (matches C++ LONG DaysSinceApplication)
2035 days_since_application = (
2036 current_date - self.m_epadata.m_FoliarAppDate
2037 ).days
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
2047 # Dose reduced due to active ingredient half-life
2048 if self.m_epadata.m_AI_HalfLife > 0:
2049 import math
2051 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife
2052 dose *= math.exp(-k * days_since_application)
2054 # Adds to any diet-based exposure. Only foragers impacted.
2055 self.m_epadata.m_D_C_Foragers += dose
2057 def consume_food(self, event, day_num):
2058 """
2059 Calculate colony food consumption and pesticide exposure.
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
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
2073 # Get incoming resources from foraging
2074 incoming_pollen = 0.0
2075 incoming_nectar = 0.0
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
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)
2087 # Process pollen consumption
2088 c_actual_p = 0.0
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
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
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 )
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 )
2129 # Remove pollen from stores
2130 self.resources.remove_pollen(shortfall)
2132 # Process nectar consumption
2133 c_actual_n = 0.0
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
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
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 )
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 )
2192 # Remove nectar from stores
2193 self.resources.remove_nectar(shortfall)
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 )
2230 def get_pollen_needs(self, event):
2231 """
2232 Calculate pollen needs in grams based on colony composition and season.
2234 Args:
2235 event: Current event with date and temperature information
2237 Returns:
2238 float: Pollen needs in grams
2239 """
2240 need = 0.0
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 ]
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 ]
2256 nurse_bee_quantity = self.get_nurse_bees()
2257 moved_nurse_bees = 0
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]
2270 if moved_nurse_bees >= nurse_bee_quantity:
2271 break
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
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
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 )
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 )
2333 need = (l_needs + a_needs) / 1000.0 # Convert to grams
2335 return need
2337 def get_nectar_needs(self, event):
2338 """
2339 Calculate nectar needs in grams based on colony composition and season.
2341 Args:
2342 event: Current event with date and temperature information
2344 Returns:
2345 float: Nectar needs in grams
2346 """
2347 need = 0.0
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 )
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 )
2408 need = (l_needs + a_needs) / 1000.0 # Convert to grams
2410 return need
2412 def get_incoming_pollen_quant(self):
2413 """
2414 Calculate incoming pollen quantity in grams from foraging.
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
2430 def get_incoming_nectar_quant(self):
2431 """
2432 Calculate incoming nectar quantity in grams from foraging.
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 )
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 )
2453 return nectar
2455 def get_incoming_pollen_pesticide_concentration(self, day_num):
2456 """
2457 Calculate incoming pollen pesticide concentration accounting for decay.
2459 Args:
2460 day_num: Current day number in simulation
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)
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 ):
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)
2491 self.add_event_notification(
2492 cur_date.strftime("%m/%d/%Y"),
2493 "Incoming Foliar Spray Pollen Pesticide",
2494 )
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 )
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 )
2537 return incoming_concentration
2539 def get_incoming_nectar_pesticide_concentration(self, day_num):
2540 """
2541 Calculate incoming nectar pesticide concentration accounting for decay.
2543 Args:
2544 day_num: Current day number in simulation
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)
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 ):
2565 # Base concentration from foliar spray
2566 incoming_concentration = 110.0 * self.m_epadata.m_E_AppRate / 1000000.0
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)
2576 self.add_event_notification(
2577 cur_date.strftime("%m/%d/%Y"),
2578 "Incoming Foliar Spray Nectar Pesticide",
2579 )
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 )
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 )
2622 return incoming_concentration
2624 def is_pollen_feeding_day(self, event):
2625 """
2626 Check if supplemental pollen feeding should occur.
2628 Args:
2629 event: Current event with date information
2631 Returns:
2632 bool: True if pollen feeding should occur
2633 """
2634 feeding_day = False
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 )
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 )
2661 return feeding_day
2663 def is_nectar_feeding_day(self, event):
2664 """
2665 Check if supplemental nectar feeding should occur.
2667 Args:
2668 event: Current event with date information
2670 Returns:
2671 bool: True if nectar feeding should occur
2672 """
2673 feeding_day = False
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 )
2699 return feeding_day
2701 def add_pollen_to_resources(self, resource):
2702 """
2703 Add pollen to colony resources with storage limits.
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
2711 prop_full = self.resources.get_pollen_quantity() / self.m_ColonyPolMaxAmount
2712 reduction = 1 - prop_full
2714 if prop_full > 0.9:
2715 resource.resource_quantity *= reduction
2716 resource.pesticide_quantity *= reduction
2718 self.resources.add_pollen(resource)
2720 def add_nectar_to_resources(self, resource):
2721 """
2722 Add nectar to colony resources with storage limits.
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
2730 prop_full = self.resources.get_nectar_quantity() / self.m_ColonyNecMaxAmount
2731 reduction = 1 - prop_full
2733 if reduction < 0:
2734 reduction = 0 # Don't exceed max value
2736 if prop_full > 0.9:
2737 resource.resource_quantity *= reduction
2738 resource.pesticide_quantity *= reduction
2740 self.resources.add_nectar(resource)
2742 def initialize_colony_resources(self):
2743 """
2744 Port of CColony::InitializeColonyResources
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)