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.
- boris/analysis_plugins/_latency.py +1 -1
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/config.py +10 -0
- boris/connections.py +2 -1
- boris/core.py +2 -0
- boris/core_ui.py +6 -2
- boris/export_observation.py +2 -6
- boris/external_processes.py +98 -73
- boris/import_observations.py +28 -19
- boris/observation_operations.py +45 -26
- boris/plot_events.py +1 -1
- boris/plot_events_rt.py +1 -1
- boris/plot_spectrogram_rt.py +62 -13
- boris/plot_waveform_rt.py +1 -1
- boris/plugins.py +136 -25
- boris/preferences.py +182 -108
- boris/preferences_ui.py +216 -32
- boris/project_functions.py +1 -3
- boris/utilities.py +21 -14
- boris/version.py +2 -2
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/METADATA +4 -1
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/RECORD +26 -25
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/WHEEL +0 -0
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.4.1.dist-info → boris_behav_obs-9.5.2.dist-info}/top_level.txt +0 -0
boris/plot_spectrogram_rt.py
CHANGED
|
@@ -24,10 +24,10 @@ Copyright 2012-2025 Olivier Friard
|
|
|
24
24
|
import wave
|
|
25
25
|
import matplotlib
|
|
26
26
|
|
|
27
|
-
matplotlib.use("
|
|
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(
|
|
210
|
+
self.sound_info[: int(self.interval * self.frame_rate)],
|
|
207
211
|
mode="psd",
|
|
208
|
-
|
|
212
|
+
NFFT=nfft,
|
|
209
213
|
Fs=self.frame_rate,
|
|
210
|
-
|
|
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
|
-
|
|
244
|
+
NFFT=nfft,
|
|
226
245
|
Fs=self.frame_rate,
|
|
227
|
-
|
|
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
|
-
|
|
285
|
+
NFFT=nfft,
|
|
252
286
|
Fs=self.frame_rate,
|
|
253
|
-
|
|
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
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, {})
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
251
|
-
|
|
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"{
|
|
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
|