boris-behav-obs 9.4.1__py2.py3-none-any.whl → 9.5.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,10 +24,10 @@ Copyright 2012-2025 Olivier Friard
24
24
  import wave
25
25
  import matplotlib
26
26
 
27
- matplotlib.use("Qt5Agg")
27
+ matplotlib.use("QtAgg")
28
28
 
29
29
  import numpy as np
30
-
30
+ from scipy import signal
31
31
  from . import config as cfg
32
32
 
33
33
  from PySide6.QtWidgets import (
@@ -43,8 +43,6 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
43
43
  from matplotlib.figure import Figure
44
44
  import matplotlib.ticker as mticker
45
45
 
46
- # matplotlib.pyplot.switch_backend("Qt5Agg")
47
-
48
46
 
49
47
  class Plot_spectrogram_RT(QWidget):
50
48
  # send keypress event to mainwindow
@@ -115,7 +113,7 @@ class Plot_spectrogram_RT(QWidget):
115
113
  else:
116
114
  return False
117
115
 
118
- def get_wav_info(self, wav_file: str):
116
+ def get_wav_info(self, wav_file: str) -> tuple[np.array, int]:
119
117
  """
120
118
  read wav file and extract information
121
119
 
@@ -184,7 +182,7 @@ class Plot_spectrogram_RT(QWidget):
184
182
 
185
183
  return {"media_length": self.media_length, "frame_rate": self.frame_rate}
186
184
 
187
- def plot_spectro(self, current_time: float, force_plot: bool = False):
185
+ def plot_spectro(self, current_time: float, force_plot: bool = False) -> tuple[float, bool]:
188
186
  """
189
187
  plot sound spectrogram centered on the current time
190
188
 
@@ -200,15 +198,36 @@ class Plot_spectrogram_RT(QWidget):
200
198
 
201
199
  self.ax.clear()
202
200
 
201
+ window_type = "blackmanharris" # self.config_param.get(cfg.SPECTROGRAM_WINDOW_TYPE, cfg.SPECTROGRAM_DEFAULT_WINDOW_TYPE)
202
+ nfft = int(self.config_param.get(cfg.SPECTROGRAM_NFFT, cfg.SPECTROGRAM_DEFAULT_NFFT))
203
+ noverlap = self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP)
204
+ vmin = self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN)
205
+ vmax = self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX)
206
+
203
207
  # start
204
208
  if current_time <= self.interval / 2:
205
209
  self.ax.specgram(
206
- self.sound_info[: int((self.interval) * self.frame_rate)],
210
+ self.sound_info[: int(self.interval * self.frame_rate)],
207
211
  mode="psd",
208
- # NFFT=1024,
212
+ NFFT=nfft,
209
213
  Fs=self.frame_rate,
210
- # noverlap=900,
214
+ noverlap=noverlap,
215
+ window=signal.get_window(window_type, nfft),
216
+ # matplotlib.mlab.window_hanning
217
+ # if window_type == "hanning"
218
+ # else matplotlib.mlab.window_hamming
219
+ # if window_type == "hamming"
220
+ # else matplotlib.mlab.window_blackmanharris
221
+ # if window_type == "blackmanharris"
222
+ # else matplotlib.mlab.window_hanning,
211
223
  cmap=self.spectro_color_map,
224
+ vmin=vmin,
225
+ vmax=vmax,
226
+ # mode="psd",
227
+ ## NFFT=1024,
228
+ # Fs=self.frame_rate,
229
+ ## noverlap=900,
230
+ # cmap=self.spectro_color_map,
212
231
  )
213
232
 
214
233
  self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
@@ -222,10 +241,25 @@ class Plot_spectrogram_RT(QWidget):
222
241
  self.ax.specgram(
223
242
  self.sound_info[i:],
224
243
  mode="psd",
225
- # NFFT=1024,
244
+ NFFT=nfft,
226
245
  Fs=self.frame_rate,
227
- # noverlap=900,
246
+ noverlap=noverlap,
247
+ window=signal.get_window(window_type, nfft),
248
+ # matplotlib.mlab.window_hanning
249
+ # if window_type == "hanning"
250
+ # else matplotlib.mlab.window_hamming
251
+ # if window_type == "hamming"
252
+ # else matplotlib.mlab.window_blackmanharris
253
+ # if window_type == "blackmanharris"
254
+ # else matplotlib.mlab.window_hanning,
228
255
  cmap=self.spectro_color_map,
256
+ vmin=vmin,
257
+ vmax=vmax,
258
+ # mode="psd",
259
+ ## NFFT=1024,
260
+ # Fs=self.frame_rate,
261
+ ## noverlap=900,
262
+ # cmap=self.spectro_color_map,
229
263
  )
230
264
 
231
265
  lim1 = current_time - (self.media_length - self.interval / 2)
@@ -248,10 +282,25 @@ class Plot_spectrogram_RT(QWidget):
248
282
  )
249
283
  ],
250
284
  mode="psd",
251
- # NFFT=1024,
285
+ NFFT=nfft,
252
286
  Fs=self.frame_rate,
253
- # noverlap=900,
287
+ noverlap=noverlap,
288
+ window=signal.get_window(window_type, nfft),
289
+ # matplotlib.mlab.window_hanning
290
+ # if window_type == "hanning"
291
+ # else matplotlib.mlab.window_hamming
292
+ # if window_type == "hamming"
293
+ # else matplotlib.mlab.window_blackmanharris
294
+ # if window_type == "blackmanharris"
295
+ # else matplotlib.mlab.window_hanning,
254
296
  cmap=self.spectro_color_map,
297
+ vmin=vmin,
298
+ vmax=vmax,
299
+ # mode="psd",
300
+ ## NFFT=1024,
301
+ # Fs=self.frame_rate,
302
+ ## noverlap=900,
303
+ # cmap=self.spectro_color_map,
255
304
  )
256
305
 
257
306
  self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
boris/plot_waveform_rt.py CHANGED
@@ -25,7 +25,7 @@ import wave
25
25
  from . import config as cfg
26
26
  import matplotlib
27
27
 
28
- matplotlib.use("Qt5Agg")
28
+ matplotlib.use("QtAgg")
29
29
 
30
30
  import numpy as np
31
31
  from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
boris/plugins.py CHANGED
@@ -47,12 +47,12 @@ def add_plugins_to_menu(self):
47
47
  self.menu_plugins.addAction(action)
48
48
 
49
49
 
50
- def get_plugin_name(plugin_path: str):
50
+ def get_plugin_name(plugin_path: str) -> str | None:
51
51
  """
52
- get name of plugin
52
+ get name of a Python plugin
53
53
  """
54
54
  # search plugin name
55
- plugin_name = None
55
+ plugin_name: str | None = None
56
56
  with open(plugin_path, "r") as f_in:
57
57
  for line in f_in:
58
58
  if line.startswith("__plugin_name__"):
@@ -61,6 +61,48 @@ def get_plugin_name(plugin_path: str):
61
61
  return plugin_name
62
62
 
63
63
 
64
+ def get_r_plugin_name(plugin_path: str) -> str | None:
65
+ """
66
+ get name of a R plugin
67
+ """
68
+ # search plugin name
69
+ plugin_name: str | None = None
70
+ with open(plugin_path, "r") as f_in:
71
+ for line in f_in:
72
+ if line.startswith("plugin_name"):
73
+ if "=" in line:
74
+ plugin_name = line.split("=")[1].strip().replace('"', "").replace("'", "")
75
+ break
76
+ elif "<-" in line:
77
+ plugin_name = line.split("<-")[1].strip().replace('"', "").replace("'", "")
78
+ break
79
+ else:
80
+ plugin_name = None
81
+ break
82
+ return plugin_name
83
+
84
+
85
+ def get_r_plugin_description(plugin_path: str) -> str | None:
86
+ """
87
+ get description of a R plugin
88
+ """
89
+ # search plugin name
90
+ plugin_description: str | None = None
91
+ with open(plugin_path, "r") as f_in:
92
+ for line in f_in:
93
+ if line.startswith("description"):
94
+ if "=" in line:
95
+ plugin_description = line.split("=")[1].strip().replace('"', "").replace("'", "")
96
+ break
97
+ elif "<-" in line:
98
+ plugin_description = line.split("<-")[1].strip().replace('"', "").replace("'", "")
99
+ break
100
+ else:
101
+ plugin_description = None
102
+ break
103
+ return plugin_description
104
+
105
+
64
106
  def load_plugins(self):
65
107
  """
66
108
  load selected plugins in analysis menu
@@ -80,8 +122,6 @@ def load_plugins(self):
80
122
 
81
123
  # load BORIS plugins
82
124
  for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
83
- if file_.name == "__init__.py":
84
- continue
85
125
  if file_.name.startswith("_"):
86
126
  continue
87
127
  plugin_name = get_plugin_name(file_)
@@ -96,8 +136,6 @@ def load_plugins(self):
96
136
  # load personal plugins
97
137
  if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
98
138
  for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
99
- if file_.name == "__init__.py":
100
- continue
101
139
  if file_.name.startswith("_"):
102
140
  continue
103
141
  plugin_name = get_plugin_name(file_)
@@ -109,6 +147,20 @@ def load_plugins(self):
109
147
 
110
148
  self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
111
149
 
150
+ # load personal R plugins
151
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
152
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.R")):
153
+ if file_.name.startswith("_"):
154
+ continue
155
+ plugin_name = get_r_plugin_name(file_)
156
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
157
+ # check if plugin with same name already loaded
158
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
159
+ msg()
160
+ continue
161
+
162
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
163
+
112
164
  logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
113
165
 
114
166
 
@@ -203,7 +255,7 @@ def run_plugin(self, plugin_name):
203
255
  QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
204
256
  return
205
257
 
206
- plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
258
+ plugin_path: str = self.config_param.get(cfg.ANALYSIS_PLUGINS, {}).get(plugin_name, "")
207
259
 
208
260
  logging.debug(f"{plugin_path=}")
209
261
 
@@ -213,24 +265,12 @@ def run_plugin(self, plugin_name):
213
265
 
214
266
  logging.debug(f"run plugin from {plugin_path}")
215
267
 
216
- module_name = Path(plugin_path).stem
217
-
218
- spec = importlib.util.spec_from_file_location(module_name, plugin_path)
219
- plugin_module = importlib.util.module_from_spec(spec)
220
-
221
- logging.debug(f"{plugin_module=}")
222
-
223
- spec.loader.exec_module(plugin_module)
224
-
225
- logging.info(
226
- f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
227
- )
228
-
268
+ # select observations to analyze
229
269
  selected_observations, parameters = self.obs_param()
230
270
  if not selected_observations:
231
271
  return
232
272
 
233
- logging.info("preparing dtaaframe for plugin")
273
+ logging.info("preparing dataframe for plugin")
234
274
 
235
275
  df = project_functions.project2dataframe(self.pj, selected_observations)
236
276
 
@@ -247,8 +287,79 @@ def run_plugin(self, plugin_name):
247
287
  filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
248
288
  logging.info("done")
249
289
 
250
- plugin_results = plugin_module.run(filtered_df)
251
- # test if plugin_tests is a tuple: if not transform to tuple
290
+ if Path(plugin_path).suffix == ".py":
291
+ # load plugin as module
292
+ module_name = Path(plugin_path).stem
293
+
294
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
295
+ plugin_module = importlib.util.module_from_spec(spec)
296
+
297
+ logging.debug(f"{plugin_module=}")
298
+
299
+ spec.loader.exec_module(plugin_module)
300
+
301
+ plugin_version = plugin_module.__version__
302
+ plugin_version_date = plugin_module.__version_date__
303
+
304
+ logging.info(
305
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
306
+ )
307
+
308
+ # run plugin
309
+ plugin_results = plugin_module.run(filtered_df)
310
+
311
+ if Path(plugin_path).suffix in (".R", ".r"):
312
+ try:
313
+ from rpy2 import robjects
314
+ from rpy2.robjects import pandas2ri
315
+ from rpy2.robjects.packages import SignatureTranslatedAnonymousPackage
316
+ from rpy2.robjects.conversion import localconverter
317
+ except Exception:
318
+ QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
319
+ return
320
+
321
+ # Read code from file
322
+ try:
323
+ with open(plugin_path, "r") as f:
324
+ r_code = f.read()
325
+ except Exception:
326
+ QMessageBox.critical(self, cfg.programName, f"Error reading the plugin {plugin_path}.")
327
+ return
328
+
329
+ # read version
330
+ plugin_version = next(
331
+ (
332
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
333
+ for x in r_code.splitlines()
334
+ if x.replace(" ", "").startswith("version<-")
335
+ ),
336
+ None,
337
+ )
338
+ # read version date
339
+ plugin_version_date = next(
340
+ (
341
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
342
+ for x in r_code.split("\n")
343
+ if x.replace(" ", "").startswith("version_date<")
344
+ ),
345
+ None,
346
+ )
347
+
348
+ r_plugin = SignatureTranslatedAnonymousPackage(r_code, "r_plugin")
349
+
350
+ with localconverter(robjects.default_converter + pandas2ri.converter):
351
+ r_df = robjects.conversion.py2rpy(filtered_df)
352
+
353
+ try:
354
+ r_result = r_plugin.run(r_df)
355
+ except Exception as e:
356
+ QMessageBox.critical(self, cfg.programName, f"Error in the plugin {plugin_path}: {e}.")
357
+ return
358
+
359
+ with localconverter(robjects.default_converter + pandas2ri.converter):
360
+ plugin_results = robjects.conversion.rpy2py(r_result)
361
+
362
+ # test if plugin_results is a tuple: if not transform it to tuple
252
363
  if not isinstance(plugin_results, tuple):
253
364
  plugin_results = tuple([plugin_results])
254
365
 
@@ -261,7 +372,7 @@ def run_plugin(self, plugin_name):
261
372
  self.plugin_visu[-1].ptText.appendPlainText(result)
262
373
  self.plugin_visu[-1].show()
263
374
  elif isinstance(result, pd.DataFrame):
264
- self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_module.__version__} ({plugin_module.__version_date__})", result))
375
+ self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_version} ({plugin_version_date})", result))
265
376
  self.plugin_visu[-1].show()
266
377
  else:
267
378
  # result is not str nor dataframe