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.
Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. 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