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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- 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,536 @@
|
|
|
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 logging
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from decimal import Decimal as dec
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
31
|
+
from matplotlib.figure import Figure
|
|
32
|
+
from PySide6.QtCore import Signal, QEvent, QThread, QObject, Slot, QTimer
|
|
33
|
+
from PySide6.QtWidgets import (
|
|
34
|
+
QSizePolicy,
|
|
35
|
+
QWidget,
|
|
36
|
+
QPushButton,
|
|
37
|
+
QLabel,
|
|
38
|
+
QVBoxLayout,
|
|
39
|
+
QHBoxLayout,
|
|
40
|
+
QSpacerItem,
|
|
41
|
+
QApplication,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from . import utilities as util
|
|
45
|
+
from . import config as cfg
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MyMplCanvas(FigureCanvas):
|
|
49
|
+
def __init__(self, parent=None):
|
|
50
|
+
self.fig = Figure()
|
|
51
|
+
self.axes = self.fig.add_subplot(1, 1, 1)
|
|
52
|
+
FigureCanvas.__init__(self, self.fig)
|
|
53
|
+
self.setParent(parent)
|
|
54
|
+
FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
55
|
+
FigureCanvas.updateGeometry(self)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Plot_data(QWidget):
|
|
59
|
+
send_fig = Signal(float)
|
|
60
|
+
|
|
61
|
+
# send keypress event to mainwindow
|
|
62
|
+
sendEvent = Signal(QEvent)
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
file_name,
|
|
67
|
+
interval,
|
|
68
|
+
time_offset,
|
|
69
|
+
plot_style,
|
|
70
|
+
plot_title,
|
|
71
|
+
y_label,
|
|
72
|
+
columns_to_plot,
|
|
73
|
+
substract_first_value,
|
|
74
|
+
converters,
|
|
75
|
+
column_converter,
|
|
76
|
+
log_level="",
|
|
77
|
+
):
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
self.installEventFilter(self)
|
|
81
|
+
|
|
82
|
+
self.setWindowTitle(f"External data: {plot_title}")
|
|
83
|
+
|
|
84
|
+
d = {}
|
|
85
|
+
# convert dict keys in int:
|
|
86
|
+
for k in column_converter:
|
|
87
|
+
d[int(k)] = column_converter[k]
|
|
88
|
+
column_converter = dict(d)
|
|
89
|
+
|
|
90
|
+
self.myplot = MyMplCanvas(self)
|
|
91
|
+
|
|
92
|
+
self.button_plus = QPushButton("+", self)
|
|
93
|
+
self.button_plus.clicked.connect(lambda: self.zoom(-1))
|
|
94
|
+
|
|
95
|
+
self.button_minus = QPushButton("-", self)
|
|
96
|
+
self.button_minus.clicked.connect(lambda: self.zoom(1))
|
|
97
|
+
|
|
98
|
+
self.layout = QVBoxLayout()
|
|
99
|
+
|
|
100
|
+
self.hlayout1 = QHBoxLayout()
|
|
101
|
+
self.hlayout1.addWidget(QLabel("Zoom"))
|
|
102
|
+
self.hlayout1.addWidget(self.button_plus)
|
|
103
|
+
self.hlayout1.addWidget(self.button_minus)
|
|
104
|
+
self.hlayout1.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
105
|
+
|
|
106
|
+
self.hlayout2 = QHBoxLayout()
|
|
107
|
+
self.hlayout2.addWidget(QLabel("Value"))
|
|
108
|
+
self.lb_value = QLabel("")
|
|
109
|
+
self.hlayout2.addWidget(self.lb_value)
|
|
110
|
+
self.hlayout2.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
111
|
+
|
|
112
|
+
self.layout.addLayout(self.hlayout1)
|
|
113
|
+
self.layout.addLayout(self.hlayout2)
|
|
114
|
+
self.layout.addWidget(self.myplot)
|
|
115
|
+
|
|
116
|
+
self.setLayout(self.layout)
|
|
117
|
+
|
|
118
|
+
self.plot_style = plot_style
|
|
119
|
+
self.plot_title = plot_title
|
|
120
|
+
try:
|
|
121
|
+
self.time_offset = dec(time_offset)
|
|
122
|
+
except Exception:
|
|
123
|
+
self.error_msg = f"The offset value {time_offset} is not a decimal value"
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self.y_label = y_label
|
|
127
|
+
self.error_msg = ""
|
|
128
|
+
|
|
129
|
+
result, error_msg, data = util.txt2np_array(
|
|
130
|
+
file_name,
|
|
131
|
+
columns_to_plot,
|
|
132
|
+
substract_first_value,
|
|
133
|
+
converters=converters,
|
|
134
|
+
column_converter=column_converter,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
print(f"{error_msg=}")
|
|
138
|
+
|
|
139
|
+
if not result:
|
|
140
|
+
self.error_msg = error_msg
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
logging.debug("data[50]: {}".format(data[:50]))
|
|
145
|
+
logging.debug("shape: {}".format(data.shape))
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if data.shape == (0,):
|
|
149
|
+
self.error_msg = "Empty input file"
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# sort data by time ascending
|
|
153
|
+
data = data[data[:, 0].argsort()]
|
|
154
|
+
|
|
155
|
+
# unique
|
|
156
|
+
_, idx = np.unique(data[:, 0], return_index=True)
|
|
157
|
+
data = data[idx]
|
|
158
|
+
|
|
159
|
+
# time
|
|
160
|
+
min_time_value, max_time_value = min(data[:, 0]), max(data[:, 0])
|
|
161
|
+
|
|
162
|
+
# variable
|
|
163
|
+
min_var_value, max_var_value = min(data[:, 1]), max(data[:, 1])
|
|
164
|
+
|
|
165
|
+
# check if time is linear
|
|
166
|
+
diff = set(np.round(np.diff(data, axis=0)[:, 0], 4))
|
|
167
|
+
|
|
168
|
+
# check if only one time value is available
|
|
169
|
+
if not diff:
|
|
170
|
+
self.error_msg = "only one time value is present"
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if min(diff) == 0:
|
|
174
|
+
self.error_msg = "more values for same time"
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
"""logging.debug(f"diff: {diff}")"""
|
|
178
|
+
|
|
179
|
+
min_time_step = min(diff)
|
|
180
|
+
|
|
181
|
+
"""logging.debug(f"min_time_step: {min_time_step}")"""
|
|
182
|
+
|
|
183
|
+
# check if sampling rate is not constant
|
|
184
|
+
if len(diff) != 1:
|
|
185
|
+
"""logging.debug("len diff != 1")"""
|
|
186
|
+
|
|
187
|
+
min_time_step = min(diff)
|
|
188
|
+
|
|
189
|
+
"""logging.debug(f"min_time_step: {min_time_step}")"""
|
|
190
|
+
|
|
191
|
+
# increase value for low sampling rate (> 1 s)
|
|
192
|
+
if min_time_step > 1:
|
|
193
|
+
min_time_step = 1
|
|
194
|
+
|
|
195
|
+
x2 = np.arange(min_time_value, max_time_value + min_time_step, min_time_step)
|
|
196
|
+
data = np.array((x2, np.interp(x2, data[:, 0], data[:, 1]))).T
|
|
197
|
+
del x2
|
|
198
|
+
|
|
199
|
+
"""logging.debug(f"data[:,0]: {data[:, 0]}")"""
|
|
200
|
+
|
|
201
|
+
# time
|
|
202
|
+
min_time_value, max_time_value = min(data[:, 0]), max(data[:, 0])
|
|
203
|
+
# variable
|
|
204
|
+
min_var_value, max_var_value = min(data[:, 1]), max(data[:, 1])
|
|
205
|
+
|
|
206
|
+
diff = set(np.round(np.diff(data, axis=0)[:, 0], 4))
|
|
207
|
+
min_time_step = min(diff)
|
|
208
|
+
|
|
209
|
+
# subsampling
|
|
210
|
+
if min_time_step < 0.04:
|
|
211
|
+
data = data[0 :: int(round(0.04 / min_time_step, 2))]
|
|
212
|
+
min_time_step = 0.04
|
|
213
|
+
|
|
214
|
+
"""logging.debug(f"new data after subsampling: {data[:50]}")"""
|
|
215
|
+
|
|
216
|
+
min_var_value, max_var_value = min(data[:, 1]), max(data[:, 1])
|
|
217
|
+
|
|
218
|
+
max_frequency = 1 / min_time_step
|
|
219
|
+
|
|
220
|
+
self.time_interval = interval * max_frequency
|
|
221
|
+
|
|
222
|
+
# plotter and thread are none at the beginning
|
|
223
|
+
self.plotter = Plotter()
|
|
224
|
+
self.plotter.data = data
|
|
225
|
+
self.plotter.max_frequency = max_frequency
|
|
226
|
+
|
|
227
|
+
self.plotter.min_value = min_var_value
|
|
228
|
+
self.plotter.max_value = max_var_value
|
|
229
|
+
|
|
230
|
+
self.plotter.min_time_value = min_time_value
|
|
231
|
+
self.plotter.max_time_value = max_time_value
|
|
232
|
+
|
|
233
|
+
self.plotter.min_time_step = min_time_step
|
|
234
|
+
|
|
235
|
+
# interval must be even
|
|
236
|
+
interval += 1 if interval % 2 else 0
|
|
237
|
+
self.plotter.interval = interval
|
|
238
|
+
|
|
239
|
+
self.thread = QThread()
|
|
240
|
+
|
|
241
|
+
# connect signals
|
|
242
|
+
self.send_fig.connect(self.plotter.replot)
|
|
243
|
+
self.plotter.return_fig.connect(self.plot)
|
|
244
|
+
# move to thread and start
|
|
245
|
+
self.plotter.moveToThread(self.thread)
|
|
246
|
+
self.thread.start()
|
|
247
|
+
|
|
248
|
+
if min_time_step < 0.2:
|
|
249
|
+
self.time_out = 200
|
|
250
|
+
else:
|
|
251
|
+
self.time_out = round(min_time_step * 1000)
|
|
252
|
+
|
|
253
|
+
def eventFilter(self, receiver, event):
|
|
254
|
+
"""
|
|
255
|
+
send event (if keypress) to main window
|
|
256
|
+
"""
|
|
257
|
+
if event.type() == QEvent.KeyPress:
|
|
258
|
+
self.sendEvent.emit(event)
|
|
259
|
+
return True
|
|
260
|
+
else:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def zoom(self, z):
|
|
264
|
+
if z == -1 and self.plotter.interval <= 10:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
if z == 1 and self.plotter.interval > 3600:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
new_interval = round(self.plotter.interval + z * self.plotter.interval / 2)
|
|
271
|
+
new_interval += 1 if new_interval % 2 else 0
|
|
272
|
+
|
|
273
|
+
self.plotter.interval = new_interval
|
|
274
|
+
|
|
275
|
+
def timer_plot_data_out(self, time_):
|
|
276
|
+
self.update_plot(time_)
|
|
277
|
+
|
|
278
|
+
def update_plot(self, time_):
|
|
279
|
+
"""
|
|
280
|
+
update plot by signal
|
|
281
|
+
"""
|
|
282
|
+
self.send_fig.emit(float(time_) + float(self.time_offset))
|
|
283
|
+
|
|
284
|
+
def close_plot(self):
|
|
285
|
+
self.thread.quit()
|
|
286
|
+
self.thread.wait()
|
|
287
|
+
self.close()
|
|
288
|
+
|
|
289
|
+
# Slot receives data and plots it
|
|
290
|
+
def plot(self, x, y, position_data, position_start, min_value, max_value, position_end):
|
|
291
|
+
# print current value
|
|
292
|
+
try:
|
|
293
|
+
if x[0] == 0:
|
|
294
|
+
self.lb_value.setText(str(round(y[position_data], 3)))
|
|
295
|
+
else:
|
|
296
|
+
self.lb_value.setText(str(round(y[len(y) // 2], 3)))
|
|
297
|
+
except Exception:
|
|
298
|
+
self.lb_value.setText("Read error")
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
self.myplot.axes.clear()
|
|
302
|
+
self.myplot.axes.set_title(self.plot_title)
|
|
303
|
+
self.myplot.axes.set_xlim(position_start, position_end)
|
|
304
|
+
self.myplot.axes.set_ylabel(self.y_label, rotation=90, labelpad=10)
|
|
305
|
+
self.myplot.axes.set_ylim((min_value, max_value))
|
|
306
|
+
self.myplot.axes.plot(x, y, self.plot_style)
|
|
307
|
+
self.myplot.axes.axvline(x=position_data, color=cfg.REALTIME_PLOT_CURSOR_COLOR, linestyle="-")
|
|
308
|
+
|
|
309
|
+
self.myplot.draw()
|
|
310
|
+
except Exception:
|
|
311
|
+
logging.debug(f"error in plotting external data: {sys.exc_info()[1]}")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Plotter(QObject):
|
|
315
|
+
return_fig = Signal(
|
|
316
|
+
np.ndarray, # x array
|
|
317
|
+
np.ndarray, # y array
|
|
318
|
+
float, # position_data
|
|
319
|
+
float, # position start
|
|
320
|
+
float, # min value
|
|
321
|
+
float, # max value
|
|
322
|
+
float, # position end
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
@Slot(float)
|
|
326
|
+
def replot(self, current_time): # time_ in s
|
|
327
|
+
logging.debug("current_time: {}".format(current_time))
|
|
328
|
+
|
|
329
|
+
current_discrete_time = round(round(current_time / self.min_time_step) * self.min_time_step, 2)
|
|
330
|
+
|
|
331
|
+
logging.debug("current_discrete_time: {}".format(current_discrete_time))
|
|
332
|
+
logging.debug("self.interval: {}".format(self.interval))
|
|
333
|
+
|
|
334
|
+
freq_interval = int(round(self.interval / self.min_time_step))
|
|
335
|
+
|
|
336
|
+
if self.min_time_value <= current_discrete_time <= self.max_time_value:
|
|
337
|
+
logging.debug("self.min_time_value <= current_discrete_time <= self.max_time_value")
|
|
338
|
+
|
|
339
|
+
idx = np.where(self.data[:, 0] == current_discrete_time)[0]
|
|
340
|
+
if not len(idx):
|
|
341
|
+
idx = np.where(abs(self.data[:, 0] - current_discrete_time) <= 0.02)[0]
|
|
342
|
+
|
|
343
|
+
if len(idx):
|
|
344
|
+
position_data = idx[0]
|
|
345
|
+
|
|
346
|
+
logging.debug(f"position data: {position_data}")
|
|
347
|
+
|
|
348
|
+
position_start = int(position_data - freq_interval // 2)
|
|
349
|
+
|
|
350
|
+
flag_i, flag_j = False, False
|
|
351
|
+
|
|
352
|
+
if position_start < 0:
|
|
353
|
+
i = np.array([np.nan] * abs(position_start)).T
|
|
354
|
+
flag_i = True
|
|
355
|
+
|
|
356
|
+
logging.debug(f"len(i): {len(i)}")
|
|
357
|
+
|
|
358
|
+
position_start = 0
|
|
359
|
+
|
|
360
|
+
position_end = int(position_data + freq_interval // 2)
|
|
361
|
+
|
|
362
|
+
if position_end >= len(self.data):
|
|
363
|
+
j = np.array([np.nan] * abs(position_end - len(self.data))).T
|
|
364
|
+
flag_j = True
|
|
365
|
+
|
|
366
|
+
position_end = len(self.data)
|
|
367
|
+
|
|
368
|
+
d = self.data[position_start:position_end][:, 1]
|
|
369
|
+
|
|
370
|
+
if flag_i:
|
|
371
|
+
d = np.append(i, d, axis=0)
|
|
372
|
+
|
|
373
|
+
if flag_j:
|
|
374
|
+
d = np.append(d, j, axis=0)
|
|
375
|
+
else:
|
|
376
|
+
# not known problem
|
|
377
|
+
d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
|
|
378
|
+
|
|
379
|
+
elif current_time > self.max_time_value:
|
|
380
|
+
logging.debug(f"self.interval/self.min_time_step/2: {self.interval / self.min_time_step / 2}")
|
|
381
|
+
|
|
382
|
+
dim_footer = int(round((current_time - self.max_time_value) / self.min_time_step + self.interval / self.min_time_step / 2))
|
|
383
|
+
|
|
384
|
+
footer = np.array([np.nan] * dim_footer).T
|
|
385
|
+
logging.debug(f"len footer: {len(footer)}")
|
|
386
|
+
|
|
387
|
+
a = (self.interval / 2 - (current_time - self.max_time_value)) / self.min_time_step
|
|
388
|
+
logging.debug(f"a: {a}")
|
|
389
|
+
|
|
390
|
+
if a >= 0:
|
|
391
|
+
logging.debug("a>=0")
|
|
392
|
+
|
|
393
|
+
st = int(round(len(self.data) - a))
|
|
394
|
+
logging.debug(f"st: {st}")
|
|
395
|
+
|
|
396
|
+
flag_i = False
|
|
397
|
+
if st < 0:
|
|
398
|
+
i = np.array([np.nan] * abs(st)).T
|
|
399
|
+
st = 0
|
|
400
|
+
flag_i = True
|
|
401
|
+
|
|
402
|
+
d = np.append(self.data[st : len(self.data)][:, 1], footer, axis=0)
|
|
403
|
+
|
|
404
|
+
if flag_i:
|
|
405
|
+
d = np.append(i, d, axis=0)
|
|
406
|
+
|
|
407
|
+
logging.debug(f"len d a>=0: {len(d)}")
|
|
408
|
+
|
|
409
|
+
else: # a <0
|
|
410
|
+
logging.debug("a<0")
|
|
411
|
+
d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
|
|
412
|
+
|
|
413
|
+
logging.debug(f"len d a<0: {len(d)}")
|
|
414
|
+
|
|
415
|
+
elif current_time < self.min_time_value:
|
|
416
|
+
x = (self.min_time_value - current_time) / self.min_time_step
|
|
417
|
+
dim_header = int(round(self.interval / self.min_time_step / 2 + x))
|
|
418
|
+
header = np.array([np.nan] * dim_header).T
|
|
419
|
+
|
|
420
|
+
b = int(round(self.interval / self.min_time_step / 2 - x))
|
|
421
|
+
|
|
422
|
+
if b >= 0:
|
|
423
|
+
d = np.append(header, self.data[0:b][:, 1], axis=0)
|
|
424
|
+
if len(d) < freq_interval:
|
|
425
|
+
d = np.append(d, np.array([np.nan] * int(freq_interval - len(d))).T, axis=0)
|
|
426
|
+
|
|
427
|
+
else:
|
|
428
|
+
d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
|
|
429
|
+
|
|
430
|
+
y = d
|
|
431
|
+
logging.debug(f"len y: {len(y)}")
|
|
432
|
+
|
|
433
|
+
logging.debug(f"self.min_time_step: {self.min_time_step}")
|
|
434
|
+
|
|
435
|
+
x = np.arange(
|
|
436
|
+
current_time - self.interval // 2,
|
|
437
|
+
current_time + self.interval // 2,
|
|
438
|
+
self.min_time_step,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
logging.debug(f"len x 1: {len(x)}")
|
|
442
|
+
|
|
443
|
+
self.return_fig.emit(
|
|
444
|
+
x,
|
|
445
|
+
y,
|
|
446
|
+
current_discrete_time, # position_data
|
|
447
|
+
current_discrete_time - self.interval // 2, # position_start
|
|
448
|
+
self.min_value,
|
|
449
|
+
self.max_value,
|
|
450
|
+
current_discrete_time + self.interval // 2, # position_end,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
if __name__ == "__main__":
|
|
455
|
+
"""
|
|
456
|
+
arguments:
|
|
457
|
+
1 file_name
|
|
458
|
+
2 columns_to_plot (example: 1,2)
|
|
459
|
+
3 substract_first_value:True/False
|
|
460
|
+
4 interval (in seconds)
|
|
461
|
+
5 column_converter
|
|
462
|
+
|
|
463
|
+
examples:
|
|
464
|
+
python3 plot_data_module.py data_file.csv 4,6 True 60 "{4:'hhmmss_2_seconds'}"
|
|
465
|
+
python3 plot_data_module.py data_file.csv 1,2 True 60 "{1:'convert_time_ecg'}"
|
|
466
|
+
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
file_name = sys.argv[1]
|
|
470
|
+
columns_to_plot = sys.argv[2]
|
|
471
|
+
substract_first_value = sys.argv[3]
|
|
472
|
+
interval = int(sys.argv[4])
|
|
473
|
+
column_converter = eval(sys.argv[5])
|
|
474
|
+
|
|
475
|
+
time_offset = 0
|
|
476
|
+
color = "g-"
|
|
477
|
+
plot_title = "test"
|
|
478
|
+
y_label = "TEST"
|
|
479
|
+
|
|
480
|
+
converters = {
|
|
481
|
+
"convert_time_ecg": {
|
|
482
|
+
"name": "convert_time_ecg",
|
|
483
|
+
"description": "convert '%d/%m/%Y %H:%M:%S.%f' in seconds from epoch",
|
|
484
|
+
"code": (
|
|
485
|
+
"\nimport datetime\n"
|
|
486
|
+
"epoch = datetime.datetime.utcfromtimestamp(0)\n"
|
|
487
|
+
'datetime_format = "%d/%m/%Y %H:%M:%S.%f"\n\n'
|
|
488
|
+
"OUTPUT = (datetime.datetime.strptime(INPUT, datetime_format) - epoch).total_seconds()\n"
|
|
489
|
+
),
|
|
490
|
+
},
|
|
491
|
+
"hhmmss_2_seconds": {
|
|
492
|
+
"name": "hhmmss_2_seconds",
|
|
493
|
+
"description": "convert HH:MM:SS in seconds",
|
|
494
|
+
"code": "\nh, m, s = INPUT.split(':')\nOUTPUT = int(h) * 3600 + int(m) * 60 + int(s)\n\n",
|
|
495
|
+
},
|
|
496
|
+
"invert_value": {
|
|
497
|
+
"name": "invert value",
|
|
498
|
+
"description": "invert the value",
|
|
499
|
+
"code": "\nOUTPUT = -float(INPUT)\n\n",
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
app = QApplication(sys.argv)
|
|
504
|
+
|
|
505
|
+
win = Plot_data(
|
|
506
|
+
file_name,
|
|
507
|
+
interval,
|
|
508
|
+
time_offset,
|
|
509
|
+
color,
|
|
510
|
+
plot_title,
|
|
511
|
+
y_label,
|
|
512
|
+
columns_to_plot,
|
|
513
|
+
substract_first_value,
|
|
514
|
+
converters,
|
|
515
|
+
column_converter,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if win.error_msg:
|
|
519
|
+
sys.exit()
|
|
520
|
+
|
|
521
|
+
win.show()
|
|
522
|
+
|
|
523
|
+
timer_started_at = time.time()
|
|
524
|
+
|
|
525
|
+
# def timer_plot_data_out():
|
|
526
|
+
# win.update_plot(time.time() - timer_started_at)
|
|
527
|
+
|
|
528
|
+
def get_time():
|
|
529
|
+
return time.time() - timer_started_at
|
|
530
|
+
|
|
531
|
+
win.plot_data_timer = QTimer()
|
|
532
|
+
win.plot_data_timer.setInterval(win.time_out)
|
|
533
|
+
win.plot_data_timer.timeout.connect(lambda: win.timer_plot_data_out(get_time()))
|
|
534
|
+
win.plot_data_timer.start()
|
|
535
|
+
|
|
536
|
+
sys.exit(app.exec_())
|