Coverage for pybeepop/pybeepop.py: 88%

86 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-25 18:27 +0000

1""" 

2pybeepop - BeePop+ interface for Python 

3""" 

4 

5import os 

6import platform 

7import pandas as pd 

8from .tools import BeePopModel 

9from .plots import plot_timeseries 

10import json 

11 

12 

13class PyBeePop: 

14 """ 

15 Python interface for the BeePop+ honey bee colony simulation model. 

16 

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. 

20 

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. 

24 

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

32 

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. 

43 

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. 

53 

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

58 

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 

110 

111 def set_parameters(self, parameters): 

112 """ 

113 Set BeePop+ parameters based on a dictionary {parameter: value}. 

114 

115 Args: 

116 parameters (dict): Dictionary of BeePop+ parameters {parameter: value}. See https://doi.org/10.3390/ecologies3030022 or the documentation for valid parameters. 

117 

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) 

127 

128 def get_parameters(self): 

129 """ 

130 Return all parameters that have been set by the user. 

131 

132 Returns: 

133 dict: Dictionary of current BeePop+ parameters. 

134 """ 

135 return self.beepop.get_parameters() 

136 

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

141 

142 Args: 

143 weather_file (str): Path to the weather file (csv or txt). See docs/weather_readme.txt and manuscript for format details. 

144 

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) 

154 

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'. 

158 

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. 

161 

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) 

172 

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

177 

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. 

180 

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) 

190 

191 def run_model(self): 

192 """ 

193 Run the BeePop+ model simulation. 

194 

195 Raises: 

196 RuntimeError: If the weather file has not yet been set. 

197 

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 

208 

209 def get_output(self, format="DataFrame"): 

210 """ 

211 Get the output from the last BeePop+ run. 

212 

213 Args: 

214 format (str, optional): Return results as DataFrame ('DataFrame') or JSON string ('json'). Defaults to 'DataFrame'. 

215 

216 Raises: 

217 RuntimeError: If there is no output because run_model has not yet been called. 

218 

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 

231 

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. 

244 

245 Args: 

246 columns (list, optional): List of column names to plot (as strings). Defaults to key colony metrics. 

247 

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. 

251 

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 

268 

269 def get_error_log(self): 

270 """ 

271 Return the BeePop+ session error log as a string for debugging. Useful for troubleshooting. 

272 

273 Returns: 

274 str: Error log from the BeePop+ session. 

275 """ 

276 return self.beepop.get_errors() 

277 

278 def get_info_log(self): 

279 """ 

280 Return the BeePop+ session info log as a string for debugging.. 

281 

282 Returns: 

283 str: Info log from the BeePop+ session. 

284 """ 

285 return self.beepop.get_info() 

286 

287 def version(self): 

288 """ 

289 Return the BeePop+ version as a string. 

290 

291 Returns: 

292 str: BeePop+ version string. 

293 """ 

294 version = self.beepop.get_version() 

295 return version 

296 

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