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.

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,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_())