Coverage for pybeepop/pybeepop.py: 88%
107 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 14:01 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 14:01 +0000
1"""
2pybeepop - BeePop+ interface for Python
3"""
5import os
6import platform
7import pandas as pd
8from .tools import BeePopModel
9from .plots import plot_timeseries
10import json
13class PyBeePop:
14 """
15 Python interface for the BeePop+ honey bee colony simulation model.
17 BeePop+ is a mechanistic model for simulating honey bee colony dynamics, designed for ecological risk assessment and research applications.
18 This interface enables programmatic access to BeePop+ from Python, supporting batch simulations, sensitivity analysis, and integration with
19 data analysis workflows.
21 For scientific background, model structure, and example applications, see:
22 Garber et al. (2022), "Simulating the Effects of Pesticides on Honey Bee (Apis mellifera L.) Colonies with BeePop+", Ecologies.
23 Minucci et al. (2025), "pybeepop: A Python interface for the BeePop+ honey bee colony model," Journal of Open Research Software.
25 Example usage:
26 >>> from pybeepop.pybeepop import PyBeePop
27 >>> model = PyBeePop(parameter_file='params.txt', weather_file='weather.csv', residue_file='residues.csv')
28 >>> model.run_model()
29 >>> results = model.get_output()
30 >>> model.plot_output()
31 """
33 def __init__(
34 self,
35 lib_file=None,
36 parameter_file=None,
37 weather_file=None,
38 residue_file=None,
39 latitude=30.0,
40 verbose=False,
41 ):
42 """
43 Initialize a PyBeePop object connected to a BeePop+ shared library.
45 Args:
46 lib_file (str, optional): Path to the BeePop+ shared library (.dll or .so). If None, attempts to auto-detect based on OS and architecture.
47 parameter_file (str, optional): Path to a text file of BeePop+ parameters (one per line, parameter=value). See https://doi.org/10.3390/ecologies3030022
48 or the documentation for valid parameters.
49 weather_file (str, optional): Path to a .csv or comma-separated .txt file containing weather data, where each row denotes:
50 Date (MM/DD/YY), Max Temp (C), Min Temp (C), Avg Temp (C), Windspeed (m/s), Rainfall (mm), Hours of daylight (optional).
51 residue_file (str, optional): Path to a .csv or comma-separated .txt file containing pesticide residue data. Each row should specify Date (MM/DD/YYYY),
52 Concentration in nectar (g A.I. / g), Concentration in pollen (g A.I. / g). Values can be in scientific notation (e.g., "9.00E-08").
53 latitude (float, optional): Latitude in decimal degrees for daylight hour calculations (-90 to 90). Defaults to 30.0.
54 verbose (bool, optional): If True, print additional debugging statements. Defaults to False.
56 Raises:
57 FileNotFoundError: If a provided file does not exist at the specified path.
58 NotImplementedError: If run on a platform that is not 64-bit Windows or Linux.
59 ValueError: If latitude is outside the valid range.
60 """
62 self.parent = os.path.dirname(os.path.abspath(__file__))
63 self.platform = platform.system()
64 self.verbose = verbose
65 if (
66 lib_file is None
67 ): # detect OS and architecture and use pre-compiled BeePop+ if possible
68 if self.platform == "Windows":
69 if platform.architecture()[0] == "32bit":
70 raise NotImplementedError(
71 "Windows x86 (32-bit) is not supported by BeePop+. Please run on an x64 platform."
72 )
73 else:
74 lib_file = os.path.join(self.parent, "lib/beepop_win64.dll")
75 elif self.platform == "Linux":
76 lib_file = os.path.join(self.parent, "lib/beepop_linux.so")
77 if self.verbose:
78 print(
79 """
80 Running in Linux mode. Trying manylinux/musllinux version.
81 If you encounter errors, you may need to compile your own version of BeePop+ from source and pass the path to your
82 .so file with the lib_file option. Currently, only 64-bit architecture is supported.
83 See the pybeepop README for instructions.
84 """
85 )
86 else:
87 raise NotImplementedError("BeePop+ only supports Windows and Linux.")
88 if not os.path.isfile(lib_file):
89 raise FileNotFoundError(
90 """
91 BeePop+ shared object library does not exist or is not compatible with your operating system.
92 You may need to compile BeePop+ from source (see https://github.com/USEPA/pybeepop/blob/main/README.md for more info.)
93 Currently, only 64-bit architecture is supported.
94 """
95 )
96 self.lib_file = lib_file
97 self.beepop = BeePopModel(self.lib_file, verbose=self.verbose)
98 # Reset latitude to avoid inheritance from previous instances
99 # Validate and set the provided latitude
100 if not -90 <= latitude <= 90:
101 raise ValueError("Latitude must be between -90 and 90 degrees")
102 self.current_latitude = latitude
103 self.beepop.set_latitude(self.current_latitude)
104 self.parameters = None
105 if parameter_file is not None:
106 self.load_parameter_file(self.parameter_file)
107 else:
108 self.parameter_file = None
109 if weather_file is not None:
110 self.load_weather(weather_file)
111 else:
112 self.weather_file = None
113 if residue_file is not None:
114 self.load_residue_file(self.residue_file)
115 else:
116 self.residue_file = None
117 # self.new_features = new_features # not being used?
118 self.output = None
120 def set_parameters(self, parameters):
121 """
122 Set BeePop+ parameters based on a dictionary {parameter: value}.
124 Args:
125 parameters (dict): Dictionary of BeePop+ parameters {parameter: value}. See https://doi.org/10.3390/ecologies3030022 or the documentation for valid parameters.
127 Raises:
128 TypeError: If parameters is not a dict.
129 ValueError: If a parameter is not a valid BeePop+ parameter.
130 """
131 if (parameters is not None) and (not isinstance(parameters, dict)):
132 raise TypeError(
133 "parameters must be a named dictionary of BeePop+ parameters"
134 )
135 self.parameters = self.beepop.set_parameters(parameters)
137 def get_parameters(self):
138 """
139 Return all parameters that have been set by the user.
141 Returns:
142 dict: Dictionary of current BeePop+ parameters.
143 """
144 return self.beepop.get_parameters()
146 def set_latitude(self, latitude):
147 """
148 Set the latitude for daylight hour calculations.
150 Args:
151 latitude (float): Latitude in decimal degrees (-90 to 90). Positive values are North, negative are South.
153 Raises:
154 ValueError: If latitude is outside the valid range.
155 """
156 if not -90 <= latitude <= 90:
157 raise ValueError("Latitude must be between -90 and 90 degrees")
158 self.current_latitude = latitude
159 self.beepop.set_latitude(latitude)
161 def get_latitude(self):
162 """
163 Get the currently set latitude.
165 Returns:
166 float: Current latitude in decimal degrees.
167 """
168 return self.current_latitude
170 def set_simulation_dates(self, start_date, end_date):
171 """
172 Convenience method to set simulation start and end dates. The dates can
173 also be set directly as SimStart/SimEnd using the set_parameters() or
174 load_parameters() methods.
176 Args:
177 start_date (str): Simulation start date in MM/DD/YYYY format.
178 end_date (str): Simulation end date in MM/DD/YYYY format.
179 """
180 date_params = {"SimStart": start_date, "SimEnd": end_date}
181 self.set_parameters(date_params)
183 if self.verbose:
184 print(f"Set simulation dates: {start_date} to {end_date}")
186 def load_weather(self, weather_file):
187 """
188 Load a weather file. The file should be a .csv or comma-delimited .txt file where each row denotes:
189 Date (MM/DD/YYYY), Max Temp (C), Min Temp (C), Avg Temp (C), Windspeed (m/s), Rainfall (mm), Hours of daylight (optional).
191 Note: Loading weather may reset simulation dates (SimStart/SimEnd) to the weather file's date range.
192 Any previously set parameters will be automatically re-applied after weather loading.
194 Args:
195 weather_file (str): Path to the weather file (csv or txt). See docs/weather_readme.txt and manuscript for format details.
197 Raises:
198 FileNotFoundError: If the provided file does not exist at the specified path.
199 """
200 if not os.path.isfile(weather_file):
201 raise FileNotFoundError(
202 "Weather file does not exist at path: {}!".format(weather_file)
203 )
204 self.weather_file = weather_file
206 # Load weather - the underlying BeePopModel.load_weather() will automatically
207 # re-apply any previously set parameters after loading
208 self.beepop.load_weather(self.weather_file)
210 def load_parameter_file(self, parameter_file):
211 """
212 Load a .txt file of parameter values to set. Each row of the file is a string with the format 'parameter=value'.
214 Args:
215 parameter_file (str): Path to a txt file of BeePop+ parameters. See https://doi.org/10.3390/ecologies3030022 or the documentation for valid parameters.
217 Raises:
218 FileNotFoundError: If the provided file does not exist at the specified path.
219 ValueError: If a listed parameter is not a valid BeePop+ parameter.
220 """
221 if not os.path.isfile(parameter_file):
222 raise FileNotFoundError(
223 "Paramter file does not exist at path: {}!".format(parameter_file)
224 )
225 self.parameter_file = parameter_file
226 self.beepop.load_input_file(self.parameter_file)
228 def load_residue_file(self, residue_file):
229 """
230 Load a .csv or comma-delimited .txt file of pesticide residues in pollen/nectar. Each row should specify Date (MM/DD/YYYY),
231 Concentration in nectar (g A.I. / g), Concentration in pollen (g A.I. / g). Values can be in scientific notation (e.g., "9.00E-08").
233 Args:
234 residue_file (str): Path to the residue .csv or .txt file. See docs/residue_file_readme.txt and manuscript for format details.
236 Raises:
237 FileNotFoundError: If the provided file does not exist at the specified path.
238 """
239 if not os.path.isfile(residue_file):
240 raise FileNotFoundError(
241 "Residue file does not exist at path: {}!".format(residue_file)
242 )
243 self.residue_file = residue_file
244 self.beepop.load_contam_file(self.residue_file)
246 def run_model(self):
247 """
248 Run the BeePop+ model simulation.
250 Raises:
251 RuntimeError: If the weather file has not yet been set.
253 Returns:
254 pandas.DataFrame: DataFrame of daily time series results for the BeePop+ run, including colony size, adult workers, brood, eggs, and other metrics.
255 """
256 # check to see if parameters have been supplied
257 if (self.parameter_file is None) and (self.parameters is None):
258 print("No parameters have been set. Running with defualt settings.")
259 if self.weather_file is None:
260 raise RuntimeError("Weather must be set before running BeePop+!")
261 self.output = self.beepop.run_beepop()
262 return self.output
264 def get_output(self, format="DataFrame"):
265 """
266 Get the output from the last BeePop+ run.
268 Args:
269 format (str, optional): Return results as DataFrame ('DataFrame') or JSON string ('json'). Defaults to 'DataFrame'.
271 Raises:
272 RuntimeError: If there is no output because run_model has not yet been called.
274 Returns:
275 pandas.DataFrame or str: DataFrame or JSON string of the model results. JSON output is a dictionary of lists keyed by column name.
276 """
277 if self.output is None:
278 raise RuntimeError(
279 "There are no results to plot. Please run the model first."
280 )
281 if format == "json":
282 result = json.dumps(self.output.to_dict(orient="list"))
283 else:
284 result = self.output
285 return result
287 def plot_output(
288 self,
289 columns=[
290 "Colony Size",
291 "Adult Workers",
292 "Capped Worker Brood",
293 "Worker Larvae",
294 "Worker Eggs",
295 ],
296 ):
297 """
298 Plot the output as a time series.
300 Args:
301 columns (list, optional): List of column names to plot (as strings). Defaults to key colony metrics.
303 Raises:
304 RuntimeError: If there is no output because run_model has not yet been called.
305 IndexError: If any column name is not a valid output column.
307 Returns:
308 matplotlib.axes.Axes: Matplotlib Axes object for further customization.
309 """
310 if self.output is None:
311 raise RuntimeError(
312 "There are no results to plot. Please run the model first."
313 )
314 invalid_cols = [col not in self.output.columns for col in columns]
315 if any(invalid_cols):
316 raise IndexError(
317 "The column name {} is not a valid output column.".format(
318 [i for (i, v) in zip(columns, invalid_cols) if v]
319 )
320 )
321 plot = plot_timeseries(output=self.output, columns=columns)
322 return plot
324 def get_error_log(self):
325 """
326 Return the BeePop+ session error log as a string for debugging. Useful for troubleshooting.
328 Returns:
329 str: Error log from the BeePop+ session.
330 """
331 return self.beepop.get_errors()
333 def get_info_log(self):
334 """
335 Return the BeePop+ session info log as a string for debugging..
337 Returns:
338 str: Info log from the BeePop+ session.
339 """
340 return self.beepop.get_info()
342 def version(self):
343 """
344 Return the BeePop+ version as a string.
346 Returns:
347 str: BeePop+ version string.
348 """
349 version = self.beepop.get_version()
350 return version
352 def exit(self):
353 """
354 Close the connection to the BeePop+ shared library and clean up resources.
355 """
356 if hasattr(self, "beepop") and self.beepop is not None:
357 if hasattr(self.beepop, "lib") and self.beepop.lib is not None:
358 # Clear any remaining buffers
359 try:
360 self.beepop.clear_buffers()
361 self.beepop.close_library()
362 except:
363 pass # Ignore errors during cleanup
364 self.beepop = None
366 def __del__(self):
367 """Destructor to ensure cleanup when object is garbage collected."""
368 self.exit()