Coverage for pybeepop/beepop/beepop.py: 54%
455 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 13:34 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 13:34 +0000
1"""BeePop+ Python Interface Module.
3This module provides the main Python interface to the BeePop+ bee colony simulation system.
4It serves as a port of the C++ VPopLib library, offering equivalent functionality for
5modeling honey bee colony dynamics, population growth, and environmental interactions.
7The module is designed to mirror the C++ API while leveraging Python's strengths for
8data analysis and scientific computing.
10Architecture Overview:
11 The BeePop+ system follows a hierarchical architecture:
13 BeePop (Main Interface)
14 └── VarroaPopSession (Simulation Manager)
15 ├── Colony (Core Colony Simulation)
16 │ ├── Queen (Egg laying and colony leadership)
17 │ ├── Adult Bees (Workers, Drones, Foragers)
18 │ ├── Brood (Developing bees)
19 │ ├── Larvae (Larval stage bees)
20 │ └── Eggs (Egg stage)
21 ├── WeatherEvents (Environmental conditions)
22 ├── MiteTreatments (Varroa mite management)
23 └── NutrientContaminationTable (Pesticide/toxin tracking)
25Main Classes:
26 BeePop: Primary interface class for simulation management
27 SNCElement: Nutrient contamination data element
29Key Features:
30 - Complete bee lifecycle simulation (egg → larva → pupa → adult)
31 - Varroa mite population dynamics and treatment modeling
32 - Weather-driven foraging and development
33 - Pesticide exposure and mortality tracking
34 - Resource management (pollen, nectar)
35 - Queen replacement events
37Example:
38 Basic simulation setup and execution:
40 >>> beepop = BeePop()
41 >>> beepop.set_latitude(40.0) # Set location
42 >>> beepop.initialize_model() # Initialize colony
43 >>> beepop.set_weather_from_file("weather.txt") # Load weather
44 >>> beepop.run_simulation() # Execute simulation
45 >>> success, results = beepop.get_results() # Get output
47Version:
48 1/15/2025 - Python port of BeePop+ C++ library
50Notes:
51 This Python implementation maintains compatibility with the original C++
52 VPopLib API while adding Python-specific enhancements for data handling
53 and analysis integration.
54"""
56from datetime import datetime
57import re
58import io
59import pandas as pd
60from typing import List, Tuple, Optional, Any
61from pybeepop.beepop.session import VarroaPopSession
62from pybeepop.beepop.colony import Colony
63from pybeepop.beepop.weatherevents import Event, WeatherEvents
64from pybeepop.beepop.nutrientcontaminationtable import (
65 SNCElement,
66 NutrientContaminationTable,
67)
69BEEPOP_VERSION = "1/15/2025"
72class BeePop:
73 """Main interface class for BeePop+ bee colony simulation.
75 This class provides the primary Python interface to the BeePop+ simulation system,
76 equivalent to the C++ VPopLib interface. It manages the underlying simulation
77 session, colony dynamics, weather conditions, and provides methods for
78 configuration, execution, and results retrieval.
80 The BeePop class acts as an interface for high-level interaction with the complex
81 underlying bee colony simulation system, handling session management,
82 parameter validation, and data formatting.
84 Attributes:
85 session (VarroaPopSession): The underlying simulation session that manages
86 all colony dynamics, weather events, and simulation state.
88 Example:
89 Basic simulation workflow:
91 >>> # Initialize simulation
92 >>> beepop = BeePop()
93 >>> beepop.set_latitude(39.5) # Set geographic location
94 >>> beepop.initialize_model() # Initialize colony model
96 >>> # Configure simulation parameters
97 >>> params = ["ICWorkerAdults=25000", "SimStart=06/01/2024"]
98 >>> beepop.set_ic_variables_v(params)
100 >>> # Load environmental data
101 >>> beepop.set_weather_from_file("weather_data.txt")
103 >>> # Execute simulation
104 >>> success = beepop.run_simulation()
105 >>> if success:
106 ... success, results = beepop.get_results()
107 ... print(f"Simulation completed with {len(results)} data points")
109 Note:
110 This class is designed to be thread-safe for individual instances,
111 but multiple BeePop instances should not share session data.
112 """
114 def __init__(self):
115 """Initialize a new BeePop+ simulation session.
117 Creates a new simulation session with default settings, initializing
118 the underlying VarroaPopSession and setting up the colony and weather
119 management systems.
121 Raises:
122 RuntimeError: If session initialization fails due to system constraints
123 or missing dependencies.
124 """
125 self.session = VarroaPopSession()
126 self._initialize_session()
128 def _initialize_session(self):
129 """Set up the session with default colony and weather objects.
131 Internal method that ensures the session has properly initialized
132 Colony and WeatherEvents objects. This method is called automatically
133 during BeePop initialization.
135 Note:
136 This is an internal method and should not be called directly by users.
137 """
138 # Create colony if not present
139 if not hasattr(self.session, "colony") or self.session.colony is None:
140 self.session.colony = Colony()
142 # Create weather if not present
143 if not hasattr(self.session, "weather") or self.session.weather is None:
144 self.session.weather = WeatherEvents()
146 # Helper methods for session access
147 def _get_colony(self):
148 """Get the colony object."""
149 return getattr(self.session, "colony", None)
151 def _get_weather(self):
152 """Get the weather object."""
153 return getattr(self.session, "weather", None)
155 def _clear_error_list(self):
156 """Clear the error list."""
157 if hasattr(self.session, "clear_error_list"):
158 self.session.clear_error_list()
160 def _clear_info_list(self):
161 """Clear the info list."""
162 if hasattr(self.session, "clear_info_list"):
163 self.session.clear_info_list()
165 def _add_to_error_list(self, msg: str):
166 """Add message to error list."""
167 if hasattr(self.session, "add_to_error_list"):
168 self.session.add_to_error_list(msg)
170 def _add_to_info_list(self, msg: str):
171 """Add message to info list."""
172 if hasattr(self.session, "add_to_info_list"):
173 self.session.add_to_info_list(msg)
175 def _is_error_reporting_enabled(self) -> bool:
176 """Check if error reporting is enabled."""
177 if hasattr(self.session, "is_error_reporting_enabled"):
178 return self.session.is_error_reporting_enabled()
179 return True
181 def _is_info_reporting_enabled(self) -> bool:
182 """Check if info reporting is enabled."""
183 if hasattr(self.session, "is_info_reporting_enabled"):
184 return self.session.is_info_reporting_enabled()
185 return True
187 # Core Interface Methods
189 def initialize_model(self) -> bool:
190 """Initialize the colony simulation model.
192 Port of the C++ InitializeModel() function. This method sets up the
193 colony for simulation by calling the colony's create() method and
194 clearing any existing error or info messages.
196 This must be called before running a simulation to ensure the colony
197 is properly initialized with default or previously set parameters.
199 Returns:
200 bool: True if initialization successful, False if any errors occurred.
202 Example:
203 >>> beepop = BeePop()
204 >>> if beepop.initialize_model():
205 ... print("Model initialized successfully")
206 ... else:
207 ... print("Model initialization failed")
209 Note:
210 After calling this method, the colony will be reset to initial
211 conditions. Any previous simulation state will be lost.
212 """
213 try:
214 colony = self._get_colony()
215 if colony and hasattr(colony, "create"):
216 colony.create()
217 self._clear_error_list()
218 self._clear_info_list()
219 return True
220 except Exception as e:
221 self._add_to_error_list(f"Failed to initialize model: {str(e)}")
222 return False
224 def clear_results_buffer(self) -> bool:
225 """
226 Port of ClearResultsBuffer().
227 Clear the results text buffer.
229 Returns:
230 bool: True if successful
231 """
232 try:
233 if hasattr(self.session, "clear_results"):
234 self.session.clear_results()
235 elif hasattr(self.session, "results_text"):
236 if hasattr(self.session.results_text, "clear"):
237 self.session.results_text.clear()
238 elif hasattr(self.session.results_text, "RemoveAll"):
239 self.session.results_text.RemoveAll()
240 return True
241 except Exception:
242 return False
244 def set_latitude(self, lat: float) -> bool:
245 """Set the geographic latitude for the simulation.
247 Port of the C++ SetLatitude() function. The latitude affects daylight
248 hours calculation, which influences bee foraging behavior, brood
249 development rates, and overall colony dynamics.
251 Args:
252 lat (float): Latitude in decimal degrees. Positive values for
253 northern hemisphere, negative for southern hemisphere.
254 Valid range: -90.0 to +90.0 degrees.
256 Returns:
257 bool: True if latitude was set successfully, False otherwise.
259 Example:
260 >>> beepop = BeePop()
261 >>> beepop.set_latitude(40.7) # New York City latitude
262 >>> beepop.set_latitude(-33.9) # Sydney, Australia latitude
264 Note:
265 If weather data is already loaded, daylight hours will be
266 automatically recalculated for the new latitude. This ensures
267 consistency between geographic location and environmental conditions.
268 """
269 try:
270 if hasattr(self.session, "set_latitude"):
271 self.session.set_latitude(lat)
272 else:
273 self.session.latitude = lat
275 # Update daylight hours for existing weather events if weather is loaded
276 if (
277 hasattr(self.session, "is_weather_loaded")
278 and self.session.is_weather_loaded()
279 ):
280 weather = self._get_weather()
281 if weather and hasattr(weather, "update_daylight_for_latitude"):
282 weather.update_daylight_for_latitude(lat)
283 return True
284 except Exception:
285 return False
287 def get_latitude(self) -> Tuple[bool, float]:
288 """
289 Port of GetLatitude().
290 Get the current simulation latitude.
292 Returns:
293 Tuple[bool, float]: (success, latitude)
294 """
295 try:
296 if hasattr(self.session, "get_latitude"):
297 lat = self.session.get_latitude()
298 else:
299 lat = getattr(self.session, "latitude", 0.0)
300 return True, lat
301 except Exception:
302 return False, 0.0
304 # Parameter File Loading Methods
306 def load_parameter_file(self, file_path: str) -> bool:
307 """
308 Load parameters from a text file containing key=value pairs.
310 This functionality is not present in the C++ library - it's added
311 for compatibility with PyBeePop's parameter file loading capability.
313 The file should contain lines in the format:
314 ParameterName=ParameterValue
316 Args:
317 file_path: Path to the parameter file
319 Returns:
320 bool: True if file was loaded and parameters set successfully
321 """
322 try:
323 with open(file_path, "r") as f:
324 lines = f.readlines()
326 # Parse lines into key=value pairs (matching PyBeePop implementation)
327 parameter_pairs = []
328 for line in lines:
329 # Remove whitespace and newlines, skip empty lines and comments
330 clean_line = line.replace(" ", "").replace("\n", "").strip()
331 if clean_line and not clean_line.startswith("#") and "=" in clean_line:
332 parameter_pairs.append(clean_line)
334 if not parameter_pairs:
335 if self._is_error_reporting_enabled():
336 self._add_to_error_list(
337 f"No valid parameters found in file: {file_path}"
338 )
339 return False
341 # Use existing set_ic_variables_v method to set all parameters
342 success = self.set_ic_variables_v(parameter_pairs, reset_ics=False)
344 if success:
345 if self._is_info_reporting_enabled():
346 self._add_to_info_list(
347 f"Loaded {len(parameter_pairs)} parameters from {file_path}"
348 )
349 else:
350 if self._is_error_reporting_enabled():
351 self._add_to_error_list(
352 f"Failed to load parameters from {file_path}"
353 )
355 return success
357 except FileNotFoundError:
358 if self._is_error_reporting_enabled():
359 self._add_to_error_list(f"Parameter file not found: {file_path}")
360 return False
361 except Exception as e:
362 if self._is_error_reporting_enabled():
363 self._add_to_error_list(
364 f"Error loading parameter file {file_path}: {str(e)}"
365 )
366 return False
368 # Initial Conditions Methods
370 def set_ic_variables_s(self, name: str, value: str) -> bool:
371 """
372 Port of SetICVariablesS().
373 Set initial condition variable by name and value strings.
375 Args:
376 name: Parameter name
377 value: Parameter value as string
379 Returns:
380 bool: True if successful
381 """
382 try:
383 success = False
384 if hasattr(self.session, "update_colony_parameters"):
385 success = self.session.update_colony_parameters(name, value)
386 else:
387 # Fallback: try to set attribute directly on colony
388 colony = self._get_colony()
389 if colony and hasattr(colony, name):
390 setattr(colony, name, value)
391 success = True
393 if success:
394 if self._is_info_reporting_enabled():
395 self._add_to_info_list(
396 f"Setting Variables. Name = {name} Value = {value}"
397 )
398 return True
399 else:
400 if self._is_error_reporting_enabled():
401 self._add_to_error_list(f"Failed to set {name} to {value}")
402 return False
403 except Exception as e:
404 if self._is_error_reporting_enabled():
405 self._add_to_error_list(
406 f"Exception setting {name} to {value}: {str(e)}"
407 )
408 return False
410 def set_ic_variables_v(self, nv_pairs: List[str], reset_ics: bool = True) -> bool:
411 """
412 Port of SetICVariablesV().
413 Set initial condition variables from list of name=value pairs.
415 Args:
416 nv_pairs: List of "name=value" strings
417 reset_ics: Whether to reset initial conditions first
419 Returns:
420 bool: True if successful
421 """
422 try:
423 if reset_ics:
424 # Clear date range value lists before loading new ICs
425 colony = self.session.get_colony()
426 if colony and hasattr(colony, "m_init_cond"):
427 # Clear DRVs if they exist
428 for drv_name in [
429 "m_AdultLifespanDRV",
430 "m_ForagerLifespanDRV",
431 "m_EggTransitionDRV",
432 "m_BroodTransitionDRV",
433 "m_LarvaeTransitionDRV",
434 "m_AdultTransitionDRV",
435 ]:
436 if hasattr(colony.m_init_cond, drv_name):
437 drv = getattr(colony.m_init_cond, drv_name)
438 if hasattr(drv, "clear_all"):
439 drv.clear_all()
441 # Clear mite treatment info
442 if hasattr(colony, "m_mite_treatment_info"):
443 if hasattr(colony.m_mite_treatment_info, "clear_all"):
444 colony.m_mite_treatment_info.clear_all()
446 success = True
447 for pair in nv_pairs:
448 eq_pos = pair.find("=")
449 if (
450 eq_pos > 0
451 ): # Equals sign must be present with at least one character before it
452 name = pair[:eq_pos].strip()
453 value = pair[eq_pos + 1 :].strip()
454 if not self.set_ic_variables_s(name, value):
455 success = False
456 else:
457 if self.session.is_error_reporting_enabled():
458 self.session.add_to_error_list(
459 f"Invalid name=value pair: {pair}"
460 )
461 success = False
463 return success
464 except Exception as e:
465 if self.session.is_error_reporting_enabled():
466 self.session.add_to_error_list(
467 f"Exception in set_ic_variables_v: {str(e)}"
468 )
469 return False
471 # Weather Methods
473 def set_weather_s(self, weather_event_string: str) -> bool:
474 """
475 Port of SetWeatherS().
476 Set weather from a single weather event string.
478 Args:
479 weather_event_string: Weather string in format: "Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours"
481 Returns:
482 bool: True if successful
483 """
484 try:
485 weather_events = self._get_weather()
486 event = self._weather_string_to_event(weather_event_string)
488 if event and weather_events:
489 if hasattr(event, "update_forage_attribute_for_event") and hasattr(
490 self.session, "get_latitude"
491 ):
492 event.update_forage_attribute_for_event(
493 self.session.get_latitude(), getattr(event, "windspeed", 0.0)
494 )
495 if hasattr(weather_events, "add_event"):
496 weather_events.add_event(event)
497 return True
498 else:
499 if self._is_error_reporting_enabled():
500 self._add_to_error_list(
501 f"Bad Weather String Format: {weather_event_string}"
502 )
503 return False
504 except Exception as e:
505 if self._is_error_reporting_enabled():
506 self._add_to_error_list(f"Exception in set_weather_s: {str(e)}")
507 return False
509 def set_weather_v(self, weather_event_string_list: List[str]) -> bool:
510 """
511 Port of SetWeatherV().
512 Set weather from a list of weather event strings.
514 Args:
515 weather_event_string_list: List of weather strings
517 Returns:
518 bool: True if all successful
519 """
520 try:
521 success = True
522 for weather_str in weather_event_string_list:
523 if not self.set_weather_s(weather_str):
524 success = False
525 break
527 weather = self.session.get_weather()
528 if weather and weather.get_total_events() > 0:
529 # Only set simulation dates from weather if they haven't been explicitly set
530 # This preserves user-defined SimStart/SimEnd parameters
531 if (
532 not hasattr(self.session, "_sim_dates_explicitly_set")
533 or not self.session._sim_dates_explicitly_set
534 ):
535 self.session.set_sim_start(weather.get_beginning_time())
536 self.session.set_sim_end(weather.get_ending_time())
537 weather.set_initialized(True)
538 else:
539 success = False
541 return success
542 except Exception as e:
543 if self.session.is_error_reporting_enabled():
544 self.session.add_to_error_list(f"Exception in set_weather_v: {str(e)}")
545 return False
547 def clear_weather(self) -> bool:
548 """
549 Port of ClearWeather().
550 Clear all weather events.
552 Returns:
553 bool: True if successful
554 """
555 try:
556 weather = self.session.get_weather()
557 if weather:
558 weather.clear_all_events()
559 return True
560 except Exception:
561 return False
563 def set_weather_from_file(self, file_path: str, delimiter: str = None) -> bool:
564 """
565 Helper method to load weather data from a CSV or tab-separated file.
567 Args:
568 file_path: Path to the weather file
569 delimiter: Field delimiter (auto-detected if None). Common values: ',' or '\t'
571 Returns:
572 bool: True if successful
574 The file should contain weather data in the format:
575 Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours
577 Date can be in MM/DD/YYYY or MM-DD-YYYY format.
578 Temperatures should be in Celsius.
579 Windspeed in km/h, Rainfall in mm, DaylightHours as decimal hours.
580 """
581 try:
582 with open(file_path, "r", encoding="utf-8") as file:
583 lines = file.readlines()
585 if not lines:
586 if self._is_error_reporting_enabled():
587 self._add_to_error_list(f"Weather file is empty: {file_path}")
588 return False
590 # Auto-detect delimiter if not specified
591 if delimiter is None:
592 first_line = lines[0].strip()
593 if "\t" in first_line:
594 delimiter = "\t"
595 elif "," in first_line:
596 delimiter = ","
597 else:
598 if self._is_error_reporting_enabled():
599 self._add_to_error_list(
600 f"Cannot auto-detect delimiter in weather file: {file_path}"
601 )
602 return False
604 # Process each line and convert to weather event strings
605 weather_strings = []
606 for line_num, line in enumerate(lines, 1):
607 line = line.strip()
608 if not line: # Skip empty lines
609 continue
611 try:
612 # Split by delimiter and clean up whitespace
613 fields = [field.strip() for field in line.split(delimiter)]
615 if len(fields) < 6:
616 if self._is_error_reporting_enabled():
617 self._add_to_error_list(
618 f"Insufficient fields in line {line_num}: {line} (expected at least 6 fields)"
619 )
620 return False
622 # Take first 7 fields (Date, MaxTemp, MinTemp, AvgTemp, Windspeed, Rainfall, DaylightHours)
623 # If only 6 fields, we'll let the weather parsing handle the missing daylight hours
624 weather_fields = fields[:7] if len(fields) >= 7 else fields[:6]
626 # Rejoin with commas for the weather string format expected by set_weather_s
627 weather_string = ",".join(weather_fields)
628 weather_strings.append(weather_string)
630 except Exception as e:
631 if self._is_error_reporting_enabled():
632 self._add_to_error_list(
633 f"Error parsing line {line_num}: {line} - {str(e)}"
634 )
635 return False
637 # Use set_weather_v to process all weather strings
638 return self.set_weather_v(weather_strings)
640 except FileNotFoundError:
641 if self._is_error_reporting_enabled():
642 self._add_to_error_list(f"Weather file not found: {file_path}")
643 return False
644 except Exception as e:
645 if self._is_error_reporting_enabled():
646 self._add_to_error_list(f"Exception in set_weather_from_file: {str(e)}")
647 return False
649 # Contamination Table Methods
651 def set_contamination_table(self, contamination_table_list: List[str]) -> bool:
652 """
653 Port of SetContaminationTable().
654 Set contamination table from list of strings.
656 Args:
657 contamination_table_list: List of "Date,NectarConc,PollenConc" strings
659 Returns:
660 bool: True if any items were added successfully
661 """
662 try:
663 success = False
664 if len(contamination_table_list) > 0:
665 colony = self.session.get_colony()
666 if colony and hasattr(colony, "m_nutrient_ct"):
667 colony.m_nutrient_ct.remove_all()
669 for ct_record in contamination_table_list:
670 element = self._string_to_nutrient_element(ct_record)
671 if element:
672 colony.m_nutrient_ct.add_contaminant_conc(element)
673 success = True # Set true if any items are added
675 # Enable contamination table if any items were added successfully
676 if success:
677 colony.m_nutrient_ct.nutrient_cont_enabled = True
679 return success
680 except Exception as e:
681 if self.session.is_error_reporting_enabled():
682 self.session.add_to_error_list(
683 f"Exception in set_contamination_table: {str(e)}"
684 )
685 return False
687 def clear_contamination_table(self) -> bool:
688 """
689 Port of ClearContaminationTable().
690 Clear the contamination table.
692 Returns:
693 bool: True if successful
694 """
695 try:
696 colony = self.session.get_colony()
697 if colony and hasattr(colony, "m_nutrient_ct"):
698 colony.m_nutrient_ct.remove_all()
699 return True
700 except Exception:
701 return False
703 # Error and Info Reporting Methods
705 def enable_error_reporting(self, enable: bool) -> bool:
706 """
707 Port of EnableErrorReporting().
708 Enable or disable error reporting.
710 Args:
711 enable: True to enable, False to disable
713 Returns:
714 bool: True if successful
715 """
716 try:
717 self.session.enable_error_reporting(enable)
718 return True
719 except Exception as e:
720 raise e
721 # return False
723 def enable_info_reporting(self, enable: bool) -> bool:
724 """
725 Port of EnableInfoReporting().
726 Enable or disable info reporting.
728 Args:
729 enable: True to enable, False to disable
731 Returns:
732 bool: True if successful
733 """
734 try:
735 self.session.enable_info_reporting(enable)
736 return True
737 except Exception:
738 return False
740 def get_error_list(self) -> Tuple[bool, List[str]]:
741 """
742 Port of GetErrorList().
743 Get the list of error messages.
745 Returns:
746 Tuple[bool, List[str]]: (success, error_list)
747 """
748 try:
749 error_list = []
750 errors = self.session.get_error_list()
751 if errors:
752 error_list = list(errors) # Convert to Python list
753 return True, error_list
754 except Exception:
755 return False, []
757 def clear_error_list(self) -> bool:
758 """
759 Port of ClearErrorList().
760 Clear the error list.
762 Returns:
763 bool: True if successful
764 """
765 try:
766 self.session.clear_error_list()
767 return True
768 except Exception:
769 return False
771 def get_info_list(self) -> Tuple[bool, List[str]]:
772 """
773 Port of GetInfoList().
774 Get the list of info messages.
776 Returns:
777 Tuple[bool, List[str]]: (success, info_list)
778 """
779 try:
780 info_list = []
781 infos = self.session.get_info_list()
782 if infos:
783 info_list = list(infos) # Convert to Python list
784 return True, info_list
785 except Exception:
786 return False, []
788 def clear_info_list(self) -> bool:
789 """
790 Port of ClearInfoList().
791 Clear the info list.
793 Returns:
794 bool: True if successful
795 """
796 try:
797 self.session.clear_info_list()
798 return True
799 except Exception:
800 return False
802 # Simulation Methods
804 def run_simulation(self) -> bool:
805 """
806 Port of RunSimulation().
807 Initialize and run the simulation.
809 Returns:
810 bool: True if successful
811 """
812 try:
813 self.session.initialize_simulation()
814 self.session.simulate()
815 return True
816 except Exception as e:
817 if self.session.is_error_reporting_enabled():
818 self.session.add_to_error_list(f"Exception in run_simulation: {str(e)}")
819 raise e
820 # return False
822 def get_results(self) -> Tuple[bool, List[str]]:
823 """
824 Port of GetResults().
825 Get the simulation results.
827 Returns:
828 Tuple[bool, List[str]]: (success, results_list)
829 """
830 try:
831 # Access results_text directly from session
832 if hasattr(self.session, "results_text") and self.session.results_text:
833 results_list = list(self.session.results_text) # Convert to Python list
834 return True, results_list
835 return False, []
836 except Exception:
837 return False, []
839 def get_results_dataframe(self) -> Tuple[bool, Optional[pd.DataFrame]]:
840 """
841 Get simulation results as a pandas DataFrame with PyBeePop-compatible formatting.
843 This method mimics the PyBeePop package's result formatting, providing
844 the same 44 columns with proper column names and data types.
846 Returns:
847 Tuple[bool, Optional[pd.DataFrame]]: (success, dataframe)
848 Returns (True, DataFrame) on success,
849 (False, None) on failure
850 """
851 try:
852 # Get raw results
853 success, results_list = self.get_results()
854 if not success or not results_list:
855 return False, None
857 # PyBeePop-compatible column names (44 columns total)
858 colnames = [
859 "Date",
860 "Colony Size",
861 "Adult Drones",
862 "Adult Workers",
863 "Foragers",
864 "Active Foragers",
865 "Capped Drone Brood",
866 "Capped Worker Brood",
867 "Drone Larvae",
868 "Worker Larvae",
869 "Drone Eggs",
870 "Worker Eggs",
871 "Daily Eggs Laid",
872 "DD",
873 "L",
874 "N",
875 "P",
876 "dd",
877 "l",
878 "n",
879 "Free Mites",
880 "Drone Brood Mites",
881 "Worker Brood Mites",
882 "Mites/Drone Cell",
883 "Mites/Worker Cell",
884 "Mites Dying",
885 "Proportion Mites Dying",
886 "Colony Pollen (g)",
887 "Pollen Pesticide Concentration (ug/g)",
888 "Colony Nectar (g)",
889 "Nectar Pesticide Concentration (ug/g)",
890 "Dead Drone Larvae",
891 "Dead Worker Larvae",
892 "Dead Drone Adults",
893 "Dead Worker Adults",
894 "Dead Foragers",
895 "Queen Strength",
896 "Average Temperature (C)",
897 "Rain (mm)",
898 "Min Temp (C)",
899 "Max Temp (C)",
900 "Daylight hours",
901 "Forage Inc",
902 "Forage Day",
903 ]
905 # Join results into a single string (mimicking PyBeePop approach)
906 out_lines = results_list[1:] # Skip header line
907 if not out_lines:
908 return False, None
910 out_str = io.StringIO("\n".join(out_lines))
912 # Read with pandas using whitespace separator and expected column names
913 # Skip first 3 rows if they contain headers/metadata (like PyBeePop)
914 try:
915 # Try to determine if we need to skip rows by checking first few lines
916 if len(results_list) >= 4:
917 # Check if first line looks like a header
918 first_line = results_list[0].strip()
919 if any(
920 word in first_line.lower()
921 for word in ["date", "colony", "adult", "version"]
922 ):
923 # Has header, might need to skip more rows
924 skip_rows = 0
925 # Look for the actual data start
926 for i, line in enumerate(results_list):
927 if line.strip():
928 first_word = line.split()[0] if line.split() else ""
929 # Data line starts with either "Initial" or a date (numbers/slashes)
930 if first_word == "Initial" or not any(
931 char.isalpha() for char in first_word
932 ):
933 # Found line starting with "Initial" or numbers (date), this is data
934 skip_rows = i
935 break
936 out_str = io.StringIO("\n".join(results_list[skip_rows:]))
937 else:
938 # No header, use all data
939 out_str = io.StringIO("\n".join(results_list))
940 else:
941 out_str = io.StringIO("\n".join(out_lines))
943 # Read CSV with whitespace separator
944 out_df = pd.read_csv(
945 out_str,
946 sep=r"\s+", # Whitespace separator
947 names=colnames, # Use our column names
948 dtype={"Date": str}, # Keep dates as strings
949 engine="python", # Use python engine for regex separator
950 )
952 # Ensure we have the right number of columns
953 if len(out_df.columns) > len(colnames):
954 # Too many columns, take first 44
955 out_df = out_df.iloc[:, : len(colnames)]
956 out_df.columns = colnames
957 elif len(out_df.columns) < len(colnames):
958 # Too few columns, pad with NaN
959 for i in range(len(out_df.columns), len(colnames)):
960 out_df[colnames[i]] = float("nan")
962 return True, out_df
964 except Exception as parse_error:
965 # If parsing fails, try simpler approach
966 if self._is_error_reporting_enabled():
967 self._add_to_error_list(
968 f"DataFrame parsing error: {str(parse_error)}"
969 )
971 # Fallback: create DataFrame manually
972 data_rows = []
973 for line in out_lines:
974 if line.strip():
975 values = line.split()
976 if values:
977 # Pad or truncate to match expected column count
978 if len(values) < len(colnames):
979 values.extend(
980 [float("nan")] * (len(colnames) - len(values))
981 )
982 elif len(values) > len(colnames):
983 values = values[: len(colnames)]
984 data_rows.append(values)
986 if data_rows:
987 out_df = pd.DataFrame(data_rows, columns=colnames)
988 # Ensure Date column is string
989 out_df["Date"] = out_df["Date"].astype(str)
990 return True, out_df
991 else:
992 return False, None
994 except Exception as e:
995 if self._is_error_reporting_enabled():
996 self._add_to_error_list(f"Exception in get_results_dataframe: {str(e)}")
997 return False, None
999 # Version Methods
1001 def get_lib_version(self) -> Tuple[bool, str]:
1002 """
1003 Port of GetLibVersion().
1004 Get the library version string.
1006 Returns:
1007 Tuple[bool, str]: (success, version)
1008 """
1009 try:
1010 return True, BEEPOP_VERSION
1011 except Exception:
1012 return False, ""
1014 # Helper Methods
1016 def _weather_string_to_event(
1017 self, weather_string: str, calc_daylight_by_lat: bool = True
1018 ) -> Optional[Any]:
1019 """
1020 Port of WeatherStringToEvent().
1021 Convert a weather string to an Event object.
1023 The string format is: Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours
1024 Space or comma delimited.
1026 Args:
1027 weather_string: Weather data string
1028 calc_daylight_by_lat: Whether to calculate daylight by latitude
1030 Returns:
1031 Event object or None if parsing failed
1032 """
1033 try:
1034 # Split by comma or space
1035 tokens = re.split(r"[,\s]+", weather_string.strip())
1037 if len(tokens) < 6:
1038 return None
1040 # Parse date
1041 try:
1042 event_date = datetime.strptime(tokens[0], "%m/%d/%Y")
1043 except ValueError:
1044 try:
1045 event_date = datetime.strptime(tokens[0], "%m/%d/%y")
1046 except ValueError:
1047 return None
1049 # Parse numeric values
1050 try:
1051 max_temp = float(tokens[1])
1052 min_temp = float(tokens[2])
1053 avg_temp = float(tokens[3])
1054 windspeed = float(tokens[4])
1055 rainfall = float(tokens[5])
1056 except (ValueError, IndexError):
1057 return None
1059 # Create event
1060 if Event is not None:
1061 event = Event(
1062 time=event_date,
1063 temp=avg_temp,
1064 max_temp=max_temp,
1065 min_temp=min_temp,
1066 windspeed=windspeed,
1067 rainfall=rainfall,
1068 )
1069 else:
1070 # Create a minimal event object if Event class is not available
1071 event = type(
1072 "Event",
1073 (),
1074 {
1075 "time": event_date,
1076 "temp": avg_temp,
1077 "max_temp": max_temp,
1078 "min_temp": min_temp,
1079 "windspeed": windspeed,
1080 "rainfall": rainfall,
1081 "daylight_hours": 0.0,
1082 "set_daylight_hours": lambda self, dh: setattr(
1083 self, "daylight_hours", dh
1084 ),
1085 "calc_today_daylight_from_latitude": lambda self, lat: 12.0, # Default daylight
1086 "update_forage_attribute_for_event": lambda self, lat, ws: None,
1087 },
1088 )()
1090 # Set daylight hours
1091 if calc_daylight_by_lat:
1092 daylight = event.calc_today_daylight_from_latitude(
1093 self.session.get_latitude()
1094 )
1095 event.set_daylight_hours(daylight)
1096 else:
1097 if len(tokens) >= 7:
1098 try:
1099 daylight = float(tokens[6])
1100 event.set_daylight_hours(daylight)
1101 except ValueError:
1102 return None
1103 else:
1104 return None
1106 return event
1108 except Exception:
1109 return None
1111 def _string_to_nutrient_element(self, element_string: str) -> Optional[Any]:
1112 """
1113 Port of String2NutrientElement().
1114 Convert a nutrient string into an SNCElement.
1116 Nutrient String Format is: Date,Nectar,Pollen
1118 Args:
1119 element_string: Nutrient contamination string
1121 Returns:
1122 SNCElement object or None if parsing failed
1123 """
1124 try:
1125 tokens = re.split(r"[,\s]+", element_string.strip())
1127 if len(tokens) < 3:
1128 return None
1130 # Parse date
1131 try:
1132 nc_date = datetime.strptime(tokens[0], "%m/%d/%Y")
1133 except ValueError:
1134 try:
1135 nc_date = datetime.strptime(tokens[0], "%m/%d/%y")
1136 except ValueError:
1137 return None
1139 # Parse concentrations
1140 try:
1141 nectar_conc = float(tokens[1])
1142 pollen_conc = float(tokens[2])
1143 except (ValueError, IndexError):
1144 return None
1146 # Create element
1147 if SNCElement is not None:
1148 element = SNCElement(
1149 nc_date=nc_date,
1150 nc_nectar_cont=nectar_conc,
1151 nc_pollen_cont=pollen_conc,
1152 )
1153 else:
1154 # Create a minimal element object if SNCElement class is not available
1155 element = type(
1156 "SNCElement",
1157 (),
1158 {
1159 "nc_date": nc_date,
1160 "nc_nectar_cont": nectar_conc,
1161 "nc_pollen_cont": pollen_conc,
1162 },
1163 )()
1165 return element
1167 except Exception:
1168 return None