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

1"""BeePop+ Python Interface Module. 

2 

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. 

6 

7The module is designed to mirror the C++ API while leveraging Python's strengths for 

8data analysis and scientific computing. 

9 

10Architecture Overview: 

11 The BeePop+ system follows a hierarchical architecture: 

12 

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) 

24 

25Main Classes: 

26 BeePop: Primary interface class for simulation management 

27 SNCElement: Nutrient contamination data element 

28 

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 

36 

37Example: 

38 Basic simulation setup and execution: 

39 

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 

46 

47Version: 

48 1/15/2025 - Python port of BeePop+ C++ library 

49 

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""" 

55 

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) 

68 

69BEEPOP_VERSION = "1/15/2025" 

70 

71 

72class BeePop: 

73 """Main interface class for BeePop+ bee colony simulation. 

74 

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. 

79 

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. 

83 

84 Attributes: 

85 session (VarroaPopSession): The underlying simulation session that manages 

86 all colony dynamics, weather events, and simulation state. 

87 

88 Example: 

89 Basic simulation workflow: 

90 

91 >>> # Initialize simulation 

92 >>> beepop = BeePop() 

93 >>> beepop.set_latitude(39.5) # Set geographic location 

94 >>> beepop.initialize_model() # Initialize colony model 

95 

96 >>> # Configure simulation parameters 

97 >>> params = ["ICWorkerAdults=25000", "SimStart=06/01/2024"] 

98 >>> beepop.set_ic_variables_v(params) 

99 

100 >>> # Load environmental data 

101 >>> beepop.set_weather_from_file("weather_data.txt") 

102 

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") 

108 

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 """ 

113 

114 def __init__(self): 

115 """Initialize a new BeePop+ simulation session. 

116 

117 Creates a new simulation session with default settings, initializing 

118 the underlying VarroaPopSession and setting up the colony and weather 

119 management systems. 

120 

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() 

127 

128 def _initialize_session(self): 

129 """Set up the session with default colony and weather objects. 

130 

131 Internal method that ensures the session has properly initialized 

132 Colony and WeatherEvents objects. This method is called automatically 

133 during BeePop initialization. 

134 

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() 

141 

142 # Create weather if not present 

143 if not hasattr(self.session, "weather") or self.session.weather is None: 

144 self.session.weather = WeatherEvents() 

145 

146 # Helper methods for session access 

147 def _get_colony(self): 

148 """Get the colony object.""" 

149 return getattr(self.session, "colony", None) 

150 

151 def _get_weather(self): 

152 """Get the weather object.""" 

153 return getattr(self.session, "weather", None) 

154 

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() 

159 

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() 

164 

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) 

169 

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) 

174 

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 

180 

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 

186 

187 # Core Interface Methods 

188 

189 def initialize_model(self) -> bool: 

190 """Initialize the colony simulation model. 

191 

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. 

195 

196 This must be called before running a simulation to ensure the colony 

197 is properly initialized with default or previously set parameters. 

198 

199 Returns: 

200 bool: True if initialization successful, False if any errors occurred. 

201 

202 Example: 

203 >>> beepop = BeePop() 

204 >>> if beepop.initialize_model(): 

205 ... print("Model initialized successfully") 

206 ... else: 

207 ... print("Model initialization failed") 

208 

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 

223 

224 def clear_results_buffer(self) -> bool: 

225 """ 

226 Port of ClearResultsBuffer(). 

227 Clear the results text buffer. 

228 

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 

243 

244 def set_latitude(self, lat: float) -> bool: 

245 """Set the geographic latitude for the simulation. 

246 

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. 

250 

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. 

255 

256 Returns: 

257 bool: True if latitude was set successfully, False otherwise. 

258 

259 Example: 

260 >>> beepop = BeePop() 

261 >>> beepop.set_latitude(40.7) # New York City latitude 

262 >>> beepop.set_latitude(-33.9) # Sydney, Australia latitude 

263 

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 

274 

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 

286 

287 def get_latitude(self) -> Tuple[bool, float]: 

288 """ 

289 Port of GetLatitude(). 

290 Get the current simulation latitude. 

291 

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 

303 

304 # Parameter File Loading Methods 

305 

306 def load_parameter_file(self, file_path: str) -> bool: 

307 """ 

308 Load parameters from a text file containing key=value pairs. 

309 

310 This functionality is not present in the C++ library - it's added 

311 for compatibility with PyBeePop's parameter file loading capability. 

312 

313 The file should contain lines in the format: 

314 ParameterName=ParameterValue 

315 

316 Args: 

317 file_path: Path to the parameter file 

318 

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() 

325 

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) 

333 

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 

340 

341 # Use existing set_ic_variables_v method to set all parameters 

342 success = self.set_ic_variables_v(parameter_pairs, reset_ics=False) 

343 

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 ) 

354 

355 return success 

356 

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 

367 

368 # Initial Conditions Methods 

369 

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. 

374 

375 Args: 

376 name: Parameter name 

377 value: Parameter value as string 

378 

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 

392 

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 

409 

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. 

414 

415 Args: 

416 nv_pairs: List of "name=value" strings 

417 reset_ics: Whether to reset initial conditions first 

418 

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() 

440 

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() 

445 

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 

462 

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 

470 

471 # Weather Methods 

472 

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. 

477 

478 Args: 

479 weather_event_string: Weather string in format: "Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours" 

480 

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) 

487 

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 

508 

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. 

513 

514 Args: 

515 weather_event_string_list: List of weather strings 

516 

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 

526 

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 

540 

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 

546 

547 def clear_weather(self) -> bool: 

548 """ 

549 Port of ClearWeather(). 

550 Clear all weather events. 

551 

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 

562 

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. 

566 

567 Args: 

568 file_path: Path to the weather file 

569 delimiter: Field delimiter (auto-detected if None). Common values: ',' or '\t' 

570 

571 Returns: 

572 bool: True if successful 

573 

574 The file should contain weather data in the format: 

575 Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours 

576 

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() 

584 

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 

589 

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 

603 

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 

610 

611 try: 

612 # Split by delimiter and clean up whitespace 

613 fields = [field.strip() for field in line.split(delimiter)] 

614 

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 

621 

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] 

625 

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) 

629 

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 

636 

637 # Use set_weather_v to process all weather strings 

638 return self.set_weather_v(weather_strings) 

639 

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 

648 

649 # Contamination Table Methods 

650 

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. 

655 

656 Args: 

657 contamination_table_list: List of "Date,NectarConc,PollenConc" strings 

658 

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() 

668 

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 

674 

675 # Enable contamination table if any items were added successfully 

676 if success: 

677 colony.m_nutrient_ct.nutrient_cont_enabled = True 

678 

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 

686 

687 def clear_contamination_table(self) -> bool: 

688 """ 

689 Port of ClearContaminationTable(). 

690 Clear the contamination table. 

691 

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 

702 

703 # Error and Info Reporting Methods 

704 

705 def enable_error_reporting(self, enable: bool) -> bool: 

706 """ 

707 Port of EnableErrorReporting(). 

708 Enable or disable error reporting. 

709 

710 Args: 

711 enable: True to enable, False to disable 

712 

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 

722 

723 def enable_info_reporting(self, enable: bool) -> bool: 

724 """ 

725 Port of EnableInfoReporting(). 

726 Enable or disable info reporting. 

727 

728 Args: 

729 enable: True to enable, False to disable 

730 

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 

739 

740 def get_error_list(self) -> Tuple[bool, List[str]]: 

741 """ 

742 Port of GetErrorList(). 

743 Get the list of error messages. 

744 

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, [] 

756 

757 def clear_error_list(self) -> bool: 

758 """ 

759 Port of ClearErrorList(). 

760 Clear the error list. 

761 

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 

770 

771 def get_info_list(self) -> Tuple[bool, List[str]]: 

772 """ 

773 Port of GetInfoList(). 

774 Get the list of info messages. 

775 

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, [] 

787 

788 def clear_info_list(self) -> bool: 

789 """ 

790 Port of ClearInfoList(). 

791 Clear the info list. 

792 

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 

801 

802 # Simulation Methods 

803 

804 def run_simulation(self) -> bool: 

805 """ 

806 Port of RunSimulation(). 

807 Initialize and run the simulation. 

808 

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 

821 

822 def get_results(self) -> Tuple[bool, List[str]]: 

823 """ 

824 Port of GetResults(). 

825 Get the simulation results. 

826 

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, [] 

838 

839 def get_results_dataframe(self) -> Tuple[bool, Optional[pd.DataFrame]]: 

840 """ 

841 Get simulation results as a pandas DataFrame with PyBeePop-compatible formatting. 

842 

843 This method mimics the PyBeePop package's result formatting, providing 

844 the same 44 columns with proper column names and data types. 

845 

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 

856 

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 ] 

904 

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 

909 

910 out_str = io.StringIO("\n".join(out_lines)) 

911 

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)) 

942 

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 ) 

951 

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") 

961 

962 return True, out_df 

963 

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 ) 

970 

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) 

985 

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 

993 

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 

998 

999 # Version Methods 

1000 

1001 def get_lib_version(self) -> Tuple[bool, str]: 

1002 """ 

1003 Port of GetLibVersion(). 

1004 Get the library version string. 

1005 

1006 Returns: 

1007 Tuple[bool, str]: (success, version) 

1008 """ 

1009 try: 

1010 return True, BEEPOP_VERSION 

1011 except Exception: 

1012 return False, "" 

1013 

1014 # Helper Methods 

1015 

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. 

1022 

1023 The string format is: Date,MaxTemp,MinTemp,AvgTemp,Windspeed,Rainfall,DaylightHours 

1024 Space or comma delimited. 

1025 

1026 Args: 

1027 weather_string: Weather data string 

1028 calc_daylight_by_lat: Whether to calculate daylight by latitude 

1029 

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()) 

1036 

1037 if len(tokens) < 6: 

1038 return None 

1039 

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 

1048 

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 

1058 

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 )() 

1089 

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 

1105 

1106 return event 

1107 

1108 except Exception: 

1109 return None 

1110 

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. 

1115 

1116 Nutrient String Format is: Date,Nectar,Pollen 

1117 

1118 Args: 

1119 element_string: Nutrient contamination string 

1120 

1121 Returns: 

1122 SNCElement object or None if parsing failed 

1123 """ 

1124 try: 

1125 tokens = re.split(r"[,\s]+", element_string.strip()) 

1126 

1127 if len(tokens) < 3: 

1128 return None 

1129 

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 

1138 

1139 # Parse concentrations 

1140 try: 

1141 nectar_conc = float(tokens[1]) 

1142 pollen_conc = float(tokens[2]) 

1143 except (ValueError, IndexError): 

1144 return None 

1145 

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 )() 

1164 

1165 return element 

1166 

1167 except Exception: 

1168 return None