boris-behav-obs 9.7.7__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/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import wave
|
|
25
|
+
from . import config as cfg
|
|
26
|
+
import matplotlib
|
|
27
|
+
|
|
28
|
+
matplotlib.use("QtAgg")
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
|
32
|
+
from PySide6.QtCore import Signal, QEvent, Qt
|
|
33
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
34
|
+
from matplotlib.figure import Figure
|
|
35
|
+
import matplotlib.ticker as mticker
|
|
36
|
+
|
|
37
|
+
# matplotlib.pyplot.switch_backend("Qt5Agg")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Plot_waveform_RT(QWidget):
|
|
41
|
+
# send keypress event to mainwindow
|
|
42
|
+
sendEvent = Signal(QEvent)
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.setWindowTitle("Waveform")
|
|
47
|
+
|
|
48
|
+
self.interval = 60 # interval of visualization (in seconds)
|
|
49
|
+
self.time_mem = -1
|
|
50
|
+
|
|
51
|
+
self.cursor_color: str = cfg.REALTIME_PLOT_CURSOR_COLOR
|
|
52
|
+
|
|
53
|
+
self.spectro_color_map = matplotlib.pyplot.get_cmap("viridis")
|
|
54
|
+
|
|
55
|
+
self.figure = Figure()
|
|
56
|
+
self.ax = self.figure.add_subplot(1, 1, 1)
|
|
57
|
+
|
|
58
|
+
self.canvas = FigureCanvas(self.figure)
|
|
59
|
+
|
|
60
|
+
layout = QVBoxLayout()
|
|
61
|
+
layout.addWidget(self.canvas)
|
|
62
|
+
|
|
63
|
+
hlayout1 = QHBoxLayout()
|
|
64
|
+
hlayout1.addWidget(QLabel("Time interval"))
|
|
65
|
+
hlayout1.addWidget(
|
|
66
|
+
QPushButton(
|
|
67
|
+
"+",
|
|
68
|
+
self,
|
|
69
|
+
clicked=lambda: self.time_interval_changed(1),
|
|
70
|
+
focusPolicy=Qt.NoFocus,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
hlayout1.addWidget(
|
|
74
|
+
QPushButton(
|
|
75
|
+
"-",
|
|
76
|
+
self,
|
|
77
|
+
clicked=lambda: self.time_interval_changed(-1),
|
|
78
|
+
focusPolicy=Qt.NoFocus,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
layout.addLayout(hlayout1)
|
|
82
|
+
|
|
83
|
+
self.setLayout(layout)
|
|
84
|
+
|
|
85
|
+
self.installEventFilter(self)
|
|
86
|
+
|
|
87
|
+
def eventFilter(self, receiver, event):
|
|
88
|
+
"""
|
|
89
|
+
send event (if keypress) to main window
|
|
90
|
+
"""
|
|
91
|
+
if event.type() == QEvent.KeyPress:
|
|
92
|
+
self.sendEvent.emit(event)
|
|
93
|
+
return True
|
|
94
|
+
else:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def get_wav_info(self, wav_file: str):
|
|
98
|
+
"""
|
|
99
|
+
read wav file and extract information
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
wav_file (str): path of wav file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
np.array: signal contained in wav file
|
|
106
|
+
int: frame rate of wav file
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
wav = wave.open(wav_file, "r")
|
|
111
|
+
frames = wav.readframes(-1)
|
|
112
|
+
# signal = np.fromstring(frames, dtype=np.int16)
|
|
113
|
+
signal = np.frombuffer(frames, dtype=np.int16)
|
|
114
|
+
frame_rate = wav.getframerate()
|
|
115
|
+
wav.close()
|
|
116
|
+
return signal, frame_rate
|
|
117
|
+
except Exception:
|
|
118
|
+
return np.array([]), 0
|
|
119
|
+
|
|
120
|
+
def load_wav(self, wav_file_path: str) -> dict:
|
|
121
|
+
"""
|
|
122
|
+
load wav file in numpy array
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
wav_file_path (str): path of wav file
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict: "error" key if error, "media_length" and "frame_rate"
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
self.sound_info, self.frame_rate = self.get_wav_info(wav_file_path)
|
|
133
|
+
if not self.frame_rate:
|
|
134
|
+
return {"error": f"unknown format for file {wav_file_path}"}
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
return {"error": "File not found: {}".format(wav_file_path)}
|
|
137
|
+
|
|
138
|
+
self.media_length = len(self.sound_info) / self.frame_rate
|
|
139
|
+
self.wav_file_path = wav_file_path
|
|
140
|
+
|
|
141
|
+
return {"media_length": self.media_length, "frame_rate": self.frame_rate}
|
|
142
|
+
|
|
143
|
+
def time_interval_changed(self, action: int) -> None:
|
|
144
|
+
"""
|
|
145
|
+
change the time interval for plotting waveform
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
action (int): -1 decrease time interval, +1 increase time interval
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
None
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
if action == -1 and self.interval <= 5:
|
|
155
|
+
return
|
|
156
|
+
self.interval += 5 * action
|
|
157
|
+
self.plot_waveform(current_time=self.time_mem, force_plot=True)
|
|
158
|
+
|
|
159
|
+
def plot_waveform(self, current_time: float, force_plot: bool = False):
|
|
160
|
+
"""
|
|
161
|
+
plot sound waveform centered on the current time
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
current_time (float): time for displaying waveform
|
|
165
|
+
force_plot (bool): force plot even if media paused
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
if not force_plot and current_time == self.time_mem:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
self.time_mem = current_time
|
|
172
|
+
|
|
173
|
+
self.ax.clear()
|
|
174
|
+
|
|
175
|
+
# start
|
|
176
|
+
if current_time <= self.interval / 2:
|
|
177
|
+
time_ = np.linspace(
|
|
178
|
+
0,
|
|
179
|
+
len(self.sound_info[: int((self.interval) * self.frame_rate)]) / self.frame_rate,
|
|
180
|
+
num=len(self.sound_info[: int((self.interval) * self.frame_rate)]),
|
|
181
|
+
)
|
|
182
|
+
self.ax.plot(time_, self.sound_info[: int((self.interval) * self.frame_rate)])
|
|
183
|
+
|
|
184
|
+
self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
|
|
185
|
+
|
|
186
|
+
# cursor
|
|
187
|
+
self.ax.axvline(x=current_time, color=self.cursor_color, linestyle="-")
|
|
188
|
+
|
|
189
|
+
elif current_time >= self.media_length - self.interval / 2:
|
|
190
|
+
i = int(round(len(self.sound_info) - (self.interval * self.frame_rate), 0))
|
|
191
|
+
|
|
192
|
+
time_ = np.linspace(
|
|
193
|
+
0,
|
|
194
|
+
len(self.sound_info[i:]) / self.frame_rate,
|
|
195
|
+
num=len(self.sound_info[i:]),
|
|
196
|
+
)
|
|
197
|
+
self.ax.plot(time_, self.sound_info[i:])
|
|
198
|
+
|
|
199
|
+
lim1 = current_time - (self.media_length - self.interval / 2)
|
|
200
|
+
lim2 = lim1 + self.interval
|
|
201
|
+
|
|
202
|
+
self.ax.set_xlim(lim1, lim2)
|
|
203
|
+
|
|
204
|
+
self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
|
|
205
|
+
self.ax.set_xticklabels([str(round(w + self.media_length - self.interval, 1)) for w in self.ax.get_xticks()])
|
|
206
|
+
|
|
207
|
+
# cursor
|
|
208
|
+
self.ax.axvline(x=lim1 + self.interval / 2, color=self.cursor_color, linestyle="-")
|
|
209
|
+
|
|
210
|
+
# middle
|
|
211
|
+
else:
|
|
212
|
+
start = (current_time - self.interval / 2) * self.frame_rate
|
|
213
|
+
end = (current_time + self.interval / 2) * self.frame_rate
|
|
214
|
+
|
|
215
|
+
time_ = np.linspace(
|
|
216
|
+
0,
|
|
217
|
+
len(self.sound_info[int(round(start, 0)) : int(round(end, 0))]) / self.frame_rate,
|
|
218
|
+
num=len(self.sound_info[int(round(start, 0)) : int(round(end, 0))]),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
self.ax.plot(time_, self.sound_info[int(round(start, 0)) : int(round(end, 0))])
|
|
222
|
+
|
|
223
|
+
self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
|
|
224
|
+
self.ax.set_xticklabels([str(round(current_time + w - self.interval / 2, 1)) for w in self.ax.get_xticks()])
|
|
225
|
+
|
|
226
|
+
# cursor
|
|
227
|
+
self.ax.axvline(x=self.interval / 2, color=self.cursor_color, linestyle="-")
|
|
228
|
+
"""self.figure.subplots_adjust(wspace=0, hspace=0)"""
|
|
229
|
+
|
|
230
|
+
self.canvas.draw()
|
boris/plugins.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import logging
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from PySide6.QtGui import QAction
|
|
29
|
+
from PySide6.QtWidgets import QMessageBox
|
|
30
|
+
|
|
31
|
+
from . import config as cfg
|
|
32
|
+
from . import project_functions
|
|
33
|
+
from . import dialog
|
|
34
|
+
from . import view_df
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_plugins_to_menu(self):
|
|
38
|
+
"""
|
|
39
|
+
add plugins to the plugins menu
|
|
40
|
+
"""
|
|
41
|
+
for plugin_name in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
|
|
42
|
+
logging.debug(f"adding plugin '{plugin_name}' to menu")
|
|
43
|
+
# Create an action for each submenu option
|
|
44
|
+
action = QAction(self, triggered=lambda checked=False, name=plugin_name: run_plugin(self, name))
|
|
45
|
+
action.setText(plugin_name)
|
|
46
|
+
|
|
47
|
+
self.menu_plugins.addAction(action)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_plugin_name(plugin_path: str) -> str | None:
|
|
51
|
+
"""
|
|
52
|
+
get name of a Python plugin
|
|
53
|
+
"""
|
|
54
|
+
# search plugin name
|
|
55
|
+
plugin_name: str | None = None
|
|
56
|
+
with open(plugin_path, "r") as f_in:
|
|
57
|
+
for line in f_in:
|
|
58
|
+
if line.startswith("__plugin_name__"):
|
|
59
|
+
plugin_name = line.split("=")[1].strip().replace('"', "")
|
|
60
|
+
break
|
|
61
|
+
return plugin_name
|
|
62
|
+
|
|
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
|
+
|
|
106
|
+
def load_plugins(self):
|
|
107
|
+
"""
|
|
108
|
+
load selected plugins in config_param
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
logging.debug("Loading plugins")
|
|
112
|
+
|
|
113
|
+
def msg():
|
|
114
|
+
QMessageBox.warning(
|
|
115
|
+
self,
|
|
116
|
+
cfg.programName,
|
|
117
|
+
f"A plugin with the same name is already loaded ({self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name]}).\n\nThe plugin from {file_} is not loaded.",
|
|
118
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
119
|
+
QMessageBox.NoButton,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.menu_plugins.clear()
|
|
123
|
+
self.config_param[cfg.ANALYSIS_PLUGINS] = {}
|
|
124
|
+
|
|
125
|
+
# load BORIS plugins
|
|
126
|
+
for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
|
|
127
|
+
if file_.name.startswith("_"):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
logging.debug(f"Loading plugin: {Path(file_).stem}")
|
|
131
|
+
|
|
132
|
+
# test module
|
|
133
|
+
module_name = Path(file_).stem
|
|
134
|
+
spec = importlib.util.spec_from_file_location(module_name, file_)
|
|
135
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
136
|
+
spec.loader.exec_module(plugin_module)
|
|
137
|
+
attributes_list = dir(plugin_module)
|
|
138
|
+
|
|
139
|
+
if "__plugin_name__" in attributes_list:
|
|
140
|
+
plugin_name = plugin_module.__plugin_name__
|
|
141
|
+
else:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if "run" not in attributes_list:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# plugin_name = get_plugin_name(file_)
|
|
148
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
149
|
+
# check if plugin with same name already loaded
|
|
150
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
151
|
+
msg()
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
155
|
+
|
|
156
|
+
# load personal plugins
|
|
157
|
+
if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
|
|
158
|
+
for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
|
|
159
|
+
if file_.name.startswith("_"):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
logging.debug(f"Loading personal plugin: {Path(file_).stem}")
|
|
163
|
+
|
|
164
|
+
# test module
|
|
165
|
+
module_name = Path(file_).stem
|
|
166
|
+
spec = importlib.util.spec_from_file_location(module_name, file_)
|
|
167
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
168
|
+
spec.loader.exec_module(plugin_module)
|
|
169
|
+
attributes_list = dir(plugin_module)
|
|
170
|
+
|
|
171
|
+
if "__plugin_name__" in attributes_list:
|
|
172
|
+
plugin_name = plugin_module.__plugin_name__
|
|
173
|
+
else:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if "run" not in attributes_list:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# plugin_name = get_plugin_name(file_)
|
|
180
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
181
|
+
# check if plugin with same name already loaded
|
|
182
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
183
|
+
msg()
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
187
|
+
|
|
188
|
+
# load personal R plugins
|
|
189
|
+
if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
|
|
190
|
+
for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.R")):
|
|
191
|
+
if file_.name.startswith("_"):
|
|
192
|
+
continue
|
|
193
|
+
plugin_name = get_r_plugin_name(file_)
|
|
194
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
195
|
+
# check if plugin with same name already loaded
|
|
196
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
197
|
+
msg()
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
201
|
+
|
|
202
|
+
logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
|
|
206
|
+
"""
|
|
207
|
+
filter the dataframe following parameters
|
|
208
|
+
|
|
209
|
+
filter by selected observations.
|
|
210
|
+
filter by selected subjects.
|
|
211
|
+
filter by selected behaviors.
|
|
212
|
+
filter by time interval.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
# filter selected observations
|
|
216
|
+
df = df[df["Observation id"].isin(observations_list)]
|
|
217
|
+
|
|
218
|
+
if parameters:
|
|
219
|
+
# filter selected subjects
|
|
220
|
+
df = df[df["Subject"].isin(parameters["selected subjects"])]
|
|
221
|
+
|
|
222
|
+
# filter selected behaviors
|
|
223
|
+
df = df[df["Behavior"].isin(parameters["selected behaviors"])]
|
|
224
|
+
|
|
225
|
+
if parameters["time"] == cfg.TIME_OBS_INTERVAL:
|
|
226
|
+
# filter each observation with observation interval start/stop
|
|
227
|
+
|
|
228
|
+
# keep events between observation interval start time and observation interval stop/end
|
|
229
|
+
df_interval = df[
|
|
230
|
+
(
|
|
231
|
+
((df["Start (s)"] >= df["Observation interval start"]) & (df["Start (s)"] <= df["Observation interval stop"]))
|
|
232
|
+
| ((df["Stop (s)"] >= df["Observation interval start"]) & (df["Stop (s)"] <= df["Observation interval stop"]))
|
|
233
|
+
)
|
|
234
|
+
| ((df["Start (s)"] < df["Observation interval start"]) & (df["Stop (s)"] > df["Observation interval stop"]))
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
df_interval.loc[df["Start (s)"] < df["Observation interval start"], "Start (s)"] = df["Observation interval start"]
|
|
238
|
+
df_interval.loc[df["Stop (s)"] > df["Observation interval stop"], "Stop (s)"] = df["Observation interval stop"]
|
|
239
|
+
|
|
240
|
+
df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
|
|
241
|
+
|
|
242
|
+
df = df_interval
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
# filter selected time interval
|
|
246
|
+
if parameters["start time"] is not None and parameters["end time"] is not None:
|
|
247
|
+
MIN_TIME = parameters["start time"]
|
|
248
|
+
MAX_TIME = parameters["end time"]
|
|
249
|
+
|
|
250
|
+
# keep events between start time and end_time
|
|
251
|
+
df_interval = df[
|
|
252
|
+
(
|
|
253
|
+
((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
|
|
254
|
+
| ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
|
|
255
|
+
)
|
|
256
|
+
| ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
# cut state events to interval
|
|
260
|
+
df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
|
|
261
|
+
df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
|
|
262
|
+
|
|
263
|
+
df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
|
|
264
|
+
|
|
265
|
+
df = df_interval
|
|
266
|
+
|
|
267
|
+
print("filtered")
|
|
268
|
+
print("=" * 50)
|
|
269
|
+
|
|
270
|
+
# print(f"{df=}")
|
|
271
|
+
|
|
272
|
+
return df
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def run_plugin(self, plugin_name):
|
|
276
|
+
"""
|
|
277
|
+
run plugin
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
if not self.project:
|
|
281
|
+
QMessageBox.warning(
|
|
282
|
+
self,
|
|
283
|
+
cfg.programName,
|
|
284
|
+
"No observations found. Open a project first",
|
|
285
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
286
|
+
QMessageBox.NoButton,
|
|
287
|
+
)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
|
|
291
|
+
|
|
292
|
+
if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
|
|
293
|
+
QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
plugin_path: str = self.config_param.get(cfg.ANALYSIS_PLUGINS, {}).get(plugin_name, "")
|
|
297
|
+
|
|
298
|
+
logging.debug(f"{plugin_path=}")
|
|
299
|
+
|
|
300
|
+
if not Path(plugin_path).is_file():
|
|
301
|
+
QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
logging.debug(f"run plugin from {plugin_path}")
|
|
305
|
+
|
|
306
|
+
# select observations to analyze
|
|
307
|
+
selected_observations, parameters = self.obs_param()
|
|
308
|
+
if not selected_observations:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
logging.info("preparing dataframe for plugin")
|
|
312
|
+
|
|
313
|
+
message, df = project_functions.project2dataframe(self.pj, selected_observations)
|
|
314
|
+
if message:
|
|
315
|
+
logging.critical(message)
|
|
316
|
+
QMessageBox.critical(self, cfg.programName, message)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
logging.info("done")
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
logging.debug("dataframe info")
|
|
323
|
+
logging.debug(f"{df.info()}")
|
|
324
|
+
logging.debug(f"{df.head()}")
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
# filter the dataframe with parameters
|
|
328
|
+
logging.info("filtering dataframe for plugin")
|
|
329
|
+
filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
|
|
330
|
+
logging.info("done")
|
|
331
|
+
|
|
332
|
+
if Path(plugin_path).suffix == ".py":
|
|
333
|
+
# load plugin as module
|
|
334
|
+
module_name = Path(plugin_path).stem
|
|
335
|
+
|
|
336
|
+
spec = importlib.util.spec_from_file_location(module_name, plugin_path)
|
|
337
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
338
|
+
|
|
339
|
+
logging.debug(f"{plugin_module=}")
|
|
340
|
+
|
|
341
|
+
spec.loader.exec_module(plugin_module)
|
|
342
|
+
|
|
343
|
+
plugin_version = plugin_module.__version__
|
|
344
|
+
plugin_version_date = plugin_module.__version_date__
|
|
345
|
+
|
|
346
|
+
logging.info(
|
|
347
|
+
f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# run plugin
|
|
351
|
+
plugin_results = plugin_module.run(filtered_df)
|
|
352
|
+
|
|
353
|
+
if Path(plugin_path).suffix in (".R", ".r"):
|
|
354
|
+
try:
|
|
355
|
+
from rpy2 import robjects
|
|
356
|
+
from rpy2.robjects import pandas2ri
|
|
357
|
+
from rpy2.robjects.packages import SignatureTranslatedAnonymousPackage
|
|
358
|
+
from rpy2.robjects.conversion import localconverter
|
|
359
|
+
except Exception:
|
|
360
|
+
QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
# Read code from file
|
|
364
|
+
try:
|
|
365
|
+
with open(plugin_path, "r") as f:
|
|
366
|
+
r_code = f.read()
|
|
367
|
+
except Exception:
|
|
368
|
+
QMessageBox.critical(self, cfg.programName, f"Error reading the plugin {plugin_path}.")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
# read version
|
|
372
|
+
plugin_version = next(
|
|
373
|
+
(
|
|
374
|
+
x.split("<-")[1].replace('"', "").replace("'", "").strip()
|
|
375
|
+
for x in r_code.splitlines()
|
|
376
|
+
if x.replace(" ", "").startswith("version<-")
|
|
377
|
+
),
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
# read version date
|
|
381
|
+
plugin_version_date = next(
|
|
382
|
+
(
|
|
383
|
+
x.split("<-")[1].replace('"', "").replace("'", "").strip()
|
|
384
|
+
for x in r_code.split("\n")
|
|
385
|
+
if x.replace(" ", "").startswith("version_date<")
|
|
386
|
+
),
|
|
387
|
+
None,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
r_plugin = SignatureTranslatedAnonymousPackage(r_code, "r_plugin")
|
|
391
|
+
|
|
392
|
+
with localconverter(robjects.default_converter + pandas2ri.converter):
|
|
393
|
+
r_df = robjects.conversion.py2rpy(filtered_df)
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
r_result = r_plugin.run(r_df)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
QMessageBox.critical(self, cfg.programName, f"Error in the plugin {plugin_path}: {e}.")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
with localconverter(robjects.default_converter + pandas2ri.converter):
|
|
402
|
+
plugin_results = robjects.conversion.rpy2py(r_result)
|
|
403
|
+
|
|
404
|
+
# test if plugin_results is a tuple: if not transform it to tuple
|
|
405
|
+
if not isinstance(plugin_results, tuple):
|
|
406
|
+
plugin_results = tuple([plugin_results])
|
|
407
|
+
|
|
408
|
+
self.plugin_visu: list = []
|
|
409
|
+
for result in plugin_results:
|
|
410
|
+
if isinstance(result, str):
|
|
411
|
+
self.plugin_visu.append(dialog.Results_dialog())
|
|
412
|
+
self.plugin_visu[-1].setWindowTitle(plugin_name)
|
|
413
|
+
self.plugin_visu[-1].ptText.clear()
|
|
414
|
+
self.plugin_visu[-1].ptText.appendPlainText(result)
|
|
415
|
+
self.plugin_visu[-1].show()
|
|
416
|
+
elif isinstance(result, pd.DataFrame):
|
|
417
|
+
self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_version} ({plugin_version_date})", result))
|
|
418
|
+
self.plugin_visu[-1].show()
|
|
419
|
+
else:
|
|
420
|
+
# result is not str nor dataframe
|
|
421
|
+
QMessageBox.critical(
|
|
422
|
+
None,
|
|
423
|
+
cfg.programName,
|
|
424
|
+
(
|
|
425
|
+
f"Plugin returns an unknown object type: {type(result)}\n\n"
|
|
426
|
+
"Plugins must return str and/or Pandas Dataframes.\n"
|
|
427
|
+
"Check the plugin code."
|
|
428
|
+
),
|
|
429
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
430
|
+
QMessageBox.NoButton,
|
|
431
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .const import Bound, inf
|
|
2
|
+
from .interval import Interval, open, closed, openclosed, closedopen, empty, singleton
|
|
3
|
+
from .func import iterate
|
|
4
|
+
from .io import from_string, to_string, from_data, to_data
|
|
5
|
+
|
|
6
|
+
# disabled because BORIS does not need IntervalDict
|
|
7
|
+
# so the sortedcontainers module is not required
|
|
8
|
+
# from .dict import IntervalDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"inf",
|
|
13
|
+
"CLOSED",
|
|
14
|
+
"OPEN",
|
|
15
|
+
"Interval",
|
|
16
|
+
"open",
|
|
17
|
+
"closed",
|
|
18
|
+
"openclosed",
|
|
19
|
+
"closedopen",
|
|
20
|
+
"singleton",
|
|
21
|
+
"empty",
|
|
22
|
+
"iterate",
|
|
23
|
+
"from_string",
|
|
24
|
+
"to_string",
|
|
25
|
+
"from_data",
|
|
26
|
+
"to_data",
|
|
27
|
+
"IntervalDict",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
CLOSED = Bound.CLOSED
|
|
31
|
+
OPEN = Bound.OPEN
|