Coverage for pybeepop/beepop/colony.py: 82%
1146 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-19 20:58 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-19 20:58 +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
60import math
61from datetime import datetime, timedelta
62from types import SimpleNamespace
64from pybeepop.beepop.beelist import (
65 AdultList,
66 BroodList,
67 EggList,
68 ForagerListA,
69 LarvaList,
70)
71from pybeepop.beepop.brood import Brood
72from pybeepop.beepop.coldstoragesimulator import ColdStorageSimulator
73from pybeepop.beepop.colonyresource import ColonyResource, ResourceItem
74from pybeepop.beepop.daterangevalues import DateRangeValues
75from pybeepop.beepop.egg import Egg
76from pybeepop.beepop.epadata import EPAData
77from pybeepop.beepop.globaloptions import GlobalOptions
78from pybeepop.beepop.mite import Mite
79from pybeepop.beepop.mitetreatments import MiteTreatments
80from pybeepop.beepop.nutrientcontaminationtable import NutrientContaminationTable
81from pybeepop.beepop.queen import Queen
82from pybeepop.beepop.spores import Spores
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_SPStart = 0
298 self.m_SPEnable = False
299 self.m_SPTreatmentActive = False
300 self.m_InitMitePctResistant = 0.0
301 self.m_CurrentForagerLifespan = 0
302 self.m_RQQueenStrengthArray = []
303 self.m_NutrientContEnabled = False
304 self.m_SuppPollenEnabled = False
305 self.m_SuppNectarEnabled = False
306 self.m_SuppPollenAnnual = False
307 self.m_SuppNectarAnnual = False
309 # Supplemental feeding resource objects (match C++ structure)
310 self.m_SuppPollen = SimpleNamespace()
311 self.m_SuppPollen.m_BeginDate = datetime.now()
312 self.m_SuppPollen.m_EndDate = datetime.now()
313 self.m_SuppPollen.m_CurrentAmount = 0.0
314 self.m_SuppPollen.m_StartingAmount = 0.0
316 self.m_SuppNectar = SimpleNamespace()
317 self.m_SuppNectar.m_BeginDate = datetime.now()
318 self.m_SuppNectar.m_EndDate = datetime.now()
319 self.m_SuppNectar.m_CurrentAmount = 0.0
320 self.m_SuppNectar.m_StartingAmount = 0.0
322 self.m_InOutEvent = InOutEvent()
324 # Property aliases for consistent naming in consume_food methods
325 @property
326 def epa_data(self):
327 return self.m_epadata
329 @property
330 def nutrient_ct(self):
331 return self.m_nutrient_ct
333 # Methods from colony.h not yet implemented
334 def get_adult_aging_delay(self):
335 return self.m_adult_age_delay_limit
337 def set_adult_aging_delay(self, delay):
338 self.m_adult_age_delay_limit = delay
340 def get_adult_aging_delay_egg_threshold(self):
341 return self.m_adult_aging_delay_egg_threshold
343 def set_adult_aging_delay_egg_threshold(self, threshold):
344 self.m_adult_aging_delay_egg_threshold = threshold
346 def is_adult_aging_delay_armed(self):
347 return self.adult_aging_delay_armed
349 def set_adult_aging_delay_armed(self, armed_state):
350 self.adult_aging_delay_armed = armed_state
352 def set_initialized(self, val):
353 self.has_been_initialized = val
355 def is_initialized(self):
356 return self.has_been_initialized
358 def get_forager_lifespan(self):
359 return self.m_init_cond.m_ForagerLifespan
361 def get_cold_storage_simulator(self):
362 """Return the singleton instance of the cold storage simulator."""
363 return ColdStorageSimulator.get()
365 def get_adult_drones(self):
366 """Get the total number of adult drones."""
367 return self.dadl.get_quantity()
369 def get_adult_workers(self):
370 """Get the total number of adult workers."""
371 return self.wadl.get_quantity()
373 def get_foragers(self):
374 """Get the total number of foragers."""
375 return self.foragers.get_quantity()
377 def get_active_foragers(self):
378 """Get the number of active foragers following C++ logic."""
379 # Following C++ CForagerlistA::GetActiveQuantity() logic:
380 # Limits active foragers to a proportion of total colony size
381 return self.foragers.get_active_quantity()
383 def get_drone_brood(self):
384 """Get the total number of drone brood."""
385 return self.capdrn.get_quantity()
387 def get_worker_brood(self):
388 """Get the total number of worker brood."""
389 return self.capwkr.get_quantity()
391 def get_drone_larvae(self):
392 """Get the total number of drone larvae."""
393 return self.dlarv.get_quantity()
395 def get_worker_larvae(self):
396 """Get the total number of worker larvae."""
397 return self.wlarv.get_quantity()
399 def get_drone_eggs(self):
400 """Get the total number of drone eggs."""
401 return self.deggs.get_quantity()
403 def get_worker_eggs(self):
404 """Get the total number of worker eggs."""
405 return self.weggs.get_quantity()
407 def get_total_eggs_laid_today(self):
408 """Get the total number of all eggs laid today."""
409 return self.queen.get_teggs()
411 def get_free_mites(self):
412 """Get the number of free mites."""
413 return self.run_mite.get_total()
415 def get_drone_brood_mites(self):
416 """Get the number of mites in drone brood."""
417 return self.capdrn.get_mite_count()
419 def get_worker_brood_mites(self):
420 """Get the number of mites in worker brood."""
421 return self.capwkr.get_mite_count()
423 def get_mites_per_drone_brood(self):
424 """Get the mites per drone brood ratio."""
425 return self.capdrn.get_mites_per_cell()
427 def get_mites_per_worker_brood(self):
428 """Get the mites per worker brood ratio."""
429 return self.capwkr.get_mites_per_cell()
431 def get_prop_mites_dying(self):
432 """Get the proportion of mites dying."""
433 if (self.get_mites_dying_this_period() + self.get_total_mite_count()) > 0:
434 proportion_dying = self.get_mites_dying_this_period() / (
435 self.get_mites_dying_this_period() + self.get_total_mite_count()
436 )
437 return proportion_dying
438 else:
439 return 0.0
441 def get_col_pollen(self):
442 """Get colony pollen amount in grams."""
443 return self.resources.get_pollen_quantity()
445 def get_pollen_pest_conc(self):
446 """Get pollen pesticide concentration in ug/g."""
447 return self.resources.get_pollen_pesticide_concentration() * 1000000.0
449 def get_col_nectar(self):
450 """Get colony nectar amount in grams."""
451 return self.resources.get_nectar_quantity()
453 def get_nectar_pest_conc(self):
454 """Get nectar pesticide concentration in ug/g."""
455 return self.resources.get_nectar_pesticide_concentration() * 1000000.0
457 # Pesticide-specific death getters (to match C++ output behavior)
458 def get_dead_drone_larvae_pesticide(self):
459 """Get number of drone larvae killed by pesticide."""
460 return getattr(self, "m_dead_drone_larvae_pesticide", 0)
462 def get_dead_worker_larvae_pesticide(self):
463 """Get number of worker larvae killed by pesticide."""
464 return getattr(self, "m_dead_worker_larvae_pesticide", 0)
466 def get_dead_drone_adults_pesticide(self):
467 """Get number of drone adults killed by pesticide."""
468 return getattr(self, "m_dead_drone_adults_pesticide", 0)
470 def get_dead_worker_adults_pesticide(self):
471 """Get number of worker adults killed by pesticide."""
472 return getattr(self, "m_dead_worker_adults_pesticide", 0)
474 def get_dead_foragers_pesticide(self):
475 """Get number of foragers killed by pesticide."""
476 return getattr(self, "m_dead_foragers_pesticide", 0)
478 def get_queen_strength(self):
479 """Get the queen strength."""
480 return self.queen.get_strength() if self.queen else 0.0
482 def get_dd_lower(self):
483 """Get the lower degree day value."""
484 return self.get_dd_today_lower()
486 def get_l_lower(self):
487 """Get the lower L value."""
488 return self.get_l_today_lower()
490 def get_n_lower(self):
491 """Get the lower N value."""
492 return self.get_n_today_lower()
494 def set_mite_pct_resistance(self, pct):
495 """Set the percentage of the initial mite population resistant to treatment."""
496 self.m_InitMitePctResistant = pct
498 def set_vt_enable(self, value):
499 self.m_vt_enable = value
501 def initialize_colony(self):
502 # CRITICAL FIX: Set lengths of the various lists before initializing bees
503 # This matches the C++ CColony::InitializeColony() logic
504 self.deggs.set_length(EGGLIFE)
505 self.deggs.set_prop_transition(1.0)
506 self.weggs.set_length(EGGLIFE)
507 self.weggs.set_prop_transition(1.0)
508 self.dlarv.set_length(DLARVLIFE)
509 self.dlarv.set_prop_transition(1.0)
510 self.wlarv.set_length(WLARVLIFE)
511 self.wlarv.set_prop_transition(1.0)
512 self.capdrn.set_length(DBROODLIFE)
513 self.capdrn.set_prop_transition(1.0)
514 self.capwkr.set_length(WBROODLIFE)
515 self.capwkr.set_prop_transition(1.0)
516 self.dadl.set_length(DADLLIFE)
517 self.dadl.set_prop_transition(1.0)
518 self.wadl.set_length(WADLLIFE)
519 self.wadl.set_prop_transition(1.0)
520 self.wadl.set_colony(self)
521 self.foragers.set_length(self.m_CurrentForagerLifespan)
522 self.foragers.set_colony(self)
524 self.initialize_bees()
525 self.initialize_mites()
526 # Set pesticide Dose rate to 0
527 if self.m_epadata:
528 for attr in [
529 "m_D_L4",
530 "m_D_L5",
531 "m_D_LD",
532 "m_D_A13",
533 "m_D_A410",
534 "m_D_A1120",
535 "m_D_AD",
536 "m_D_C_Foragers",
537 "m_D_D_Foragers",
538 "m_D_L4_Max",
539 "m_D_L5_Max",
540 "m_D_LD_Max",
541 "m_D_A13_Max",
542 "m_D_A410_Max",
543 "m_D_A1120_Max",
544 "m_D_AD_Max",
545 "m_D_C_Foragers_Max",
546 "m_D_D_Foragers_Max",
547 ]:
548 setattr(self.m_epadata, attr, 0)
549 # Set resources
550 if self.resources:
551 self.resources.initialize(
552 self.m_ColonyPolInitAmount, self.m_ColonyNecInitAmount
553 )
554 if self.m_SuppPollen:
555 self.m_SuppPollen.m_CurrentAmount = self.m_SuppPollen.m_StartingAmount
556 if self.m_SuppNectar:
557 self.m_SuppNectar.m_CurrentAmount = self.m_SuppNectar.m_StartingAmount
558 # Set pesticide mortality trackers to zero
559 self.m_dead_worker_larvae_pesticide = 0
560 self.m_dead_drone_larvae_pesticide = 0
561 self.m_dead_worker_adults_pesticide = 0
562 self.m_dead_drone_adults_pesticide = 0
563 self.m_dead_foragers_pesticide = 0
564 self.m_colony_event_list.clear()
565 # Nutrient contamination table logic (if enabled)
566 # Note: In Python version, contamination table is loaded via set_contamination_table method
567 # rather than loading from file during initialization
568 if (
569 self.m_nutrient_ct
570 and getattr(self.m_nutrient_ct, "is_enabled", lambda: False)()
571 ):
572 # Contamination table is already loaded via set_contamination_table
573 pass
574 # Set initial state of AdultAgingDelayArming
575 if self.m_p_session:
576 monthnum = self.m_p_session.get_sim_start().month
577 # Ported logic for arming AdultAgingDelay
578 # Set armed if the first date is January or February (C++: if ((monthnum >= 1) && (monthnum < 3)))
579 if 1 <= monthnum < 3:
580 self.adult_aging_delay_armed = True
581 else:
582 self.adult_aging_delay_armed = False
583 self.has_been_initialized = True
585 def add_event_notification(self, date_stg, msg):
586 event_string = f"{date_stg}: {msg}"
587 if self.m_p_session and self.m_p_session.is_info_reporting_enabled():
588 self.m_colony_event_list.append(event_string)
590 def get_day_num_date(self, day_num):
591 # Returns a date object for the given simulation day number
592 if not self.m_p_session:
593 return None
594 sim_start = self.m_p_session.get_sim_start()
595 # Use timedelta to add days to datetime object
596 return sim_start + timedelta(days=day_num - 1)
598 def kill_colony(self):
599 # Set queen strength to 1 (minimum)
600 if self.queen:
601 self.queen.set_strength(1)
602 # Kill all bee lists (attributes must be set elsewhere)
603 for attr in [
604 "deggs",
605 "weggs",
606 "dlarv",
607 "wlarv",
608 "capdrn",
609 "capwkr",
610 "dadl",
611 "wadl",
612 "foragers",
613 ]:
614 bee_list = getattr(self, attr, None)
615 if bee_list:
616 bee_list.kill_all()
617 if hasattr(self, "foragers"):
618 self.foragers.clear_pending_foragers()
620 def create(self):
621 self.clear() # Clear all lists in case they have been built already
623 # Set lengths and prop transitions for all bee lists
624 self.deggs.set_length(EGGLIFE)
625 self.deggs.set_prop_transition(1.0)
626 self.weggs.set_length(EGGLIFE)
627 self.weggs.set_prop_transition(1.0)
628 self.dlarv.set_length(DLARVLIFE)
629 self.dlarv.set_prop_transition(1.0)
630 self.wlarv.set_length(WLARVLIFE)
631 self.wlarv.set_prop_transition(1.0)
632 self.capdrn.set_length(DBROODLIFE)
633 self.capdrn.set_prop_transition(1.0)
634 self.capwkr.set_length(WBROODLIFE)
635 self.capwkr.set_prop_transition(1.0)
636 self.dadl.set_length(DADLLIFE)
637 self.dadl.set_prop_transition(1.0)
638 self.wadl.set_length(WADLLIFE)
639 self.wadl.set_prop_transition(1.0)
641 # Set colony reference for lists that need it
642 if hasattr(self.wadl, "set_colony"):
643 self.wadl.set_colony(self)
644 if hasattr(self.foragers, "set_length"):
645 self.foragers.set_length(
646 getattr(self, "m_init_cond", None).m_ForagerLifespan
647 if hasattr(self, "m_init_cond")
648 else 12
649 )
650 if hasattr(self.foragers, "set_colony"):
651 self.foragers.set_colony(self)
653 # Remove any current list boxcars in preparation for new initialization
654 self.set_default_init_conditions()
656 def clear(self):
657 # Clear all lists and simulation state
658 for attr in [
659 "deggs",
660 "weggs",
661 "dlarv",
662 "wlarv",
663 "capdrn",
664 "capwkr",
665 "dadl",
666 "wadl",
667 "foragers",
668 ]:
669 bee_list = getattr(self, attr, None)
670 if bee_list:
671 bee_list.kill_all()
672 self.m_colony_event_list.clear()
674 def is_adult_aging_delay_active(self):
675 """
676 Returns True if adult aging delay is active (CColony::IsAdultAgingDelayActive).
677 Logic matches C++ implementation exactly.
678 """
679 # C++ logic: First check if armed and handle disarming
680 egg_quant_threshold = self.get_adult_aging_delay_egg_threshold()
682 if self.is_adult_aging_delay_armed():
683 if self.queen.get_teggs() > egg_quant_threshold:
684 self.set_adult_aging_delay_armed(
685 False
686 ) # Disarm when eggs exceed threshold
687 self.m_days_since_egg_laying_began = 0 # Reset counter
689 # C++ logic: active = ((m_DaysSinceEggLayingBegan++ < m_AdultAgeDelayLimit) && !IsAdultAgingDelayArmed());
690 active = (
691 self.m_days_since_egg_laying_began < self.m_adult_age_delay_limit
692 ) and not self.is_adult_aging_delay_armed()
693 self.m_days_since_egg_laying_began += 1 # Increment counter (C++ does ++)
695 return active
697 def set_default_init_conditions(self):
698 """
699 Sets default initial conditions for the colony (CColony::SetDefaultInitConditions).
700 Resets bee lists and initial state variables.
701 """
702 # Reset bee lists
703 for bee_list in [
704 self.deggs,
705 self.weggs,
706 self.dlarv,
707 self.wlarv,
708 self.capdrn,
709 self.capwkr,
710 self.dadl,
711 self.wadl,
712 self.foragers,
713 ]:
714 if hasattr(bee_list, "clear"):
715 bee_list.clear()
716 # Reset initial conditions
717 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit
718 self.adult_aging_delay_armed = False
719 self.m_dead_worker_larvae_pesticide = 0
720 self.m_dead_drone_larvae_pesticide = 0
721 self.m_dead_worker_adults_pesticide = 0
722 self.m_dead_drone_adults_pesticide = 0
723 self.m_dead_foragers_pesticide = 0
724 self.m_colony_event_list.clear()
725 # Optionally reset other state variables as needed
727 def initialize_bees(self):
728 """
729 Ported from CColony::InitializeBees.
730 Distributes bees from initial conditions into age groupings (boxcars) for each type.
731 """
732 # Set current forager lifespan and adult aging delay
733 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan
734 self.m_days_since_egg_laying_began = self.m_adult_age_delay_limit
736 # Initialize Queen
737 self.queen.set_strength(self.m_init_cond.m_QueenStrength)
739 # CRITICAL: Set forager length again to match C++ InitializeBees() exactly
740 # This is needed because m_CurrentForagerLifespan may have changed from initial conditions
741 self.foragers.set_length(self.m_CurrentForagerLifespan)
742 self.foragers.set_colony(self)
744 # Helper to distribute bees into boxcars
745 def distribute_bees(init_count, bee_list, bee_class):
746 boxcar_len = bee_list.get_length()
747 if boxcar_len == 0:
748 return
749 avg = init_count // boxcar_len
750 remainder = init_count - avg * boxcar_len
751 for i in range(boxcar_len):
752 count = avg if i < (boxcar_len - 1) else avg + remainder
753 bee = bee_class(count)
754 bee_list.add_head(bee)
756 # Eggs
757 distribute_bees(
758 self.m_init_cond.m_droneEggsField, self.deggs, self.deggs.get_bee_class()
759 )
760 distribute_bees(
761 self.m_init_cond.m_workerEggsField, self.weggs, self.weggs.get_bee_class()
762 )
764 # Larvae
765 distribute_bees(
766 self.m_init_cond.m_droneLarvaeField, self.dlarv, self.dlarv.get_bee_class()
767 )
768 distribute_bees(
769 self.m_init_cond.m_workerLarvaeField, self.wlarv, self.wlarv.get_bee_class()
770 )
772 # Capped Brood
773 distribute_bees(
774 self.m_init_cond.m_droneBroodField, self.capdrn, self.capdrn.get_bee_class()
775 )
776 distribute_bees(
777 self.m_init_cond.m_workerBroodField,
778 self.capwkr,
779 self.capwkr.get_bee_class(),
780 )
782 # Drone Adults
783 boxcar_len = self.dadl.get_length()
784 avg = self.m_init_cond.m_droneAdultsField // boxcar_len if boxcar_len else 0
785 remainder = (
786 self.m_init_cond.m_droneAdultsField - avg * boxcar_len if boxcar_len else 0
787 )
788 for i in range(boxcar_len):
789 count = avg if i < (boxcar_len - 1) else avg + remainder
790 drone = self.dadl.get_bee_class()(count)
791 drone.set_lifespan(DADLLIFE)
792 self.dadl.add_head(drone)
794 # Worker Adults and Foragers
795 total_boxcars = self.wadl.get_length() + self.foragers.get_length()
796 avg = (
797 self.m_init_cond.m_workerAdultsField // total_boxcars
798 if total_boxcars
799 else 0
800 )
801 remainder = (
802 self.m_init_cond.m_workerAdultsField - avg * total_boxcars
803 if total_boxcars
804 else 0
805 )
806 for i in range(self.wadl.get_length()):
807 worker = self.wadl.get_bee_class()(avg)
808 worker.set_lifespan(WADLLIFE)
809 self.wadl.add_head(worker)
810 for i in range(self.foragers.get_length()):
811 count = avg if i < (self.foragers.get_length() - 1) else avg + remainder
812 forager = self.foragers.get_bee_class()(count)
813 forager.set_lifespan(self.foragers.get_length())
814 self.foragers.add_head(forager)
816 # Set queen day one and egg laying delay
817 self.queen.set_day_one(1)
818 self.queen.set_egg_laying_delay(0)
820 def get_colony_size(self):
821 """
822 Returns the total colony size (CColony::GetColonySize).
823 Sum of drone adults, worker adults, and foragers.
824 """
825 return int(
826 self.dadl.get_quantity()
827 + self.wadl.get_quantity()
828 + self.foragers.get_quantity()
829 )
831 def update_bees(self, event, day_num):
832 """
833 Ported from CColony::UpdateBees.
834 Updates bee lists and colony state for the current day.
835 """
836 # Calculate larvae per bee
837 total_larvae = self.wlarv.get_quantity() + self.dlarv.get_quantity()
838 total_adults = (
839 self.wadl.get_quantity()
840 + self.dadl.get_quantity()
841 + self.foragers.get_quantity()
842 )
843 # Match C++ behavior - division by zero produces large value that triggers larv_per_bee > 2
844 if total_adults == 0:
845 larv_per_bee = float("inf") # Match C++ division by zero behavior
846 else:
847 larv_per_bee = float(total_larvae) / total_adults
849 # Arm Adult Aging Delay on Jan 1
850 if event.get_time().month == 1 and event.get_time().day == 1:
851 self.set_adult_aging_delay_armed(True)
853 # Apply date range values
854 date_stg = event.get_date_stg("%m/%d/%Y")
855 the_date = event.parse_date(date_stg)
856 if the_date:
857 # Eggs Transition Rate
858 prop_transition = self.m_init_cond.m_EggTransitionDRV.get_active_value(
859 the_date
860 )
861 if (
862 prop_transition is not None
863 and self.m_init_cond.m_EggTransitionDRV.is_enabled()
864 ):
865 self.deggs.set_prop_transition(prop_transition / 100)
866 self.weggs.set_prop_transition(prop_transition / 100)
867 else:
868 self.deggs.set_prop_transition(1.0)
869 self.weggs.set_prop_transition(1.0)
870 # Larvae Transition Rate
871 prop_transition = self.m_init_cond.m_LarvaeTransitionDRV.get_active_value(
872 the_date
873 )
874 if (
875 prop_transition is not None
876 and self.m_init_cond.m_LarvaeTransitionDRV.is_enabled()
877 ):
878 self.dlarv.set_prop_transition(prop_transition / 100)
879 self.wlarv.set_prop_transition(prop_transition / 100)
880 else:
881 self.dlarv.set_prop_transition(1.0)
882 self.wlarv.set_prop_transition(1.0)
883 # Brood Transition Rate
884 prop_transition = self.m_init_cond.m_BroodTransitionDRV.get_active_value(
885 the_date
886 )
887 if (
888 prop_transition is not None
889 and self.m_init_cond.m_BroodTransitionDRV.is_enabled()
890 ):
891 self.capdrn.set_prop_transition(prop_transition / 100)
892 self.capwkr.set_prop_transition(prop_transition / 100)
893 else:
894 self.capdrn.set_prop_transition(1.0)
895 self.capwkr.set_prop_transition(1.0)
896 # Adults Transition Rate
897 prop_transition = self.m_init_cond.m_AdultTransitionDRV.get_active_value(
898 the_date
899 )
900 if (
901 prop_transition is not None
902 and self.m_init_cond.m_AdultTransitionDRV.is_enabled()
903 ):
904 self.dadl.set_prop_transition(prop_transition / 100)
905 self.wadl.set_prop_transition(prop_transition / 100)
906 else:
907 self.dadl.set_prop_transition(1.0)
908 self.wadl.set_prop_transition(1.0)
909 # Adults Lifespan Change
910 adult_age_limit = self.m_init_cond.m_AdultLifespanDRV.get_active_value(
911 the_date
912 )
913 if (
914 adult_age_limit is not None
915 and self.m_init_cond.m_AdultLifespanDRV.is_enabled()
916 ):
917 if self.wadl.get_length() != int(adult_age_limit):
918 self.wadl.update_length(int(adult_age_limit))
919 else:
920 if self.wadl.get_length() != WADLLIFE:
921 self.wadl.update_length(WADLLIFE)
922 # Foragers Lifespan Change
923 forager_lifespan = self.m_init_cond.m_ForagerLifespanDRV.get_active_value(
924 the_date
925 )
926 if (
927 forager_lifespan is not None
928 and self.m_init_cond.m_ForagerLifespanDRV.is_enabled()
929 ):
930 self.m_CurrentForagerLifespan = int(forager_lifespan)
931 else:
932 self.m_CurrentForagerLifespan = self.m_init_cond.m_ForagerLifespan
933 self.foragers.set_length(self.m_CurrentForagerLifespan)
934 else:
935 self.deggs.set_prop_transition(1.0)
936 self.weggs.set_prop_transition(1.0)
937 self.dlarv.set_prop_transition(1.0)
938 self.wlarv.set_prop_transition(1.0)
939 self.capdrn.set_prop_transition(1.0)
940 self.capwkr.set_prop_transition(1.0)
941 self.dadl.set_prop_transition(1.0)
942 self.wadl.set_prop_transition(1.0)
944 # Reset output data struct for algorithm intermediate results
945 self.m_InOutEvent.reset()
947 # Queen lays eggs
948 self.queen.lay_eggs(
949 day_num,
950 event.get_temp(),
951 event.get_daylight_hours(),
952 self.foragers.get_quantity(),
953 larv_per_bee,
954 )
956 # Simulate cold storage
957 cold_storage = self.get_cold_storage_simulator()
958 today = event.get_date_stg()
959 if cold_storage.is_enabled():
960 cs_state_stg = f"On {today} Cold Storage is ENABLED"
961 cold_storage.update(event, self)
962 if cold_storage.is_active_now():
963 cs_state_stg += " and ACTIVE"
964 if cold_storage.is_starting_now():
965 cs_state_stg += " and STARTING"
966 if cold_storage.is_ending_now():
967 cs_state_stg += "and ENDING"
968 if cold_storage.is_on():
969 cs_state_stg += " and ON"
970 if self.m_p_session.is_info_reporting_enabled():
971 self.m_p_session.add_to_info_list(cs_state_stg)
973 l_DEggs = Egg(self.queen.get_deggs())
974 l_WEggs = Egg(self.queen.get_weggs())
976 # At the beginning of cold storage all eggs are lost
977 if cold_storage.is_starting_now():
978 if self.m_p_session.is_info_reporting_enabled():
979 self.m_p_session.add_to_info_list(
980 f"On {today} Cold Storage is STARTING"
981 )
982 l_DEggs.set_number(0)
983 l_WEggs.set_number(0)
984 self.deggs.kill_all()
985 self.weggs.kill_all()
987 # Update stats for new eggs
988 self.m_InOutEvent.m_NewWEggs = l_WEggs.get_number()
989 self.m_InOutEvent.m_NewDEggs = l_DEggs.get_number()
991 self.deggs.update(l_DEggs)
992 self.weggs.update(l_WEggs)
994 # At the beginning of cold storage no eggs become larvae
995 if cold_storage.is_starting_now():
996 self.weggs.get_caboose().reset()
997 self.deggs.get_caboose().reset()
999 # Update stats for new larvae
1000 self.m_InOutEvent.m_WEggsToLarv = self.weggs.get_caboose().get_number()
1001 self.m_InOutEvent.m_DEggsToLarv = self.deggs.get_caboose().get_number()
1003 self.dlarv.update(self.deggs.get_caboose())
1004 self.wlarv.update(self.weggs.get_caboose())
1006 # At the beginning of cold storage no larvae become brood
1007 if cold_storage.is_starting_now():
1008 self.wlarv.get_caboose().reset()
1009 self.dlarv.get_caboose().reset()
1010 self.wlarv.kill_all()
1011 self.dlarv.kill_all()
1013 # Update stats for new brood
1014 self.m_InOutEvent.m_WLarvToBrood = self.wlarv.get_caboose().get_number()
1015 self.m_InOutEvent.m_DLarvToBrood = self.dlarv.get_caboose().get_number()
1017 self.capdrn.update(self.dlarv.get_caboose())
1018 self.capwkr.update(self.wlarv.get_caboose())
1020 # Update stats for new adults
1021 self.m_InOutEvent.m_WBroodToAdult = self.capwkr.get_caboose().get_number()
1022 self.m_InOutEvent.m_DBroodToAdult = self.capdrn.get_caboose().get_number()
1024 number_of_non_adults = (
1025 self.wlarv.get_quantity()
1026 + self.dlarv.get_quantity()
1027 + self.capdrn.get_quantity()
1028 + self.capwkr.get_quantity()
1029 )
1031 # ForageInc validity
1032 global_options = GlobalOptions.get()
1033 forage_inc_is_valid = (
1034 global_options.should_forage_day_election_based_on_temperatures
1035 or event.get_forage_inc() > 0.0
1036 )
1038 if (number_of_non_adults > 0) or (
1039 event.is_forage_day() and forage_inc_is_valid
1040 ):
1041 # Foragers killed due to pesticide
1042 foragers_to_be_killed = self.quantity_pesticide_to_kill(
1043 self.foragers,
1044 self.m_epadata.m_D_C_Foragers,
1045 0,
1046 self.m_epadata.m_AI_AdultLD50_Contact,
1047 self.m_epadata.m_AI_AdultSlope_Contact,
1048 )
1049 foragers_to_be_killed += self.quantity_pesticide_to_kill(
1050 self.foragers,
1051 self.m_epadata.m_D_D_Foragers,
1052 0,
1053 self.m_epadata.m_AI_AdultLD50,
1054 self.m_epadata.m_AI_AdultSlope,
1055 )
1056 min_age_to_forager = 14
1057 self.wadl.move_to_end(foragers_to_be_killed, min_age_to_forager)
1058 if foragers_to_be_killed > 0:
1059 notification = f"{foragers_to_be_killed} Foragers killed by pesticide - recruiting workers"
1060 self.add_event_notification(
1061 event.get_date_stg("%m/%d/%Y"), notification
1062 )
1063 self.m_InOutEvent.m_ForagersKilledByPesticide = foragers_to_be_killed
1065 # Aging adults
1066 aging_adults = not cold_storage.is_active_now() and (
1067 not global_options.should_adults_age_based_laid_eggs
1068 or self.queen.compute_L(event.get_daylight_hours()) > 0
1069 )
1070 if self.is_adult_aging_delay_active():
1071 pass # Corresponds to C++: CString stgDate = pEvent->GetDateStg();
1072 aging_adults = (
1073 aging_adults
1074 and not self.is_adult_aging_delay_active()
1075 and not self.is_adult_aging_delay_armed()
1076 )
1077 if aging_adults:
1078 self.dadl.update(self.capdrn.get_caboose(), self, event, False)
1079 wkr_adl_caboose_number = self.wadl.get_caboose().get_number()
1080 self.wadl.update(self.capwkr.get_caboose(), self, event, True)
1081 drn_number_from_caboose = self.capwkr.get_caboose().get_number()
1082 wkr_adl_caboose_number = self.wadl.get_caboose().get_number()
1083 self.m_InOutEvent.m_WAdultToForagers = (
1084 self.wadl.get_caboose().get_number()
1085 )
1086 self.foragers.update(self.wadl.get_caboose(), self, event)
1087 else:
1088 if (
1089 number_of_non_adults > 0
1090 and global_options.should_adults_age_based_laid_eggs
1091 ):
1092 self.dadl.add(self.capdrn.get_caboose(), self, event, False)
1093 self.wadl.add(self.capwkr.get_caboose(), self, event, True)
1094 self.m_InOutEvent.m_WAdultToForagers = 0
1095 reset_adult = self.dadl.get_bee_class()() # CAdult reset
1096 reset_adult.reset()
1097 self.foragers.update(reset_adult, self, event)
1098 self.m_InOutEvent.m_DeadForagers = max(
1099 0, self.foragers.get_caboose().get_number()
1100 )
1102 # Apply pesticide mortality impacts
1103 self.consume_food(event, day_num)
1104 self.determine_foliar_dose(day_num)
1105 self.apply_pesticide_mortality()
1107 def get_eggs_today(self):
1108 # Returns the total eggs today (worker + drone) from Queen
1109 return self.queen.get_teggs()
1111 def get_dd_today(self):
1112 # Returns DD value for today from Queen
1113 return self.queen.get_DD()
1115 def get_daylight_hrs_today(self, event=None):
1116 # Returns daylight hours for today from Queen
1117 return self.queen.get_L()
1119 def get_l_today(self):
1120 # Returns L value for today from Queen
1121 return self.queen.get_L()
1123 def get_n_today(self):
1124 # Returns N value for today from Queen
1125 return self.queen.get_N()
1127 def get_p_today(self):
1128 # Returns P value for today from Queen
1129 return self.queen.get_P()
1131 def get_dd_today_lower(self):
1132 # Returns dd value for today (lowercase) from Queen
1133 return self.queen.get_dd()
1135 def get_l_today_lower(self):
1136 # Returns l value for today (lowercase) from Queen
1137 return self.queen.get_l()
1139 def get_n_today_lower(self):
1140 # Returns n value for today (lowercase) from Queen
1141 return self.queen.get_n()
1143 def add_mites(self, new_mites):
1144 # Assume new mites are "virgins" (port of CColony::AddMites)
1145 virgins = self.run_mite * self.prop_rm_virgins + new_mites
1146 self.run_mite += new_mites
1147 if self.run_mite.get_total() <= 0:
1148 self.prop_rm_virgins = 1.0
1149 else:
1150 total_run = self.run_mite.get_total()
1151 # avoid division by zero though handled above
1152 self.prop_rm_virgins = (
1153 virgins.get_total() / total_run if total_run > 0 else 1.0
1154 )
1155 # Constrain proportion to be [0..1]
1156 self.prop_rm_virgins = min(self.prop_rm_virgins, 1.0)
1157 self.prop_rm_virgins = max(self.prop_rm_virgins, 0.0)
1159 def initialize_mites(self):
1160 # Initial condition infestation of capped brood (port of CColony::InitializeMites)
1161 w_count = int(
1162 (self.capwkr.get_quantity() * self.m_init_cond.m_workerBroodInfestField)
1163 / 100.0
1164 )
1165 d_count = int(
1166 (self.capdrn.get_quantity() * self.m_init_cond.m_droneBroodInfestField)
1167 / 100.0
1168 )
1169 w_mites = Mite(0, w_count)
1170 d_mites = Mite(0, d_count)
1171 # distribute_mites propagates the resistant proportion into each boxcar
1172 w_mites.set_pct_resistant(self.m_InitMitePctResistant)
1173 d_mites.set_pct_resistant(self.m_InitMitePctResistant)
1174 # Distribute mites into capped brood
1175 self.capwkr.distribute_mites(w_mites)
1176 self.capdrn.distribute_mites(d_mites)
1178 # Initial condition mites on adult bees i.e. Running Mites
1179 run_w_count = int(
1180 (
1181 self.wadl.get_quantity() * self.m_init_cond.m_workerAdultInfestField
1182 + self.foragers.get_quantity()
1183 * self.m_init_cond.m_workerAdultInfestField
1184 )
1185 / 100.0
1186 )
1187 run_d_count = int(
1188 (self.dadl.get_quantity() * self.m_init_cond.m_droneAdultInfestField)
1189 / 100.0
1190 )
1191 run_mite_w = Mite(0, run_w_count)
1192 run_mite_d = Mite(0, run_d_count)
1193 run_mite_w.set_pct_resistant(self.m_InitMitePctResistant)
1194 run_mite_d.set_pct_resistant(self.m_InitMitePctResistant)
1196 self.run_mite = run_mite_d + run_mite_w
1198 self.prop_rm_virgins = 1.0
1200 self.m_mites_dying_today = 0.0
1201 self.m_mites_dying_this_period = 0.0
1203 def update_mites(self, event, day_num):
1204 # Port of CColony::UpdateMites
1205 #
1206 # Assume UpdateMites is called after UpdateBees. This means the
1207 # last Larva boxcar has been moved to the first Brood boxcar and the last
1208 # Brood boxcar has been moved to the first Adult boxcar. Therefore, we infest
1209 # the first Brood boxcar with mites and we have mites emerging from the
1210 # first boxcar in the appropriate Adult list.
1211 #
1212 # The proportion of running mites that have not infested before (prop_rm_virgins)
1213 # is maintained and updated each day. Mites with that proportion infest each day
1214 # and the proportion is updated at the end of this function.
1216 # Reset today's mite death counter
1217 self.m_mites_dying_today = 0.0
1219 # The cells being infested this cycle are the head (index 0) of capped brood lists
1220 WkrBrood = (
1221 self.capwkr.bees[0]
1222 if getattr(self.capwkr, "bees", None) and len(self.capwkr.bees) > 0
1223 else None
1224 )
1225 DrnBrood = (
1226 self.capdrn.bees[0]
1227 if getattr(self.capdrn, "bees", None) and len(self.capdrn.bees) > 0
1228 else None
1229 )
1231 if WkrBrood is None:
1232 # Nothing to do without brood
1233 return
1234 if DrnBrood is None:
1235 # create an empty brood-like object for math, fallback
1236 class _EmptyBrood:
1237 def get_number(self):
1238 return 0
1240 DrnBrood = _EmptyBrood()
1242 # Calculate proportion of RunMites that can invade cells (per Calis)
1243 B = self.get_colony_size() * 0.125 # Weight in grams of colony
1244 if B > 0.0:
1245 rD = 6.49 * (DrnBrood.get_number() / B)
1246 rW = 0.56 * (WkrBrood.get_number() / B)
1247 I = 1 - math.exp(-(rD + rW))
1248 I = max(I, 0.0)
1249 else:
1250 I = 0.0
1252 # WMites = RunMite * (I * PROPINFSTW)
1253 WMites = self.run_mite * (I * PROPINFSTW)
1255 # Likelihood of finding drone cell
1256 if WkrBrood.get_number() > 0:
1257 Likelihood = float(DrnBrood.get_number()) / float(WkrBrood.get_number())
1258 Likelihood = min(Likelihood, 1.0)
1259 else:
1260 Likelihood = 1.0
1262 # DMites = RunMite * (I * PROPINFSTD * Likelihood)
1263 DMites = self.run_mite * (I * PROPINFSTD * Likelihood)
1265 # If no worker targets, send WMites to drone candidates
1266 if WkrBrood.get_number() == 0:
1267 DMites += WMites
1268 WMites.set_resistant(0)
1269 WMites.set_non_resistant(0)
1271 # OverflowLikelihood = RunMite * (I * PROPINFSTD * (1.0 - Likelihood));
1272 OverflowLikelihood = self.run_mite * (I * PROPINFSTD * (1.0 - Likelihood))
1273 # Preserve pct resistant of DMites
1274 try:
1275 OverflowLikelihood.set_pct_resistant(DMites.get_pct_resistant())
1276 except Exception:
1277 pass
1279 # Determine if too many mites/drone cell. If so send excess to worker cells
1280 OverflowMax = Mite(0, 0)
1281 max_allowed = MAXMITES_PER_DRONE_CELL * DrnBrood.get_number()
1282 if DMites.get_total() > max_allowed:
1283 # Don't truncate to int - preserve floating point precision like C++
1284 overflow_count = DMites.get_total() - max_allowed
1285 OverflowMax = Mite(0, overflow_count)
1286 try:
1287 OverflowMax.set_pct_resistant(DMites.get_pct_resistant())
1288 except Exception:
1289 pass
1290 # DMites -= OverflowMax
1291 DMites = DMites - OverflowMax
1293 # Add overflow mites to those available to infest worker brood
1294 WMites = WMites + OverflowMax + OverflowLikelihood
1296 # Limit worker mites per cell
1297 max_w = MAXMITES_PER_WORKER_CELL * WkrBrood.get_number()
1298 if WMites.get_total() > max_w:
1299 pr = WMites.get_pct_resistant()
1300 # Don't truncate to int - preserve floating point precision like C++
1301 WMites = Mite(0, max_w)
1302 try:
1303 WMites.set_pct_resistant(pr)
1304 except Exception:
1305 pass
1307 # Remove the mites used to infest from running mites
1308 self.run_mite = self.run_mite - WMites - DMites
1309 if self.run_mite.get_total() < 0:
1310 self.run_mite = Mite(0, 0)
1312 # Assign mites to the brood head and set prop virgins
1313 WkrBrood.set_mites(WMites)
1314 if hasattr(WkrBrood, "set_prop_virgins"):
1315 WkrBrood.set_prop_virgins(self.prop_rm_virgins)
1316 DrnBrood.set_mites(DMites)
1317 if hasattr(DrnBrood, "set_prop_virgins"):
1318 DrnBrood.set_prop_virgins(self.prop_rm_virgins)
1320 # Emerging mites from first adult boxcar
1321 # Prepare emerge records - USE BROOD OBJECTS LIKE C++
1322 WkrHead = (
1323 self.wadl.bees[0]
1324 if getattr(self.wadl, "bees", None) and len(self.wadl.bees) > 0
1325 else None
1326 )
1327 DrnHead = (
1328 self.dadl.bees[0]
1329 if getattr(self.dadl, "bees", None) and len(self.dadl.bees) > 0
1330 else None
1331 )
1333 # Use actual Brood objects like C++ CBrood WkrEmerge; CBrood DrnEmerge;
1334 WkrEmerge = Brood()
1335 DrnEmerge = Brood()
1337 if WkrHead:
1338 WkrEmerge.number = int(WkrHead.get_number()) # Ensure integer type
1339 WkrEmerge.set_prop_virgins(
1340 WkrHead.get_prop_virgins()
1341 if hasattr(WkrHead, "get_prop_virgins")
1342 else 0.0
1343 )
1344 if WkrHead.have_mites_been_counted():
1345 WkrEmerge.mites = Mite(0, 0)
1346 else:
1347 m = WkrHead.get_mites() if hasattr(WkrHead, "get_mites") else 0
1348 if isinstance(m, Mite):
1349 WkrEmerge.mites = m
1350 else:
1351 # Don't truncate to int - preserve floating point precision like C++
1352 WkrEmerge.mites = Mite(0, m)
1353 WkrHead.set_mites_counted(True)
1355 if DrnHead:
1356 DrnEmerge.number = int(DrnHead.get_number()) # Ensure integer type
1357 DrnEmerge.set_prop_virgins(
1358 DrnHead.get_prop_virgins()
1359 if hasattr(DrnHead, "get_prop_virgins")
1360 else 0.0
1361 )
1362 if DrnHead.have_mites_been_counted():
1363 DrnEmerge.mites = Mite(0, 0)
1364 else:
1365 m = DrnHead.get_mites() if hasattr(DrnHead, "get_mites") else 0
1366 if isinstance(m, Mite):
1367 DrnEmerge.mites = m
1368 else:
1369 # Don't truncate to int - preserve floating point precision like C++
1370 DrnEmerge.mites = Mite(0, m)
1371 DrnHead.set_mites_counted(True)
1373 # Mites per cell - USE DIRECT ACCESS LIKE C++
1374 MitesPerCellW = (
1375 WkrEmerge.mites.get_total() / WkrEmerge.number
1376 if WkrEmerge.number > 0
1377 else 0.0
1378 )
1379 MitesPerCellD = (
1380 DrnEmerge.mites.get_total() / DrnEmerge.number
1381 if DrnEmerge.number > 0
1382 else 0.0
1383 )
1385 # Survivorship
1386 PropSurviveMiteW = self.m_init_cond.m_workerMiteSurvivorship / 100.0
1387 PropSurviveMiteD = self.m_init_cond.m_droneMiteSurvivorshipField / 100.0
1389 # Reproduction rates per mite per cell
1390 if MitesPerCellW <= 1.0:
1391 ReproMitePerCellW = self.m_init_cond.m_workerMiteOffspring
1392 else:
1393 ReproMitePerCellW = (1.15 * MitesPerCellW) - (
1394 0.233 * MitesPerCellW * MitesPerCellW
1395 )
1396 if ReproMitePerCellW < 0:
1397 ReproMitePerCellW = 0.0
1399 if MitesPerCellD <= 2.0:
1400 ReproMitePerCellD = self.m_init_cond.m_droneMiteOffspringField
1401 else:
1402 ReproMitePerCellD = (
1403 1.734
1404 - (0.0755 * MitesPerCellD)
1405 - (0.0069 * MitesPerCellD * MitesPerCellD)
1406 )
1407 if ReproMitePerCellD < 0:
1408 ReproMitePerCellD = 0.0
1410 PROPRUNMITE2 = 0.6
1412 SurviveMitesW = WkrEmerge.mites * PropSurviveMiteW
1413 SurviveMitesD = DrnEmerge.mites * PropSurviveMiteD
1415 NumEmergingMites = SurviveMitesW.get_total() + SurviveMitesD.get_total()
1417 NewMitesW = SurviveMitesW * ReproMitePerCellW
1418 NewMitesD = SurviveMitesD * ReproMitePerCellD
1420 # Only mites which hadn't previously infested can survive to infest again.
1421 SurviveMitesW = SurviveMitesW * WkrEmerge.get_prop_virgins()
1422 SurviveMitesD = SurviveMitesD * DrnEmerge.get_prop_virgins()
1424 NumVirgins = SurviveMitesW.get_total() + SurviveMitesD.get_total()
1426 RunMiteVirgins = self.run_mite * self.prop_rm_virgins
1427 RunMiteW = NewMitesW + (SurviveMitesW * PROPRUNMITE2)
1428 RunMiteD = NewMitesD + (SurviveMitesD * PROPRUNMITE2)
1430 # Mites dying today are the number which originally emerged from brood minus the ones that eventually became running mites
1431 self.m_mites_dying_today = (
1432 WkrEmerge.mites.get_total() + DrnEmerge.mites.get_total()
1433 )
1434 self.m_mites_dying_today = max(0.0, self.m_mites_dying_today)
1436 # Add new running mites
1437 self.run_mite = self.run_mite + RunMiteD + RunMiteW
1439 # Update proportion of virgins
1440 if self.run_mite.get_total() <= 0:
1441 self.prop_rm_virgins = 1.0
1442 else:
1443 numerator = (
1444 RunMiteVirgins.get_total()
1445 + NewMitesW.get_total()
1446 + NewMitesD.get_total()
1447 )
1448 self.prop_rm_virgins = (
1449 numerator / self.run_mite.get_total()
1450 if self.run_mite.get_total() > 0
1451 else 1.0
1452 )
1453 # Clamp
1454 self.prop_rm_virgins = min(self.prop_rm_virgins, 1.0)
1455 self.prop_rm_virgins = max(self.prop_rm_virgins, 0.0)
1457 # Kill NonResistant Running Mites if Treatment Enabled
1458 if self.m_vt_enable and hasattr(self.m_mite_treatment_info, "get_active_item"):
1459 the_date = self.get_day_num_date(day_num)
1460 the_item = None
1461 the_item = self.m_mite_treatment_info.get_active_item(the_date)
1462 has_item = the_item is not None
1463 if has_item and the_item:
1464 Quan = self.run_mite.get_total()
1465 # Only susceptible mites die; the resistant subpopulation survives, so
1466 # repeated treatments select for resistance.
1467 if hasattr(self.run_mite, "get_non_resistant") and hasattr(
1468 self.run_mite, "set_non_resistant"
1469 ):
1470 new_nonres = (
1471 self.run_mite.get_non_resistant()
1472 * (100.0 - the_item.pct_mortality)
1473 / 100.0
1474 )
1475 self.run_mite.set_non_resistant(new_nonres)
1476 self.m_mites_dying_today += Quan - self.run_mite.get_total()
1478 self.m_mites_dying_this_period += self.m_mites_dying_today
1480 def requeen_if_needed(
1481 self,
1482 sim_day_num,
1483 event,
1484 egg_laying_delay,
1485 wkr_drn_ratio,
1486 enable_requeen,
1487 scheduled,
1488 queen_strength,
1489 rq_once,
1490 requeen_date,
1491 ):
1492 """
1493 Port of CColony::ReQueenIfNeeded
1495 Two modes:
1496 - Scheduled: trigger on ReQueenDate (initial exact year match, subsequent annual matches)
1497 - Automatic: trigger when proportion of unfertilized (drone) eggs > 0.15 during Apr-Sep (months 4..9)
1499 When requeening occurs, a strength may be popped from m_RQQueenStrengthArray (if present).
1500 After requeening, egg laying is delayed by egg_laying_delay days (queen.requeen handles this).
1501 """
1502 applied_strength = queen_strength
1504 if not enable_requeen:
1505 return
1507 try:
1508 if scheduled == 0:
1509 # Scheduled re-queening:
1510 # initial: year, month, day must match
1511 # subsequent annual: year < current year and month/day match and rq_once != 0
1512 ev_time = event.get_time()
1513 try:
1514 rd_year = requeen_date.year
1515 rd_month = requeen_date.month
1516 rd_day = requeen_date.day
1517 except Exception:
1518 # If requeen_date doesn't expose year/month/day, bail out (no scheduled requeen)
1519 return
1521 if (
1522 rd_year == ev_time.year
1523 and rd_month == ev_time.month
1524 and rd_day == ev_time.day
1525 ) or (
1526 (rd_year < ev_time.year)
1527 and (rd_month == ev_time.month)
1528 and (rd_day == ev_time.day)
1529 and (rq_once != 0)
1530 ):
1531 if self.m_RQQueenStrengthArray:
1532 applied_strength = self.m_RQQueenStrengthArray.pop(0)
1533 notification = f"Scheduled Requeening Occurred, Strength {applied_strength:5.1f}"
1534 self.add_event_notification(
1535 event.get_date_stg("%m/%d/%Y"), notification
1536 )
1537 self.queen.requeen(egg_laying_delay, applied_strength, sim_day_num)
1538 else:
1539 # Automatic re-queening
1540 month = event.get_time().month
1541 if (
1542 (self.queen.get_prop_drone_eggs() > 0.15)
1543 and (month > 3)
1544 and (month < 10)
1545 ):
1546 if self.m_RQQueenStrengthArray:
1547 applied_strength = self.m_RQQueenStrengthArray.pop(0)
1548 notification = f"Automatic Requeening Occurred, Strength {applied_strength:5.1f}"
1549 self.add_event_notification(
1550 event.get_date_stg("%m/%d/%Y"), notification
1551 )
1552 self.queen.requeen(egg_laying_delay, applied_strength, sim_day_num)
1553 except Exception:
1554 # On unexpected errors, do not requeen
1555 return
1557 # def set_miticide_treatment(self, start_day_num, duration, mortality, enable):
1558 # pass
1560 # def set_miticide_treatment_from_treatments(self, treatments, enable):
1561 # pass
1563 def set_spore_treatment(self, start_day_num, enable):
1564 # Port of CColony::SetSporeTreatment
1565 if enable:
1566 self.m_SPStart = start_day_num
1567 self.m_SPEnable = True
1568 else:
1569 self.m_SPEnable = False
1570 self.m_SPTreatmentActive = False
1572 def remove_drone_comb(self, pct):
1573 # Port of CColony::RemoveDroneComb
1574 # Simulates the removal of drone comb. The variable pct is the amount to be removed
1575 # possible bug: when multiplying by the percentages, likely need to divide by 100 to convert to fraction
1576 if pct > 100:
1577 pct = 100.0
1578 if pct < 0:
1579 pct = 0.0
1581 # Apply to drone eggs
1582 for egg in getattr(self.deggs, "bees", []):
1583 if hasattr(egg, "number"):
1584 egg.number *= int(
1585 100.0 - pct
1586 ) # should this be *= (100.0 - pct) / 100.0 ?
1588 # Apply to drone larvae
1589 for larva in getattr(self.dlarv, "bees", []):
1590 if hasattr(larva, "number"):
1591 larva.number *= int(
1592 100.0 - pct
1593 ) # should this be *= (100.0 - pct) / 100.0 ?
1595 # Apply to drone capped brood
1596 for brood in getattr(self.capdrn, "bees", []):
1597 if hasattr(brood, "number"):
1598 brood.number *= int(
1599 100.0 - pct
1600 ) # should this be *= (100.0 - pct) / 100.0 ?
1601 if hasattr(brood, "mites"):
1602 # brood.m_Mites = brood.m_Mites * (100.0 - pct);
1603 if hasattr(brood.mites, "__mul__"):
1604 # Follow C++ logic: mites multiplied by floating point, not int
1605 brood.mites *= 100.0 - pct # should this be (100.0 - pct) / 100 ??
1606 if hasattr(brood, "set_prop_virgins"):
1607 brood.set_prop_virgins(0.0)
1609 def add_discrete_event(self, date_stg, event_id):
1610 # Port of CColony::AddDiscreteEvent
1611 if date_stg in self.m_event_map:
1612 # Date already exists, add a new event to the array
1613 self.m_event_map[date_stg].append(event_id)
1614 else:
1615 # Create new map element
1616 self.m_event_map[date_stg] = [event_id]
1618 def remove_discrete_event(self, date_stg, event_id):
1619 # Port of CColony::RemoveDiscreteEvent
1620 if date_stg in self.m_event_map:
1621 # Date exists
1622 event_array = self.m_event_map[date_stg]
1623 # Remove all occurrences of event_id
1624 self.m_event_map[date_stg] = [x for x in event_array if x != event_id]
1626 if len(self.m_event_map[date_stg]) == 0:
1627 del self.m_event_map[date_stg]
1629 def get_discrete_events(self, key):
1630 # Port of CColony::GetDiscreteEvents
1631 # Returns the event array for the given key, or None if not found
1632 return self.m_event_map.get(key, None)
1634 def do_pending_events(self, weather_event, current_sim_day):
1635 # Port of CColony::DoPendingEvents
1636 # DoPendingEvents is used when running WebBeePop. The predefined events from a legacy program are
1637 # mapped into VarroaPop parameters and this is executed as part of the main simulation loop. A much
1638 # simplified set of features for use by elementary school students.
1640 event_array = self.get_discrete_events(weather_event.get_date_stg("%m/%d/%Y"))
1641 if not event_array:
1642 return
1644 for event_id in event_array:
1645 # TRACE("A Discrete Event on %s\n",pWeatherEvent->GetDateStg("%m/%d/%Y"));
1646 EggLayDelay = 17
1647 Strength = 5
1649 if event_id == DE_SWARM: # Swarm
1650 self.add_event_notification(
1651 weather_event.get_date_stg("%m/%d/%Y"),
1652 "Detected SWARM Discrete Event",
1653 )
1654 if hasattr(self.foragers, "factor_quantity"):
1655 self.foragers.factor_quantity(0.75)
1656 if hasattr(self.wadl, "factor_quantity"):
1657 self.wadl.factor_quantity(0.75)
1658 if hasattr(self.dadl, "factor_quantity"):
1659 self.dadl.factor_quantity(0.75)
1661 elif event_id == DE_CHALKBROOD: # Chalk Brood
1662 # All Larvae Die
1663 self.add_event_notification(
1664 weather_event.get_date_stg("%m/%d/%Y"),
1665 "Detected CHALKBROOD Discrete Event",
1666 )
1667 if hasattr(self.dlarv, "factor_quantity"):
1668 self.dlarv.factor_quantity(0.0)
1669 if hasattr(self.wlarv, "factor_quantity"):
1670 self.wlarv.factor_quantity(0.0)
1672 elif event_id == DE_RESOURCEDEP: # Resource Depletion
1673 # Forager Lifespan = minimum
1674 self.add_event_notification(
1675 weather_event.get_date_stg("%m/%d/%Y"),
1676 "Detected RESOURCEDEPLETION Discrete Event",
1677 )
1678 self.m_init_cond.m_ForagerLifespan = 4
1680 elif event_id == DE_SUPERCEDURE: # Supercedure of Queen
1681 # New queen = 17 days before egg laying starts
1682 self.add_event_notification(
1683 weather_event.get_date_stg("%m/%d/%Y"),
1684 "Detected SUPERCEDURE Discrete Event",
1685 )
1686 if hasattr(self.queen, "requeen"):
1687 self.queen.requeen(EggLayDelay, Strength, current_sim_day)
1689 elif event_id == DE_PESTICIDE: # Death of foragers by pesticide
1690 # 25% of foragers die
1691 self.add_event_notification(
1692 weather_event.get_date_stg("%m/%d/%Y"),
1693 "Detected PESTICIDE Discrete Event",
1694 )
1695 if hasattr(self.foragers, "factor_quantity"):
1696 self.foragers.factor_quantity(0.75)
1698 def get_mites_dying_today(self):
1699 # Port of CColony::GetMitesDyingToday
1700 return self.m_mites_dying_today
1702 def get_nurse_bees(self):
1703 # Port of CColony::GetNurseBees
1704 # Number of nurse bees is defined as # larvae/2. Implication is that a nurse bee is needed for each two larvae
1705 total_larvae = self.wlarv.get_quantity() + self.dlarv.get_quantity()
1706 return total_larvae // 2
1708 def get_total_mite_count(self):
1709 # Port of CColony::GetTotalMiteCount
1710 # return ( RunMite.GetTotal() + CapDrn.GetMiteCount() + CapWkr.GetMiteCount() );
1711 run_mite_total = (
1712 self.run_mite.get_total() if hasattr(self.run_mite, "get_total") else 0
1713 )
1714 capdrn_mites = (
1715 self.capdrn.get_mite_count()
1716 if hasattr(self.capdrn, "get_mite_count")
1717 else 0
1718 )
1719 capwkr_mites = (
1720 self.capwkr.get_mite_count()
1721 if hasattr(self.capwkr, "get_mite_count")
1722 else 0
1723 )
1724 return run_mite_total + capdrn_mites + capwkr_mites
1726 def set_start_sample_period(self):
1727 # Port of CColony::SetStartSamplePeriod
1728 # Notifies CColony that it is the beginning of a sample period. Since we gather either weekly or
1729 # daily data this is used to reset accumulators.
1730 self.m_mites_dying_this_period = 0.0
1732 def get_mites_dying_this_period(self):
1733 # Port of CColony::GetMitesDyingThisPeriod
1734 return self.m_mites_dying_this_period
1736 def apply_pesticide_mortality(self):
1737 """
1738 Port of CColony::ApplyPesticideMortality
1740 Applies pesticide mortality to different bee populations based on their current doses
1741 compared to previously seen maximum doses. Updates mortality tracking variables.
1743 Constraint: Bee quantities are not reduced unless the current pesticide dose is > previous maximum dose. But,
1744 for bees just getting into Larva4 or Adult1, this is the first time they have had a dose.
1746 """
1747 # Worker Larvae 4
1748 # if (m_EPAData.m_D_L4 > m_EPAData.m_D_L4_Max) // IED - only reduce if current dose greater than previous maximum dose
1749 # {
1750 self.m_dead_worker_larvae_pesticide = self.apply_pesticide_to_bees(
1751 self.wlarv,
1752 3,
1753 3,
1754 self.m_epadata.m_D_L4,
1755 0,
1756 self.m_epadata.m_AI_LarvaLD50,
1757 self.m_epadata.m_AI_LarvaSlope,
1758 )
1759 self.m_epadata.m_D_L4_Max = max(
1760 self.m_epadata.m_D_L4_Max, self.m_epadata.m_D_L4
1761 )
1762 # }
1764 # Worker Larvae 5
1765 if self.m_epadata.m_D_L5 > self.m_epadata.m_D_L5_Max:
1766 self.m_dead_worker_larvae_pesticide += self.apply_pesticide_to_bees(
1767 self.wlarv,
1768 4,
1769 4,
1770 self.m_epadata.m_D_L5,
1771 self.m_epadata.m_D_L5_Max,
1772 self.m_epadata.m_AI_LarvaLD50,
1773 self.m_epadata.m_AI_LarvaSlope,
1774 )
1775 self.m_epadata.m_D_L5_Max = self.m_epadata.m_D_L5
1777 # Drone Larvae
1778 self.m_dead_drone_larvae_pesticide = self.apply_pesticide_to_bees(
1779 self.dlarv,
1780 3,
1781 3,
1782 self.m_epadata.m_D_LD,
1783 0,
1784 self.m_epadata.m_AI_LarvaLD50,
1785 self.m_epadata.m_AI_LarvaSlope,
1786 ) # New L4 drones
1787 if self.m_epadata.m_D_LD > self.m_epadata.m_D_LD_Max:
1788 self.m_dead_drone_larvae_pesticide += self.apply_pesticide_to_bees(
1789 self.dlarv,
1790 4,
1791 DLARVLIFE - 1,
1792 self.m_epadata.m_D_LD,
1793 self.m_epadata.m_D_LD_Max,
1794 self.m_epadata.m_AI_LarvaLD50,
1795 self.m_epadata.m_AI_LarvaSlope,
1796 )
1797 self.m_epadata.m_D_LD_Max = self.m_epadata.m_D_LD
1799 # Worker Adults 1-3
1800 self.m_dead_worker_adults_pesticide = self.apply_pesticide_to_bees(
1801 self.wadl,
1802 0,
1803 0,
1804 self.m_epadata.m_D_A13,
1805 0,
1806 self.m_epadata.m_AI_AdultLD50,
1807 self.m_epadata.m_AI_AdultSlope,
1808 ) # New adults
1809 if self.m_epadata.m_D_A13 > self.m_epadata.m_D_A13_Max:
1810 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees(
1811 self.wadl,
1812 1,
1813 2,
1814 self.m_epadata.m_D_A13,
1815 self.m_epadata.m_D_A13_Max,
1816 self.m_epadata.m_AI_AdultLD50,
1817 self.m_epadata.m_AI_AdultSlope,
1818 )
1819 self.m_epadata.m_D_A13_Max = self.m_epadata.m_D_A13
1821 # Worker Adults 4-10
1822 if self.m_epadata.m_D_A410 > self.m_epadata.m_D_A410_Max:
1823 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees(
1824 self.wadl,
1825 3,
1826 9,
1827 self.m_epadata.m_D_A410,
1828 self.m_epadata.m_D_A410_Max,
1829 self.m_epadata.m_AI_AdultLD50,
1830 self.m_epadata.m_AI_AdultSlope,
1831 )
1832 self.m_epadata.m_D_A410_Max = self.m_epadata.m_D_A410
1834 # Worker Adults 11-20
1835 if self.m_epadata.m_D_A1120 > self.m_epadata.m_D_A1120_Max:
1836 self.m_dead_worker_adults_pesticide += self.apply_pesticide_to_bees(
1837 self.wadl,
1838 10,
1839 WADLLIFE - 1,
1840 self.m_epadata.m_D_A1120,
1841 self.m_epadata.m_D_A1120_Max,
1842 self.m_epadata.m_AI_AdultLD50,
1843 self.m_epadata.m_AI_AdultSlope,
1844 )
1845 self.m_epadata.m_D_A1120_Max = self.m_epadata.m_D_A1120
1847 # Worker Drones
1848 self.m_dead_drone_adults_pesticide = self.apply_pesticide_to_bees(
1849 self.dadl,
1850 0,
1851 0,
1852 self.m_epadata.m_D_AD,
1853 0,
1854 self.m_epadata.m_AI_AdultLD50,
1855 self.m_epadata.m_AI_AdultSlope,
1856 )
1857 if self.m_epadata.m_D_AD > self.m_epadata.m_D_AD_Max:
1858 self.m_dead_drone_adults_pesticide += self.apply_pesticide_to_bees(
1859 self.dadl,
1860 1,
1861 DADLLIFE - 1,
1862 self.m_epadata.m_D_AD,
1863 self.m_epadata.m_D_AD_Max,
1864 self.m_epadata.m_AI_AdultLD50,
1865 self.m_epadata.m_AI_AdultSlope,
1866 )
1867 self.m_epadata.m_D_AD_Max = self.m_epadata.m_D_AD
1869 # Foragers - Contact Mortality
1870 self.m_dead_foragers_pesticide = self.apply_pesticide_to_bees(
1871 self.foragers,
1872 0,
1873 0,
1874 self.m_epadata.m_D_C_Foragers,
1875 0,
1876 self.m_epadata.m_AI_AdultLD50_Contact,
1877 self.m_epadata.m_AI_AdultSlope_Contact,
1878 )
1879 if self.m_epadata.m_D_C_Foragers > self.m_epadata.m_D_C_Foragers_Max:
1880 # Use get_length() method if available, otherwise assume reasonable default
1881 forager_length = getattr(self.foragers, "get_length", lambda: 21)() - 1
1882 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees(
1883 self.foragers,
1884 1,
1885 forager_length,
1886 self.m_epadata.m_D_C_Foragers,
1887 self.m_epadata.m_D_C_Foragers_Max,
1888 self.m_epadata.m_AI_AdultLD50_Contact,
1889 self.m_epadata.m_AI_AdultSlope_Contact,
1890 )
1891 self.m_epadata.m_D_C_Foragers_Max = self.m_epadata.m_D_C_Foragers
1893 # Foragers - Diet Mortality
1894 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees(
1895 self.foragers,
1896 0,
1897 0,
1898 self.m_epadata.m_D_D_Foragers,
1899 0,
1900 self.m_epadata.m_AI_AdultLD50,
1901 self.m_epadata.m_AI_AdultSlope,
1902 )
1903 if self.m_epadata.m_D_D_Foragers > self.m_epadata.m_D_D_Foragers_Max:
1904 # Use get_length() method if available, otherwise assume reasonable default
1905 forager_length = getattr(self.foragers, "get_length", lambda: 21)() - 1
1906 self.m_dead_foragers_pesticide += self.apply_pesticide_to_bees(
1907 self.foragers,
1908 1,
1909 forager_length,
1910 self.m_epadata.m_D_D_Foragers,
1911 self.m_epadata.m_D_D_Foragers_Max,
1912 self.m_epadata.m_AI_AdultLD50,
1913 self.m_epadata.m_AI_AdultSlope,
1914 )
1915 self.m_epadata.m_D_D_Foragers_Max = self.m_epadata.m_D_D_Foragers
1917 if self.m_dead_foragers_pesticide > 0:
1918 # Debug breakpoint placeholder (equivalent to int i = 0; in C++)
1919 pass
1921 # Reset the current doses to zero after mortality is applied.
1922 self.m_epadata.m_D_L4 = 0
1923 self.m_epadata.m_D_L5 = 0
1924 self.m_epadata.m_D_LD = 0
1925 self.m_epadata.m_D_A13 = 0
1926 self.m_epadata.m_D_A410 = 0
1927 self.m_epadata.m_D_A1120 = 0
1928 self.m_epadata.m_D_AD = 0
1929 self.m_epadata.m_D_C_Foragers = 0
1930 self.m_epadata.m_D_D_Foragers = 0
1932 def quantity_pesticide_to_kill(self, bee_list, current_dose, max_dose, ld50, slope):
1933 """
1934 Port of CColony::QuantityPesticideToKill
1936 This just calculates the number of bees in the list that would be killed by the pesticide and dose.
1938 Args:
1939 bee_list: The bee list to calculate mortality for
1940 current_dose: Current pesticide dose
1941 max_dose: Previously seen maximum dose
1942 ld50: Lethal dose 50 value
1943 slope: Dose-response slope parameter
1945 Returns:
1946 Number of bees that would be killed by pesticide
1947 """
1948 bee_quant = bee_list.get_quantity()
1950 # Calculate dose response for current and maximum doses
1951 redux_current = self.m_epadata.dose_response(current_dose, ld50, slope)
1952 redux_max = self.m_epadata.dose_response(max_dose, ld50, slope)
1954 # Less than max already seen - no additional mortality
1955 if redux_current <= redux_max:
1956 return 0
1958 # Calculate new bee quantity after mortality
1959 new_bee_quant = int(bee_quant * (1 - (redux_current - redux_max)))
1961 # Return the number killed by pesticide
1962 return bee_quant - new_bee_quant
1964 def apply_pesticide_to_bees(
1965 self, bee_list, from_idx, to_idx, current_dose, max_dose, ld50, slope
1966 ):
1967 """
1968 Port of CColony::ApplyPesticideToBees
1970 This calculates the number of bees to kill then reduces that number from all age groups
1971 between "from_idx" and "to_idx" in the list.
1973 Args:
1974 bee_list: The bee list to apply mortality to
1975 from_idx: Starting age index
1976 to_idx: Ending age index
1977 current_dose: Current pesticide dose
1978 max_dose: Previously seen maximum dose
1979 ld50: Lethal dose 50 value
1980 slope: Dose-response slope parameter
1982 Returns:
1983 Number of bees killed by pesticide
1984 """
1985 # Get bee quantity in the specified age range
1986 bee_quant = int(bee_list.get_quantity_at_range(from_idx, to_idx))
1987 if bee_quant <= 0:
1988 return 0
1990 # Calculate dose response for current and maximum doses
1991 redux_current = self.m_epadata.dose_response(current_dose, ld50, slope)
1992 redux_max = self.m_epadata.dose_response(max_dose, ld50, slope)
1994 # Less than max already seen - no additional mortality
1995 if redux_current <= redux_max:
1996 return 0
1998 # Calculate new bee quantity after mortality
1999 new_bee_quant = int(bee_quant * (1 - (redux_current - redux_max)))
2000 prop_redux = new_bee_quant / bee_quant if bee_quant > 0 else 0
2002 # Apply proportional reduction to the specified age range
2003 bee_list.set_quantity_at_proportional(from_idx, to_idx, prop_redux)
2005 # Return the number killed by pesticide
2006 return int(bee_quant - new_bee_quant)
2008 def determine_foliar_dose(self, day_num):
2009 """
2010 Port of CColony::DetermineFoliarDose
2012 If we are in a date range with Dose, this routine adds to the Dose rate variables.
2014 Args:
2015 day_num: The simulation day number
2016 """
2017 # Jump out if Foliar is not enabled
2018 if not self.m_epadata.m_FoliarEnabled:
2019 return
2021 # Get the current date (matches C++ COleDateTime* pDate = GetDayNumDate(DayNum))
2022 current_date = self.get_day_num_date(day_num)
2024 # In order to expose, must be after the application date and inside the forage window
2025 if (
2026 current_date >= self.m_epadata.m_FoliarAppDate
2027 and current_date >= self.m_epadata.m_FoliarForageBegin
2028 and current_date < self.m_epadata.m_FoliarForageEnd
2029 ):
2030 # Calculate days since application (matches C++ LONG DaysSinceApplication)
2031 days_since_application = (
2032 current_date - self.m_epadata.m_FoliarAppDate
2033 ).days
2035 # Foliar Dose is related to AI application rate and Contact Exposure factor
2036 # (See Kris Garber's EFED Training Insect Exposure.pptx for a summary)
2037 dose = (
2038 self.m_epadata.m_E_AppRate
2039 * self.m_epadata.m_AI_ContactFactor
2040 / 1000000.0
2041 ) # convert to Grams AI/bee
2043 # Dose reduced due to active ingredient half-life
2044 if self.m_epadata.m_AI_HalfLife > 0:
2045 import math
2047 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife
2048 dose *= math.exp(-k * days_since_application)
2050 # Adds to any diet-based exposure. Only foragers impacted.
2051 self.m_epadata.m_D_C_Foragers += dose
2053 def consume_food(self, event, day_num):
2054 """
2055 Calculate colony food consumption and pesticide exposure.
2057 Args:
2058 event: Current event with date and forage information
2059 day_num: Current day number in simulation
2060 """
2061 # Skip food consumption on day 1
2062 if day_num == 1:
2063 return
2065 # Calculate colony needs for pollen and nectar
2066 pollen_need = self.get_pollen_needs(event) # grams
2067 nectar_need = self.get_nectar_needs(event) # grams
2069 # Get incoming resources from foraging
2070 incoming_pollen = 0.0
2071 incoming_nectar = 0.0
2073 # Get incoming pesticide concentrations
2074 c_ai_p = 0.0 # Pesticide concentration in incoming pollen
2075 c_ai_n = 0.0 # Pesticide concentration in incoming nectar
2077 if event.is_forage_day():
2078 incoming_pollen = self.get_incoming_pollen_quant()
2079 incoming_nectar = self.get_incoming_nectar_quant()
2080 c_ai_p = self.get_incoming_pollen_pesticide_concentration(day_num)
2081 c_ai_n = self.get_incoming_nectar_pesticide_concentration(day_num)
2083 # Process pollen consumption
2084 c_actual_p = 0.0
2086 # First check if supplemental pollen feeding is available
2087 in_p = incoming_pollen
2088 if self.is_pollen_feeding_day(event):
2089 # C++ logic: All pollen needs are met by supplemental feeding
2090 if self.m_SuppPollen.m_CurrentAmount >= pollen_need:
2091 self.m_SuppPollen.m_CurrentAmount -= pollen_need
2092 pollen_need = 0 # All needs met by supplemental feeding
2093 c_ai_p = 0 # No pesticide in supplemental feed
2095 if in_p >= pollen_need:
2096 # Sufficient incoming pollen
2097 c_actual_p = c_ai_p
2098 # Add remaining pollen to resources
2099 remaining_pollen = in_p - pollen_need
2100 if remaining_pollen > 0:
2101 pollen_resource = ResourceItem(
2102 resource_quantity=remaining_pollen,
2103 pesticide_quantity=remaining_pollen * c_ai_p,
2104 )
2105 self.add_pollen_to_resources(pollen_resource)
2106 else:
2107 # Need to use stored pollen
2108 shortfall = pollen_need - in_p
2110 # Calculate resultant concentration [C1*Q1 + C2*Q2]/[Q1 + Q2]
2111 stored_conc = self.resources.get_pollen_pesticide_concentration()
2112 c_actual_p = ((c_ai_p * in_p) + (stored_conc * shortfall)) / (
2113 in_p + shortfall
2114 )
2116 # Check if we have enough stored pollen
2117 if self.resources.get_pollen_quantity() < shortfall:
2118 if self.m_NoResourceKillsColony:
2119 self.kill_colony()
2120 date_str = event.get_date_stg()
2121 self.add_event_notification(
2122 date_str, "Colony Died - Lack of Pollen Stores"
2123 )
2125 # Remove pollen from stores
2126 self.resources.remove_pollen(shortfall)
2128 # Process nectar consumption
2129 c_actual_n = 0.0
2131 # First check if supplemental nectar feeding is available
2132 in_n = incoming_nectar
2133 if self.is_nectar_feeding_day(event):
2134 # C++ logic: Add daily nectar amount to resources
2135 if (
2136 hasattr(self.m_SuppNectar, "m_StartingAmount")
2137 and hasattr(self.m_SuppNectar, "m_BeginDate")
2138 and hasattr(self.m_SuppNectar, "m_EndDate")
2139 and self.m_SuppNectar.m_BeginDate is not None
2140 and self.m_SuppNectar.m_EndDate is not None
2141 ):
2142 days_in_period = (
2143 self.m_SuppNectar.m_EndDate - self.m_SuppNectar.m_BeginDate
2144 ).days
2145 if days_in_period > 0:
2146 daily_nectar_amount = (
2147 self.m_SuppNectar.m_StartingAmount / days_in_period
2148 )
2149 if self.m_SuppNectar.m_CurrentAmount >= daily_nectar_amount:
2150 nectar_resource = ResourceItem(
2151 resource_quantity=daily_nectar_amount,
2152 pesticide_quantity=0.0, # No pesticide in supplemental feed
2153 )
2154 self.add_nectar_to_resources(nectar_resource)
2155 self.m_SuppNectar.m_CurrentAmount -= daily_nectar_amount
2156 c_ai_n = 0 # No pesticide in supplemental nectar
2158 if in_n >= nectar_need:
2159 # Sufficient incoming nectar
2160 c_actual_n = c_ai_n
2161 # Add remaining nectar to resources
2162 remaining_nectar = in_n - nectar_need
2163 if remaining_nectar > 0:
2164 nectar_resource = ResourceItem(
2165 resource_quantity=remaining_nectar,
2166 pesticide_quantity=remaining_nectar * c_ai_n,
2167 )
2168 self.add_nectar_to_resources(nectar_resource)
2169 else:
2170 # Need to use stored nectar
2171 shortfall = nectar_need - in_n
2173 # Calculate resultant concentration [C1*Q1 + C2*Q2]/[Q1 + Q2]
2174 stored_conc = self.resources.get_nectar_pesticide_concentration()
2175 c_actual_n = ((c_ai_n * in_n) + (stored_conc * shortfall)) / (
2176 in_n + shortfall
2177 )
2179 # Check if we have enough stored nectar
2180 if self.resources.get_nectar_quantity() < shortfall:
2181 if self.m_NoResourceKillsColony:
2182 self.kill_colony()
2183 date_str = event.get_date_stg()
2184 self.add_event_notification(
2185 date_str, "Colony Died - Lack of Nectar Stores"
2186 )
2188 # Remove nectar from stores
2189 self.resources.remove_nectar(shortfall)
2191 # Calculate diet doses for each life stage based on actual consumed concentrations
2192 # Diet dose = concentration * consumption rate / 1000 (convert mg to g)
2193 self.m_epadata.m_D_L4 = (
2194 c_actual_p * self.m_epadata.m_C_L4_Pollen / 1000.0
2195 + c_actual_n * self.m_epadata.m_C_L4_Nectar / 1000.0
2196 )
2197 self.m_epadata.m_D_L5 = (
2198 c_actual_p * self.m_epadata.m_C_L5_Pollen / 1000.0
2199 + c_actual_n * self.m_epadata.m_C_L5_Nectar / 1000.0
2200 )
2201 self.m_epadata.m_D_LD = (
2202 c_actual_p * self.m_epadata.m_C_LD_Pollen / 1000.0
2203 + c_actual_n * self.m_epadata.m_C_LD_Nectar / 1000.0
2204 )
2205 self.m_epadata.m_D_A13 = (
2206 c_actual_p * self.m_epadata.m_C_A13_Pollen / 1000.0
2207 + c_actual_n * self.m_epadata.m_C_A13_Nectar / 1000.0
2208 )
2209 self.m_epadata.m_D_A410 = (
2210 c_actual_p * self.m_epadata.m_C_A410_Pollen / 1000.0
2211 + c_actual_n * self.m_epadata.m_C_A410_Nectar / 1000.0
2212 )
2213 self.m_epadata.m_D_A1120 = (
2214 c_actual_p * self.m_epadata.m_C_A1120_Pollen / 1000.0
2215 + c_actual_n * self.m_epadata.m_C_A1120_Nectar / 1000.0
2216 )
2217 self.m_epadata.m_D_AD = (
2218 c_actual_p * self.m_epadata.m_C_AD_Pollen / 1000.0
2219 + c_actual_n * self.m_epadata.m_C_AD_Nectar / 1000.0
2220 )
2221 self.m_epadata.m_D_D_Foragers = (
2222 c_actual_p * self.m_epadata.m_C_Forager_Pollen / 1000.0
2223 + c_actual_n * self.m_epadata.m_C_Forager_Nectar / 1000.0
2224 )
2226 def get_pollen_needs(self, event):
2227 """
2228 Calculate pollen needs in grams based on colony composition and season.
2230 Args:
2231 event: Current event with date and temperature information
2233 Returns:
2234 float: Pollen needs in grams
2235 """
2236 need = 0.0
2238 if event.is_winter_day():
2239 # Winter consumption - nurse bees have different rates
2240 wadl_ag = [
2241 self.wadl.get_quantity_at_range(0, 2), # Ages 0-2
2242 self.wadl.get_quantity_at_range(3, 9), # Ages 3-9
2243 self.wadl.get_quantity_at_range(10, 19), # Ages 10-19
2244 ]
2246 consumption = [
2247 self.m_epadata.m_C_A13_Pollen / 1000.0,
2248 self.m_epadata.m_C_A410_Pollen / 1000.0,
2249 self.m_epadata.m_C_A1120_Pollen / 1000.0,
2250 ]
2252 nurse_bee_quantity = self.get_nurse_bees()
2253 moved_nurse_bees = 0
2255 # Allocate nurse bees from youngest age groups first
2256 for i in range(3):
2257 if wadl_ag[i] <= nurse_bee_quantity - moved_nurse_bees:
2258 moved_nurse_bees += wadl_ag[i]
2259 need += wadl_ag[i] * consumption[i]
2260 else:
2261 # Match C++ bug: update moved_nurse_bees first, then calculate need
2262 # This causes (nurse_bee_quantity - moved_nurse_bees) to be 0
2263 moved_nurse_bees += nurse_bee_quantity - moved_nurse_bees
2264 need += (nurse_bee_quantity - moved_nurse_bees) * consumption[i]
2266 if moved_nurse_bees >= nurse_bee_quantity:
2267 break
2269 # Non-nurse bees consume 2 mg per day
2270 non_nurse_bees = self.get_colony_size() - moved_nurse_bees
2271 need += non_nurse_bees * 0.002
2273 # Add forager need
2274 forager_need = 0.0
2275 if event.is_forage_day():
2276 forager_need = (
2277 self.foragers.get_active_quantity()
2278 * self.m_epadata.m_C_Forager_Pollen
2279 / 1000.0
2280 )
2281 forager_need += (
2282 (self.foragers.get_quantity() - self.foragers.get_active_quantity())
2283 * self.m_epadata.m_C_A1120_Pollen
2284 / 1000.0
2285 )
2286 else:
2287 forager_need = self.foragers.get_quantity() * 0.002
2289 need += forager_need # Already in grams
2290 else:
2291 # Non-winter day - calculate based on larvae and adult needs
2292 # Larvae needs
2293 l_needs = (
2294 self.wlarv.get_quantity_at(3) * self.m_epadata.m_C_L4_Pollen
2295 + self.wlarv.get_quantity_at(4) * self.m_epadata.m_C_L5_Pollen
2296 + self.dlarv.get_quantity() * self.m_epadata.m_C_LD_Pollen
2297 )
2299 # Adult needs
2300 if event.is_forage_day():
2301 a_needs = (
2302 self.wadl.get_quantity_at_range(0, 2)
2303 * self.m_epadata.m_C_A13_Pollen
2304 + self.wadl.get_quantity_at_range(3, 9)
2305 * self.m_epadata.m_C_A410_Pollen
2306 + self.wadl.get_quantity_at_range(10, 19)
2307 * self.m_epadata.m_C_A1120_Pollen
2308 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Pollen
2309 + self.foragers.get_active_quantity()
2310 * self.m_epadata.m_C_Forager_Pollen
2311 + self.foragers.get_unemployed_quantity()
2312 * self.m_epadata.m_C_A1120_Pollen
2313 )
2314 else:
2315 # All foragers consume like mature adults on non-forage days
2316 a_needs = (
2317 self.wadl.get_quantity_at_range(0, 2)
2318 * self.m_epadata.m_C_A13_Pollen
2319 + self.wadl.get_quantity_at_range(3, 9)
2320 * self.m_epadata.m_C_A410_Pollen
2321 + (
2322 self.wadl.get_quantity_at_range(10, 19)
2323 + self.foragers.get_quantity()
2324 )
2325 * self.m_epadata.m_C_A1120_Pollen
2326 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Pollen
2327 )
2329 need = (l_needs + a_needs) / 1000.0 # Convert to grams
2331 return need
2333 def get_nectar_needs(self, event):
2334 """
2335 Calculate nectar needs in grams based on colony composition and season.
2337 Args:
2338 event: Current event with date and temperature information
2340 Returns:
2341 float: Nectar needs in grams
2342 """
2343 need = 0.0
2345 if event.is_winter_day():
2346 colony_size = self.get_colony_size()
2347 if colony_size > 0:
2348 if event.get_temp() <= 8.5:
2349 # See K. Garber's Winter Failure logic
2350 need = 0.3121 * colony_size * pow(0.128 * colony_size, -0.48)
2351 else:
2352 # 8.5 < AveTemp < 18.0
2353 if event.is_forage_day():
2354 # Foragers need normal forager nutrition
2355 non_foragers = colony_size - self.foragers.get_active_quantity()
2356 need = (
2357 self.foragers.get_active_quantity()
2358 * self.m_epadata.m_C_Forager_Nectar
2359 ) / 1000.0 + 0.05419 * non_foragers * pow(
2360 0.128 * non_foragers, -0.27
2361 )
2362 else:
2363 # All bees consume at winter rates
2364 need = 0.05419 * colony_size * pow(0.128 * colony_size, -0.27)
2365 else:
2366 # Summer day
2367 # Larvae needs
2368 l_needs = (
2369 self.wlarv.get_quantity_at(3) * self.m_epadata.m_C_L4_Nectar
2370 + self.wlarv.get_quantity_at(4) * self.m_epadata.m_C_L5_Nectar
2371 + self.dlarv.get_quantity() * self.m_epadata.m_C_LD_Nectar
2372 )
2374 # Adult needs
2375 if event.is_forage_day():
2376 a_needs = (
2377 self.wadl.get_quantity_at_range(0, 2)
2378 * self.m_epadata.m_C_A13_Nectar
2379 + self.wadl.get_quantity_at_range(3, 9)
2380 * self.m_epadata.m_C_A410_Nectar
2381 + self.wadl.get_quantity_at_range(10, 19)
2382 * self.m_epadata.m_C_A1120_Nectar
2383 + self.foragers.get_unemployed_quantity()
2384 * self.m_epadata.m_C_A1120_Nectar
2385 + self.foragers.get_active_quantity()
2386 * self.m_epadata.m_C_Forager_Nectar
2387 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Nectar
2388 )
2389 else:
2390 # Foragers consume like mature adults
2391 a_needs = (
2392 self.wadl.get_quantity_at_range(0, 2)
2393 * self.m_epadata.m_C_A13_Nectar
2394 + self.wadl.get_quantity_at_range(3, 9)
2395 * self.m_epadata.m_C_A410_Nectar
2396 + (
2397 self.wadl.get_quantity_at_range(10, 19)
2398 + self.foragers.get_quantity()
2399 )
2400 * self.m_epadata.m_C_A1120_Nectar
2401 + self.dadl.get_quantity() * self.m_epadata.m_C_AD_Nectar
2402 )
2404 need = (l_needs + a_needs) / 1000.0 # Convert to grams
2406 return need
2408 def get_incoming_pollen_quant(self):
2409 """
2410 Calculate incoming pollen quantity in grams from foraging.
2412 Returns:
2413 float: Incoming pollen in grams
2414 """
2415 pollen = 0.0
2416 # Only bring in pollen if there are larvae
2417 if (self.wlarv.get_quantity() + self.dlarv.get_quantity()) > 0:
2418 pollen = (
2419 self.foragers.get_active_quantity()
2420 * self.m_epadata.m_I_PollenTrips
2421 * self.m_epadata.m_I_PollenLoad
2422 / 1000.0
2423 )
2424 return pollen
2426 def get_incoming_nectar_quant(self):
2427 """
2428 Calculate incoming nectar quantity in grams from foraging.
2430 Returns:
2431 float: Incoming nectar in grams
2432 """
2433 nectar = (
2434 self.foragers.get_active_quantity()
2435 * self.m_epadata.m_I_NectarTrips
2436 * self.m_epadata.m_I_NectarLoad
2437 / 1000.0
2438 )
2440 # If there are no larvae, all pollen foraging trips become nectar trips
2441 if (self.wlarv.get_quantity() + self.dlarv.get_quantity()) <= 0:
2442 nectar += (
2443 self.foragers.get_active_quantity()
2444 * self.m_epadata.m_I_PollenTrips
2445 * self.m_epadata.m_I_NectarLoad
2446 / 1000.0
2447 )
2449 return nectar
2451 def get_incoming_pollen_pesticide_concentration(self, day_num):
2452 """
2453 Calculate incoming pollen pesticide concentration accounting for decay.
2455 Args:
2456 day_num: Current day number in simulation
2458 Returns:
2459 float: Pesticide concentration in grams AI per gram pollen
2460 """
2461 incoming_concentration = 0.0
2462 cur_date = self.get_day_num_date(day_num)
2464 # Check if using nutrient contamination table
2465 if self.nutrient_ct.is_enabled():
2466 nectar_conc, pollen_conc = self.nutrient_ct.get_contaminant_conc(cur_date)
2467 incoming_concentration = pollen_conc
2468 else:
2469 # Normal foliar spray process
2470 if (
2471 self.m_epadata.m_FoliarEnabled
2472 and cur_date >= self.m_epadata.m_FoliarAppDate
2473 and cur_date >= self.m_epadata.m_FoliarForageBegin
2474 and cur_date < self.m_epadata.m_FoliarForageEnd
2475 ):
2476 # Base concentration from foliar spray
2477 incoming_concentration = 110.0 * self.m_epadata.m_E_AppRate / 1000000.0
2478 # Apply decay due to active ingredient half-life
2479 days_since_application = (
2480 cur_date - self.m_epadata.m_FoliarAppDate
2481 ).days
2482 if self.m_epadata.m_AI_HalfLife > 0:
2483 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife
2484 incoming_concentration *= math.exp(-k * days_since_application)
2486 self.add_event_notification(
2487 cur_date.strftime("%m/%d/%Y"),
2488 "Incoming Foliar Spray Pollen Pesticide",
2489 )
2491 # Seed treatment exposure
2492 if (
2493 cur_date >= self.m_epadata.m_SeedForageBegin
2494 and cur_date < self.m_epadata.m_SeedForageEnd
2495 and self.m_epadata.m_SeedEnabled
2496 ):
2497 incoming_concentration += (
2498 self.m_epadata.m_E_SeedAppRate * 18e-9
2499 ) # 18 ng ai/g per (mg ai/seed)
2500 self.add_event_notification(
2501 cur_date.strftime("%m/%d/%Y"), "Incoming Seed Pollen Pesticide"
2502 )
2504 # Soil contamination exposure
2505 if (
2506 cur_date >= self.m_epadata.m_SoilForageBegin
2507 and cur_date < self.m_epadata.m_SoilForageEnd
2508 and self.m_epadata.m_SoilEnabled
2509 ):
2510 if self.m_epadata.m_AI_KOW > 0 or self.m_epadata.m_E_SoilTheta != 0:
2511 log_kow = math.log10(self.m_epadata.m_AI_KOW)
2512 tscf = -0.0648 * (log_kow * log_kow) + 0.241 * log_kow + 0.5822
2513 soil_conc = (
2514 tscf
2515 * (pow(10, (0.95 * log_kow - 2.05)) + 0.82)
2516 * self.m_epadata.m_E_SoilConcentration
2517 * (
2518 self.m_epadata.m_E_SoilP
2519 / (
2520 self.m_epadata.m_E_SoilTheta
2521 + self.m_epadata.m_E_SoilP
2522 * self.m_epadata.m_AI_KOC
2523 * self.m_epadata.m_E_SoilFoc
2524 )
2525 )
2526 )
2527 incoming_concentration += soil_conc / 1000000.0
2528 self.add_event_notification(
2529 cur_date.strftime("%m/%d/%Y"), "Incoming Soil Pollen Pesticide"
2530 )
2532 return incoming_concentration
2534 def get_incoming_nectar_pesticide_concentration(self, day_num):
2535 """
2536 Calculate incoming nectar pesticide concentration accounting for decay.
2538 Args:
2539 day_num: Current day number in simulation
2541 Returns:
2542 float: Pesticide concentration in grams AI per gram nectar
2543 """
2544 incoming_concentration = 0.0
2545 cur_date = self.get_day_num_date(day_num)
2547 # Check if using nutrient contamination table
2548 if self.nutrient_ct.is_enabled():
2549 nectar_conc, pollen_conc = self.nutrient_ct.get_contaminant_conc(cur_date)
2550 incoming_concentration = nectar_conc
2551 else:
2552 # Normal foliar spray process
2553 if (
2554 self.m_epadata.m_FoliarEnabled
2555 and cur_date >= self.m_epadata.m_FoliarAppDate
2556 and cur_date >= self.m_epadata.m_FoliarForageBegin
2557 and cur_date < self.m_epadata.m_FoliarForageEnd
2558 ):
2559 # Base concentration from foliar spray
2560 incoming_concentration = 110.0 * self.m_epadata.m_E_AppRate / 1000000.0
2562 # Apply decay due to active ingredient half-life
2563 days_since_application = (
2564 cur_date - self.m_epadata.m_FoliarAppDate
2565 ).days
2566 if self.m_epadata.m_AI_HalfLife > 0:
2567 k = math.log(2.0) / self.m_epadata.m_AI_HalfLife
2568 incoming_concentration *= math.exp(-k * days_since_application)
2570 self.add_event_notification(
2571 cur_date.strftime("%m/%d/%Y"),
2572 "Incoming Foliar Spray Nectar Pesticide",
2573 )
2575 # Seed treatment exposure
2576 if (
2577 cur_date >= self.m_epadata.m_SeedForageBegin
2578 and cur_date < self.m_epadata.m_SeedForageEnd
2579 and self.m_epadata.m_SeedEnabled
2580 ):
2581 incoming_concentration += (
2582 self.m_epadata.m_E_SeedAppRate * 45e-9
2583 ) # 45 ng ai/g per (mg ai/seed)
2584 self.add_event_notification(
2585 cur_date.strftime("%m/%d/%Y"), "Incoming Seed Nectar Pesticide"
2586 )
2588 # Soil contamination exposure
2589 if (
2590 cur_date >= self.m_epadata.m_SoilForageBegin
2591 and cur_date < self.m_epadata.m_SoilForageEnd
2592 and self.m_epadata.m_SoilEnabled
2593 ):
2594 if self.m_epadata.m_AI_KOW > 0 or self.m_epadata.m_E_SoilTheta != 0:
2595 log_kow = math.log10(self.m_epadata.m_AI_KOW)
2596 tscf = -0.0648 * (log_kow * log_kow) + 0.241 * log_kow + 0.5822
2597 soil_conc = (
2598 tscf
2599 * (pow(10, (0.95 * log_kow - 2.05)) + 0.82)
2600 * self.m_epadata.m_E_SoilConcentration
2601 * (
2602 self.m_epadata.m_E_SoilP
2603 / (
2604 self.m_epadata.m_E_SoilTheta
2605 + self.m_epadata.m_E_SoilP
2606 * self.m_epadata.m_AI_KOC
2607 * self.m_epadata.m_E_SoilFoc
2608 )
2609 )
2610 )
2611 incoming_concentration += soil_conc / 1000000.0
2612 self.add_event_notification(
2613 cur_date.strftime("%m/%d/%Y"), "Incoming Soil Nectar Pesticide"
2614 )
2616 return incoming_concentration
2618 def is_pollen_feeding_day(self, event):
2619 """
2620 Check if supplemental pollen feeding should occur.
2622 Args:
2623 event: Current event with date information
2625 Returns:
2626 bool: True if pollen feeding should occur
2627 """
2628 feeding_day = False
2630 if self.m_SuppPollenEnabled and self.get_colony_size() > 100:
2631 if self.m_SuppPollenAnnual:
2632 # Annual feeding - check within year
2633 test_begin = event.get_time().replace(
2634 month=self.m_SuppPollen.m_BeginDate.month,
2635 day=self.m_SuppPollen.m_BeginDate.day,
2636 )
2637 test_end = event.get_time().replace(
2638 month=self.m_SuppPollen.m_EndDate.month,
2639 day=self.m_SuppPollen.m_EndDate.day,
2640 )
2642 feeding_day = (
2643 self.m_SuppPollen.m_CurrentAmount > 0.0
2644 and test_begin < event.get_time()
2645 and test_end >= event.get_time()
2646 )
2647 else:
2648 # Specific date range
2649 feeding_day = (
2650 self.m_SuppPollen.m_CurrentAmount > 0.0
2651 and self.m_SuppPollen.m_BeginDate < event.get_time()
2652 and self.m_SuppPollen.m_EndDate >= event.get_time()
2653 )
2655 return feeding_day
2657 def is_nectar_feeding_day(self, event):
2658 """
2659 Check if supplemental nectar feeding should occur.
2661 Args:
2662 event: Current event with date information
2664 Returns:
2665 bool: True if nectar feeding should occur
2666 """
2667 feeding_day = False
2669 if self.m_SuppNectarEnabled and self.get_colony_size() > 100:
2670 if self.m_SuppNectarAnnual:
2671 # Annual feeding - check within year
2672 test_begin = event.get_time().replace(
2673 month=self.m_SuppNectar.m_BeginDate.month,
2674 day=self.m_SuppNectar.m_BeginDate.day,
2675 )
2676 test_end = event.get_time().replace(
2677 month=self.m_SuppNectar.m_EndDate.month,
2678 day=self.m_SuppNectar.m_EndDate.day,
2679 )
2680 feeding_day = (
2681 self.m_SuppNectar.m_CurrentAmount > 0.0
2682 and test_begin < event.get_time()
2683 and test_end >= event.get_time()
2684 )
2685 else:
2686 # Specific date range
2687 feeding_day = (
2688 self.m_SuppNectar.m_CurrentAmount > 0.0
2689 and self.m_SuppNectar.m_BeginDate < event.get_time()
2690 and self.m_SuppNectar.m_EndDate >= event.get_time()
2691 )
2693 return feeding_day
2695 def add_pollen_to_resources(self, resource):
2696 """
2697 Add pollen to colony resources with storage limits.
2699 Args:
2700 resource: ResourceItem object with resource_quantity and pesticide_quantity
2701 """
2702 if self.m_ColonyPolMaxAmount <= 0:
2703 self.m_ColonyPolMaxAmount = 5000 # Default max
2705 prop_full = self.resources.get_pollen_quantity() / self.m_ColonyPolMaxAmount
2706 reduction = 1 - prop_full
2708 if prop_full > 0.9:
2709 resource.resource_quantity *= reduction
2710 resource.pesticide_quantity *= reduction
2712 self.resources.add_pollen(resource)
2714 def add_nectar_to_resources(self, resource):
2715 """
2716 Add nectar to colony resources with storage limits.
2718 Args:
2719 resource: ResourceItem object with resource_quantity and pesticide_quantity
2720 """
2721 if self.m_ColonyNecMaxAmount <= 0:
2722 self.m_ColonyNecMaxAmount = 5000 # Default max
2724 prop_full = self.resources.get_nectar_quantity() / self.m_ColonyNecMaxAmount
2725 reduction = 1 - prop_full
2727 reduction = max(reduction, 0) # Don't exceed max value
2729 if prop_full > 0.9:
2730 resource.resource_quantity *= reduction
2731 resource.pesticide_quantity *= reduction
2733 self.resources.add_nectar(resource)
2735 def initialize_colony_resources(self):
2736 """
2737 Port of CColony::InitializeColonyResources
2739 Initialize colony resources to zero values.
2740 TODO: This should ultimately be pre-settable at the beginning of a simulation.
2741 For now, initialize everything to 0.0.
2742 """
2743 self.resources.set_pollen_quantity(0)
2744 self.resources.set_nectar_quantity(0)
2745 self.resources.set_pollen_pesticide_quantity(0)
2746 self.resources.set_nectar_pesticide_quantity(0)