Coverage for pybeepop/pybeepop.py: 88%
86 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-25 18:27 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-25 18:27 +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 verbose=False,
40 ):
41 """
42 Initialize a PyBeePop object connected to a BeePop+ shared library.
44 Args:
45 lib_file (str, optional): Path to the BeePop+ shared library (.dll or .so). If None, attempts to auto-detect based on OS and architecture.
46 parameter_file (str, optional): Path to a text file of BeePop+ parameters (one per line, parameter=value). See https://doi.org/10.3390/ecologies3030022
47 or the documentation for valid parameters.
48 weather_file (str, optional): Path to a .csv or comma-separated .txt file containing weather data, where each row denotes:
49 Date (MM/DD/YY), Max Temp (C), Min Temp (C), Avg Temp (C), Windspeed (m/s), Rainfall (mm), Hours of daylight (optional).
50 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),
51 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").
52 verbose (bool, optional): If True, print additional debugging statements. Defaults to False.
54 Raises:
55 FileNotFoundError: If a provided file does not exist at the specified path.
56 NotImplementedError: If run on a platform that is not 64-bit Windows or Linux.
57 """
59 self.parent = os.path.dirname(os.path.abspath(__file__))
60 self.platform = platform.system()
61 self.verbose = verbose
62 if (
63 lib_file is None
64 ): # detect OS and architecture and use pre-compiled BeePop+ if possible
65 if self.platform == "Windows":
66 if platform.architecture()[0] == "32bit":
67 raise NotImplementedError(
68 "Windows x86 (32-bit) is not supported by BeePop+. Please run on an x64 platform."
69 )
70 else:
71 lib_file = os.path.join(self.parent, "lib/beepop_win64.dll")
72 elif self.platform == "Linux":
73 lib_file = os.path.join(self.parent, "lib/beepop_linux.so")
74 if self.verbose:
75 print(
76 """
77 Running in Linux mode. Trying manylinux/musllinux version.
78 If you encounter errors, you may need to compile your own version of BeePop+ from source and pass the path to your
79 .so file with the lib_file option. Currently, only 64-bit architecture is supported.
80 See the pybeepop README for instructions.
81 """
82 )
83 else:
84 raise NotImplementedError("BeePop+ only supports Windows and Linux.")
85 if not os.path.isfile(lib_file):
86 raise FileNotFoundError(
87 """
88 BeePop+ shared object library does not exist or is not compatible with your operating system.
89 You may need to compile BeePop+ from source (see https://github.com/USEPA/pybeepop/blob/main/README.md for more info.)
90 Currently, only 64-bit architecture is supported.
91 """
92 )
93 self.lib_file = lib_file
94 self.beepop = BeePopModel(self.lib_file, verbose=self.verbose)
95 self.parameters = None
96 if parameter_file is not None:
97 self.load_parameter_file(self.parameter_file)
98 else:
99 self.parameter_file = None
100 if weather_file is not None:
101 self.load_weather(weather_file)
102 else:
103 self.weather_file = None
104 if residue_file is not None:
105 self.load_residue_file(self.residue_file)
106 else:
107 self.residue_file = None
108 # self.new_features = new_features # not being used?
109 self.output = None
111 def set_parameters(self, parameters):
112 """
113 Set BeePop+ parameters based on a dictionary {parameter: value}.
115 Args:
116 parameters (dict): Dictionary of BeePop+ parameters {parameter: value}. See https://doi.org/10.3390/ecologies3030022 or the documentation for valid parameters.
118 Raises:
119 TypeError: If parameters is not a dict.
120 ValueError: If a parameter is not a valid BeePop+ parameter.
121 """
122 if (parameters is not None) and (not isinstance(parameters, dict)):
123 raise TypeError(
124 "parameters must be a named dictionary of BeePop+ parameters"
125 )
126 self.parameters = self.beepop.set_parameters(parameters)
128 def get_parameters(self):
129 """
130 Return all parameters that have been set by the user.
132 Returns:
133 dict: Dictionary of current BeePop+ parameters.
134 """
135 return self.beepop.get_parameters()
137 def load_weather(self, weather_file):
138 """
139 Load a weather file. The file should be a .csv or comma-delimited .txt file where each row denotes:
140 Date (MM/DD/YY), Max Temp (C), Min Temp (C), Avg Temp (C), Windspeed (m/s), Rainfall (mm), Hours of daylight (optional).
142 Args:
143 weather_file (str): Path to the weather file (csv or txt). See docs/weather_readme.txt and manuscript for format details.
145 Raises:
146 FileNotFoundError: If the provided file does not exist at the specified path.
147 """
148 if not os.path.isfile(weather_file):
149 raise FileNotFoundError(
150 "Weather file does not exist at path: {}!".format(weather_file)
151 )
152 self.weather_file = weather_file
153 self.beepop.load_weather(self.weather_file)
155 def load_parameter_file(self, parameter_file):
156 """
157 Load a .txt file of parameter values to set. Each row of the file is a string with the format 'parameter=value'.
159 Args:
160 parameter_file (str): Path to a txt file of BeePop+ parameters. See https://doi.org/10.3390/ecologies3030022 or the documentation for valid parameters.
162 Raises:
163 FileNotFoundError: If the provided file does not exist at the specified path.
164 ValueError: If a listed parameter is not a valid BeePop+ parameter.
165 """
166 if not os.path.isfile(parameter_file):
167 raise FileNotFoundError(
168 "Paramter file does not exist at path: {}!".format(parameter_file)
169 )
170 self.parameter_file = parameter_file
171 self.beepop.load_input_file(self.parameter_file)
173 def load_residue_file(self, residue_file):
174 """
175 Load a .csv or comma-delimited .txt file of pesticide residues in pollen/nectar. Each row should specify Date (MM/DD/YYYY),
176 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").
178 Args:
179 residue_file (str): Path to the residue .csv or .txt file. See docs/residue_file_readme.txt and manuscript for format details.
181 Raises:
182 FileNotFoundError: If the provided file does not exist at the specified path.
183 """
184 if not os.path.isfile(residue_file):
185 raise FileNotFoundError(
186 "Residue file does not exist at path: {}!".format(residue_file)
187 )
188 self.residue_file = residue_file
189 self.beepop.load_contam_file(self.residue_file)
191 def run_model(self):
192 """
193 Run the BeePop+ model simulation.
195 Raises:
196 RuntimeError: If the weather file has not yet been set.
198 Returns:
199 pandas.DataFrame: DataFrame of daily time series results for the BeePop+ run, including colony size, adult workers, brood, eggs, and other metrics.
200 """
201 # check to see if parameters have been supplied
202 if (self.parameter_file is None) and (self.parameters is None):
203 print("No parameters have been set. Running with defualt settings.")
204 if self.weather_file is None:
205 raise RuntimeError("Weather must be set before running BeePop+!")
206 self.output = self.beepop.run_beepop()
207 return self.output
209 def get_output(self, format="DataFrame"):
210 """
211 Get the output from the last BeePop+ run.
213 Args:
214 format (str, optional): Return results as DataFrame ('DataFrame') or JSON string ('json'). Defaults to 'DataFrame'.
216 Raises:
217 RuntimeError: If there is no output because run_model has not yet been called.
219 Returns:
220 pandas.DataFrame or str: DataFrame or JSON string of the model results. JSON output is a dictionary of lists keyed by column name.
221 """
222 if self.output is None:
223 raise RuntimeError(
224 "There are no results to plot. Please run the model first."
225 )
226 if format == "json":
227 result = json.dumps(self.output.to_dict(orient="list"))
228 else:
229 result = self.output
230 return result
232 def plot_output(
233 self,
234 columns=[
235 "Colony Size",
236 "Adult Workers",
237 "Capped Worker Brood",
238 "Worker Larvae",
239 "Worker Eggs",
240 ],
241 ):
242 """
243 Plot the output as a time series.
245 Args:
246 columns (list, optional): List of column names to plot (as strings). Defaults to key colony metrics.
248 Raises:
249 RuntimeError: If there is no output because run_model has not yet been called.
250 IndexError: If any column name is not a valid output column.
252 Returns:
253 matplotlib.axes.Axes: Matplotlib Axes object for further customization.
254 """
255 if self.output is None:
256 raise RuntimeError(
257 "There are no results to plot. Please run the model first."
258 )
259 invalid_cols = [col not in self.output.columns for col in columns]
260 if any(invalid_cols):
261 raise IndexError(
262 "The column name {} is not a valid output column.".format(
263 [i for (i, v) in zip(columns, invalid_cols) if v]
264 )
265 )
266 plot = plot_timeseries(output=self.output, columns=columns)
267 return plot
269 def get_error_log(self):
270 """
271 Return the BeePop+ session error log as a string for debugging. Useful for troubleshooting.
273 Returns:
274 str: Error log from the BeePop+ session.
275 """
276 return self.beepop.get_errors()
278 def get_info_log(self):
279 """
280 Return the BeePop+ session info log as a string for debugging..
282 Returns:
283 str: Info log from the BeePop+ session.
284 """
285 return self.beepop.get_info()
287 def version(self):
288 """
289 Return the BeePop+ version as a string.
291 Returns:
292 str: BeePop+ version string.
293 """
294 version = self.beepop.get_version()
295 return version
297 def exit(self):
298 """
299 Close the connection to the BeePop+ shared library and clean up resources.
300 """
301 self.beepop.close_library()
302 del self.beepop
303 return