boris-behav-obs 8.27.10__py2.py3-none-any.whl → 9.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. boris/about.py +7 -5
  2. boris/add_modifier.py +35 -35
  3. boris/add_modifier_ui.py +229 -129
  4. boris/advanced_event_filtering.py +3 -3
  5. boris/analysis_plugins/__init__.py +0 -0
  6. boris/analysis_plugins/number_of_occurences.py +60 -0
  7. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +72 -0
  8. boris/analysis_plugins/time_budget.py +95 -0
  9. boris/behav_coding_map_creator.py +103 -108
  10. boris/behavior_binary_table.py +1 -1
  11. boris/behaviors_coding_map.py +8 -8
  12. boris/coding_pad.py +6 -6
  13. boris/config.py +6 -0
  14. boris/config_file.py +1 -1
  15. boris/connections.py +4 -2
  16. boris/converters.py +2 -3
  17. boris/converters_ui.py +187 -110
  18. boris/cooccurence.py +2 -2
  19. boris/core.py +340 -94
  20. boris/core_qrc.py +16088 -13246
  21. boris/core_ui.py +922 -812
  22. boris/dialog.py +14 -13
  23. boris/duration_widget.py +5 -5
  24. boris/edit_event.py +1 -1
  25. boris/edit_event_ui.py +162 -88
  26. boris/event_operations.py +4 -25
  27. boris/events_cursor.py +17 -9
  28. boris/events_snapshots.py +5 -5
  29. boris/exclusion_matrix.py +1 -1
  30. boris/export_events.py +38 -28
  31. boris/export_observation.py +1 -1
  32. boris/external_processes.py +3 -5
  33. boris/geometric_measurement.py +49 -26
  34. boris/gui_utilities.py +31 -30
  35. boris/import_observations.py +2 -4
  36. boris/irr.py +1 -1
  37. boris/latency.py +1 -1
  38. boris/map_creator.py +77 -89
  39. boris/measurement_widget.py +4 -4
  40. boris/media_file.py +2 -4
  41. boris/menu_options.py +1 -3
  42. boris/modifiers_coding_map.py +4 -4
  43. boris/mpv2.py +0 -2
  44. boris/observation.py +124 -29
  45. boris/observation_operations.py +18 -40
  46. boris/observation_ui.py +566 -374
  47. boris/observations_list.py +6 -6
  48. boris/param_panel.py +2 -2
  49. boris/param_panel_ui.py +246 -141
  50. boris/player_dock_widget.py +16 -21
  51. boris/plot_data_module.py +6 -6
  52. boris/plot_events_rt.py +7 -8
  53. boris/plot_spectrogram_rt.py +7 -8
  54. boris/plot_waveform_rt.py +6 -7
  55. boris/plugins.py +79 -0
  56. boris/preferences.py +127 -17
  57. boris/preferences_ui.py +464 -240
  58. boris/project.py +69 -72
  59. boris/project_functions.py +233 -31
  60. boris/project_import_export.py +59 -67
  61. boris/project_ui.py +672 -440
  62. boris/qrc_boris.py +6 -3
  63. boris/qrc_boris5.py +6 -3
  64. boris/select_modifiers.py +2 -2
  65. boris/select_observations.py +2 -2
  66. boris/select_subj_behav.py +3 -3
  67. boris/state_events.py +1 -1
  68. boris/subjects_pad.py +5 -5
  69. boris/synthetic_time_budget.py +2 -2
  70. boris/time_budget_functions.py +15 -0
  71. boris/time_budget_widget.py +4 -4
  72. boris/transitions.py +34 -25
  73. boris/utilities.py +95 -2
  74. boris/version.py +2 -2
  75. boris/video_equalizer.py +4 -4
  76. boris/video_equalizer_ui.py +199 -130
  77. boris/video_operations.py +1 -1
  78. boris/view_df.py +106 -0
  79. boris/view_df_ui.py +75 -0
  80. boris/write_event.py +9 -1
  81. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.1.dist-info}/METADATA +5 -5
  82. boris_behav_obs-9.0.1.dist-info/RECORD +103 -0
  83. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.1.dist-info}/WHEEL +1 -1
  84. boris/qdarkstyle/__init__.py +0 -479
  85. boris/qdarkstyle/__main__.py +0 -66
  86. boris/qdarkstyle/colorsystem.py +0 -38
  87. boris/qdarkstyle/dark/__init__.py +0 -1
  88. boris/qdarkstyle/dark/darkstyle_rc.py +0 -11379
  89. boris/qdarkstyle/dark/palette.py +0 -38
  90. boris/qdarkstyle/example/__init__.py +0 -4
  91. boris/qdarkstyle/example/__main__.py +0 -386
  92. boris/qdarkstyle/example/ui/__init__.py +0 -4
  93. boris/qdarkstyle/light/__init__.py +0 -1
  94. boris/qdarkstyle/light/lightstyle_rc.py +0 -11305
  95. boris/qdarkstyle/light/palette.py +0 -37
  96. boris/qdarkstyle/palette.py +0 -102
  97. boris/qdarkstyle/utils/__init__.py +0 -73
  98. boris/qdarkstyle/utils/__main__.py +0 -96
  99. boris/qdarkstyle/utils/images.py +0 -449
  100. boris/qdarkstyle/utils/scss.py +0 -318
  101. boris/vlc_local.py +0 -83
  102. boris_behav_obs-8.27.10.dist-info/RECORD +0 -114
  103. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.1.dist-info}/LICENSE.TXT +0 -0
  104. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.1.dist-info}/entry_points.txt +0 -0
  105. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.1.dist-info}/top_level.txt +0 -0
boris/project.py CHANGED
@@ -24,11 +24,13 @@ import json
24
24
  import logging
25
25
  import re
26
26
 
27
- from PyQt5.QtCore import Qt, QDateTime
28
- from PyQt5.QtGui import QColor
29
- from PyQt5.QtWidgets import (
27
+ from PySide6.QtCore import Qt, QDateTime
28
+ from PySide6.QtGui import QColor
29
+ from PySide6.QtWidgets import (
30
+ QAbstractItemView,
30
31
  QApplication,
31
32
  QCheckBox,
33
+ QColorDialog,
32
34
  QDialog,
33
35
  QFileDialog,
34
36
  QHBoxLayout,
@@ -41,11 +43,9 @@ from PyQt5.QtWidgets import (
41
43
  QPushButton,
42
44
  QSizePolicy,
43
45
  QSpacerItem,
46
+ QTableWidget,
44
47
  QTableWidgetItem,
45
48
  QVBoxLayout,
46
- QColorDialog,
47
- QTableWidget,
48
- QAbstractItemView,
49
49
  )
50
50
 
51
51
  from . import add_modifier
@@ -60,11 +60,10 @@ class BehavioralCategories(QDialog):
60
60
  Class for managing the behavioral categories
61
61
  """
62
62
 
63
- def __init__(self, pj, dark_mode):
63
+ def __init__(self, pj):
64
64
  super().__init__()
65
65
 
66
66
  self.pj = pj
67
- self.dark_mode = dark_mode
68
67
  self.setWindowTitle("Behavioral categories")
69
68
 
70
69
  self.renamed = None
@@ -153,10 +152,12 @@ class BehavioralCategories(QDialog):
153
152
  """
154
153
  return a color for the not editable column
155
154
  """
156
- if self.dark_mode:
157
- return QColor(55, 65, 79)
158
- else:
159
- return QColor(230, 230, 230)
155
+ window_color = QApplication.instance().palette().window().color()
156
+ return QColor(
157
+ window_color.red() - cfg.DARKER_DIFFERENCE,
158
+ window_color.green() - cfg.DARKER_DIFFERENCE,
159
+ window_color.blue() - cfg.DARKER_DIFFERENCE,
160
+ )
160
161
 
161
162
  def lw_double_clicked(self, row: int, column: int):
162
163
  """
@@ -302,10 +303,10 @@ class projectDialog(QDialog, Ui_dlgProject):
302
303
  "remove all|Remove all behaviors",
303
304
  "lower|Convert keys to lower case",
304
305
  ]
305
- menu = QMenu()
306
- menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
307
- self.add_button_menu(behavior_button_items, menu)
308
- self.pb_behavior.setMenu(menu)
306
+ self.behavior_menu = QMenu()
307
+ self.behavior_menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
308
+ self.add_button_menu(behavior_button_items, self.behavior_menu)
309
+ self.pb_behavior.setMenu(self.behavior_menu)
309
310
 
310
311
  import_button_items = [
311
312
  "boris|from a BORIS project",
@@ -315,10 +316,10 @@ class projectDialog(QDialog, Ui_dlgProject):
315
316
  "clipboard|from the clipboard",
316
317
  "repository|from the BORIS repository",
317
318
  ]
318
- menu = QMenu()
319
- menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
320
- self.add_button_menu(import_button_items, menu)
321
- self.pb_import.setMenu(menu)
319
+ self.import_behaviors_menu = QMenu()
320
+ self.import_behaviors_menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
321
+ self.add_button_menu(import_button_items, self.import_behaviors_menu)
322
+ self.pb_import.setMenu(self.import_behaviors_menu)
322
323
 
323
324
  self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
324
325
 
@@ -342,10 +343,10 @@ class projectDialog(QDialog, Ui_dlgProject):
342
343
  "lower|Convert keys to lower case",
343
344
  ]
344
345
 
345
- menu = QMenu()
346
- menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
347
- self.add_button_menu(subjects_button_items, menu)
348
- self.pb_subjects.setMenu(menu)
346
+ self.subject_menu = QMenu()
347
+ self.subject_menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
348
+ self.add_button_menu(subjects_button_items, self.subject_menu)
349
+ self.pb_subjects.setMenu(self.subject_menu)
349
350
 
350
351
  subjects_import_button_items = [
351
352
  "boris|from a BORIS project",
@@ -353,10 +354,10 @@ class projectDialog(QDialog, Ui_dlgProject):
353
354
  "text|from a text file (CSV or TSV)",
354
355
  "clipboard|from the clipboard",
355
356
  ]
356
- menu = QMenu()
357
- menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
358
- self.add_button_menu(subjects_import_button_items, menu)
359
- self.pbImportSubjectsFromProject.setMenu(menu)
357
+ self.import_subjects_menu = QMenu()
358
+ self.import_subjects_menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
359
+ self.add_button_menu(subjects_import_button_items, self.import_subjects_menu)
360
+ self.pbImportSubjectsFromProject.setMenu(self.import_subjects_menu)
360
361
 
361
362
  self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
362
363
 
@@ -430,10 +431,12 @@ class projectDialog(QDialog, Ui_dlgProject):
430
431
  """
431
432
  return a color for the not editable column
432
433
  """
433
- if self.config_param.get(cfg.DARK_MODE, cfg.DEFAULT_FRAME_MODE):
434
- return QColor(55, 65, 79)
435
- else:
436
- return QColor(230, 230, 230)
434
+ window_color = QApplication.instance().palette().window().color()
435
+ return QColor(
436
+ window_color.red() - cfg.DARKER_DIFFERENCE,
437
+ window_color.green() - cfg.DARKER_DIFFERENCE,
438
+ window_color.blue() - cfg.DARKER_DIFFERENCE,
439
+ )
437
440
 
438
441
  def add_button_menu(self, data, menu_obj):
439
442
  """
@@ -633,45 +636,45 @@ class projectDialog(QDialog, Ui_dlgProject):
633
636
  Add a behaviors coding map from file
634
637
  """
635
638
 
636
- fn = QFileDialog().getOpenFileName(
639
+ file_name, _ = QFileDialog().getOpenFileName(
637
640
  self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
638
641
  )
639
- file_name = fn[0] if type(fn) is tuple else fn
640
- if file_name:
641
- try:
642
- bcm = json.loads(open(file_name, "r").read())
643
- except Exception:
644
- QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
645
- return
642
+ if not file_name:
643
+ return
644
+ try:
645
+ bcm = json.loads(open(file_name, "r").read())
646
+ except Exception:
647
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
648
+ return
646
649
 
647
- if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
648
- QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map.")
650
+ if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
651
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map.")
649
652
 
650
- if cfg.BEHAVIORS_CODING_MAP not in self.pj:
651
- self.pj[cfg.BEHAVIORS_CODING_MAP] = []
653
+ if cfg.BEHAVIORS_CODING_MAP not in self.pj:
654
+ self.pj[cfg.BEHAVIORS_CODING_MAP] = []
652
655
 
653
- bcm_code_not_found = []
654
- existing_codes = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
655
- for code in [bcm["areas"][key][cfg.BEHAVIOR_CODE] for key in bcm["areas"]]:
656
- if code not in existing_codes:
657
- bcm_code_not_found.append(code)
656
+ bcm_code_not_found = []
657
+ existing_codes = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
658
+ for code in [bcm["areas"][key][cfg.BEHAVIOR_CODE] for key in bcm["areas"]]:
659
+ if code not in existing_codes:
660
+ bcm_code_not_found.append(code)
658
661
 
659
- if bcm_code_not_found:
660
- QMessageBox.warning(
661
- self,
662
- cfg.programName,
663
- ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
664
- "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
665
- ),
666
- )
662
+ if bcm_code_not_found:
663
+ QMessageBox.warning(
664
+ self,
665
+ cfg.programName,
666
+ ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
667
+ "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
668
+ ),
669
+ )
667
670
 
668
- self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
671
+ self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
669
672
 
670
- self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
673
+ self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
671
674
 
672
- self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
673
- codes = ", ".join([bcm["areas"][idx][cfg.BEHAVIOR_CODE] for idx in bcm["areas"]])
674
- self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 1, QTableWidgetItem(codes))
675
+ self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
676
+ codes = ", ".join([bcm["areas"][idx][cfg.BEHAVIOR_CODE] for idx in bcm["areas"]])
677
+ self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 1, QTableWidgetItem(codes))
675
678
 
676
679
  def remove_behaviors_coding_map(self):
677
680
  """
@@ -730,7 +733,7 @@ class projectDialog(QDialog, Ui_dlgProject):
730
733
  behavioral categories manager
731
734
  """
732
735
 
733
- bc = BehavioralCategories(self.pj, self.config_param.get(cfg.DARK_MODE, cfg.DEFAULT_FRAME_MODE))
736
+ bc = BehavioralCategories(self.pj) # self.config_param.get(cfg.DARK_MODE, cfg.DEFAULT_FRAME_MODE)
734
737
 
735
738
  if bc.exec_():
736
739
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
@@ -868,7 +871,6 @@ class projectDialog(QDialog, Ui_dlgProject):
868
871
  color = col_diag.currentColor()
869
872
  if color.name() == "#000000": # black -> delete color
870
873
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
871
- # self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(230, 230, 230))
872
874
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
873
875
  elif color.isValid():
874
876
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
@@ -1200,7 +1202,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1200
1202
  if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
1201
1203
  item = QTableWidgetItem(",".join(new_excl[e]))
1202
1204
  item.setFlags(Qt.ItemIsEnabled)
1203
- # item.setBackground(QColor(230, 230, 230))
1204
1205
  item.setBackground(self.not_editable_column_color())
1205
1206
  self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
1206
1207
 
@@ -1304,14 +1305,12 @@ class projectDialog(QDialog, Ui_dlgProject):
1304
1305
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
1305
1306
  if field in (cfg.TYPE, "category", "excluded", "coding map", "modifiers"):
1306
1307
  item.setFlags(Qt.ItemIsEnabled)
1307
- # item.setBackground(QColor(230, 230, 230))
1308
1308
  item.setBackground(self.not_editable_column_color())
1309
1309
  if field == cfg.COLOR:
1310
1310
  item.setFlags(Qt.ItemIsEnabled)
1311
1311
  if QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()).isValid():
1312
1312
  item.setBackground(QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()))
1313
1313
  else:
1314
- # item.setBackground(QColor(230, 230, 230))
1315
1314
  item.setBackground(self.not_editable_column_color())
1316
1315
 
1317
1316
  self.twBehaviors.scrollToBottom()
@@ -1376,17 +1375,15 @@ class projectDialog(QDialog, Ui_dlgProject):
1376
1375
 
1377
1376
  if cfg.CODING_MAP_sp in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
1378
1377
  # let user select a coding maop
1379
- fn = QFileDialog().getOpenFileName(
1378
+ file_name, _ = QFileDialog().getOpenFileName(
1380
1379
  self,
1381
1380
  "Select a modifier coding map for " f"{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1382
1381
  "",
1383
1382
  "BORIS map files (*.boris_map);;All files (*)",
1384
1383
  )
1385
- fileName = fn[0] if type(fn) is tuple else fn
1386
-
1387
- if fileName:
1384
+ if file_name:
1388
1385
  try:
1389
- new_map = json.loads(open(fileName, "r").read())
1386
+ new_map = json.loads(open(file_name, "r").read())
1390
1387
  except Exception:
1391
1388
  QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
1392
1389
  return
@@ -23,6 +23,8 @@ import gzip
23
23
  import json
24
24
  import logging
25
25
  import os
26
+ import pandas as pd
27
+ import numpy as np
26
28
  import pathlib as pl
27
29
  import sys
28
30
  from decimal import Decimal as dec
@@ -30,8 +32,8 @@ from shutil import copyfile
30
32
  from typing import List, Tuple, Dict
31
33
 
32
34
  import tablib
33
- from PyQt5.QtWidgets import QMessageBox, QTableWidgetItem, QAbstractItemView
34
- from PyQt5.QtCore import Qt
35
+ from PySide6.QtWidgets import QMessageBox, QTableWidgetItem, QAbstractItemView
36
+ from PySide6.QtCore import Qt
35
37
 
36
38
  from . import config as cfg
37
39
  from . import db_functions
@@ -294,7 +296,7 @@ def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_f
294
296
  tuple (bool, str): if OK True else False , message
295
297
  """
296
298
 
297
- out = ""
299
+ out: str = ""
298
300
 
299
301
  # check if behaviors are defined as "state event"
300
302
  event_types = {ethogram[idx]["type"] for idx in ethogram}
@@ -304,6 +306,7 @@ def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_f
304
306
 
305
307
  subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
306
308
  ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
309
+ state_behaviors = set(util.state_behavior_codes(ethogram))
307
310
 
308
311
  for subject in sorted(set(subjects)):
309
312
  behaviors = [
@@ -315,33 +318,35 @@ def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_f
315
318
  # return (False, "The behaviour <b>{}</b> is not defined in the ethogram.<br>".format(behavior))
316
319
  continue
317
320
  else:
318
- if cfg.STATE in event_type(behavior, ethogram).upper():
319
- lst: list = []
320
- memTime: dict = {}
321
- for event in [
322
- event
323
- for event in observation[cfg.EVENTS]
324
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
325
- ]:
326
- behav_modif = [
327
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
328
- event[cfg.EVENT_MODIFIER_FIELD_IDX],
329
- ]
321
+ if behavior not in state_behaviors:
322
+ continue
330
323
 
331
- if behav_modif in lst:
332
- lst.remove(behav_modif)
333
- del memTime[str(behav_modif)]
334
- else:
335
- lst.append(behav_modif)
336
- memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
337
-
338
- for event in lst:
339
- out += (
340
- f"The behavior <b>{behavior}</b> "
341
- f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
342
- f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
343
- f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
344
- )
324
+ lst: list = []
325
+ memTime: dict = {}
326
+ for event in [
327
+ event
328
+ for event in observation[cfg.EVENTS]
329
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
330
+ ]:
331
+ behav_modif = [
332
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
333
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
334
+ ]
335
+
336
+ if behav_modif in lst:
337
+ lst.remove(behav_modif)
338
+ del memTime[str(behav_modif)]
339
+ else:
340
+ lst.append(behav_modif)
341
+ memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
342
+
343
+ for event in lst:
344
+ out += (
345
+ f"The behavior <b>{behavior}</b> "
346
+ f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
347
+ f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
348
+ f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
349
+ )
345
350
 
346
351
  return (False, out) if out else (True, "No problem detected")
347
352
 
@@ -410,6 +415,10 @@ def check_project_integrity(
410
415
 
411
416
  Returns:
412
417
  str: message
418
+
419
+
420
+ TODO: implement check on order of events (for live and media)
421
+
413
422
  """
414
423
  out: str = ""
415
424
 
@@ -1276,8 +1285,8 @@ def open_project_json(projectFileName: str) -> tuple:
1276
1285
 
1277
1286
  logging.debug(f"open project: {projectFileName}")
1278
1287
 
1279
- projectChanged = False
1280
- msg = ""
1288
+ projectChanged: bool = False
1289
+ msg: str = ""
1281
1290
 
1282
1291
  if not os.path.isfile(projectFileName):
1283
1292
  return (
@@ -1515,6 +1524,11 @@ def open_project_json(projectFileName: str) -> tuple:
1515
1524
 
1516
1525
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1517
1526
 
1527
+ # sort events by time asc
1528
+ for obs_id in pj[cfg.OBSERVATIONS]:
1529
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
1530
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort()
1531
+
1518
1532
  return projectFileName, projectChanged, pj, msg
1519
1533
 
1520
1534
 
@@ -1747,3 +1761,191 @@ def explore_project(self) -> None:
1747
1761
 
1748
1762
  else:
1749
1763
  QMessageBox.information(self, cfg.programName, "No events found")
1764
+
1765
+
1766
+ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1767
+ # print(pj.keys())
1768
+
1769
+ # print(pj["independent_variables"])
1770
+
1771
+ # indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
1772
+
1773
+ indep_variables = dict(
1774
+ [(pj["independent_variables"][idx]["label"], pj["independent_variables"][idx]["type"]) for idx in pj["independent_variables"]]
1775
+ )
1776
+
1777
+ # print()
1778
+ # print(f"{indep_variables=}")
1779
+
1780
+ # n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
1781
+
1782
+ # behavioral_categories
1783
+ behavioral_category = dict([(pj["behaviors_conf"][x]["code"], pj["behaviors_conf"][x]["category"]) for x in pj["behaviors_conf"]])
1784
+
1785
+ # print(f"{pj["behaviors_conf"]=}")
1786
+
1787
+ all_modifier_sets: list = []
1788
+ for behavior_id in pj["behaviors_conf"]:
1789
+ modifier_names = []
1790
+ set_count = 0
1791
+ if pj["behaviors_conf"][behavior_id]["modifiers"] == "":
1792
+ continue
1793
+ for modifier in pj["behaviors_conf"][behavior_id]["modifiers"].values():
1794
+ if modifier["name"]:
1795
+ modifier_names.append((pj["behaviors_conf"][behavior_id]["code"], modifier["name"]))
1796
+ else:
1797
+ set_count += 1
1798
+ modifier_names.append((pj["behaviors_conf"][behavior_id]["code"], f"set #{set_count}"))
1799
+
1800
+ # print(modifier_names)
1801
+ if modifier_names:
1802
+ all_modifier_sets.extend(modifier_names)
1803
+
1804
+ # print()
1805
+ # print(f"{all_modifier_sets=}")
1806
+
1807
+ # create df
1808
+
1809
+ data = {
1810
+ "Observation id": [],
1811
+ # "Observation date": [],
1812
+ # "Description": [],
1813
+ # "Observation type": [],
1814
+ # "Source": [],
1815
+ # "Time offset (s)": [],
1816
+ # "Coding duration": [],
1817
+ # "Media duration (s)": [],
1818
+ # "FPS (frame/s)": [],
1819
+ }
1820
+
1821
+ for indep_var in indep_variables:
1822
+ data[f"independent variable '{indep_var}'"] = []
1823
+
1824
+ data = data | {
1825
+ "Subject": [],
1826
+ "Observation duration by subject by observation": [],
1827
+ "Behavior": [],
1828
+ "Behavioral category": [],
1829
+ }
1830
+
1831
+ for modifier_set in all_modifier_sets:
1832
+ data[modifier_set] = []
1833
+
1834
+ data = data | {
1835
+ "Behavior type": [],
1836
+ "Start (s)": [],
1837
+ "Stop (s)": [],
1838
+ "Duration (s)": [],
1839
+ # "Media file name": [],
1840
+ # "Image index start": [],
1841
+ # "Image index stop": [],
1842
+ # "Image file path start": [],
1843
+ # "Image file path stop": [],
1844
+ "Comment start": [],
1845
+ "Comment stop": [],
1846
+ }
1847
+
1848
+ #
1849
+
1850
+ type_ = {
1851
+ "Observation id": "string",
1852
+ # "Observation date": "string",
1853
+ # "Description": "string",
1854
+ # "Observation type": "string",
1855
+ # "Source": "string",
1856
+ # "Time offset (s)": "string",
1857
+ # "Coding duration": "float64",
1858
+ # "Media duration (s)": "string",
1859
+ # "FPS (frame/s)": "float64",
1860
+ }
1861
+
1862
+ # TODO: set correct type in base of the var type
1863
+ for indep_var in indep_variables:
1864
+ type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == "numeric" else "string"
1865
+
1866
+ type_ = type_ | {
1867
+ "Subject": "string",
1868
+ "Observation duration by subject by observation": "float64",
1869
+ "Behavior": "string",
1870
+ "Behavioral category": "string",
1871
+ }
1872
+
1873
+ for modifer_set in all_modifier_sets:
1874
+ type_[modifer_set] = "string"
1875
+
1876
+ type_ = type_ | {
1877
+ "Behavior type": "string",
1878
+ "Start (s)": "float64",
1879
+ "Stop (s)": "float64",
1880
+ "Duration (s)": "float64",
1881
+ # "Media file name": "string",
1882
+ # "Image index start": "float64",
1883
+ # "Image index stop": "float64",
1884
+ # "Image file path start": "string",
1885
+ # "Image file path stop": "string",
1886
+ "Comment start": "string",
1887
+ "Comment stop": "string",
1888
+ }
1889
+
1890
+ state_behaviors = [pj["behaviors_conf"][x]["code"] for x in pj["behaviors_conf"] if pj["behaviors_conf"][x]["type"] == "State event"]
1891
+
1892
+ for obs_id in pj["observations"]:
1893
+ if observations_list and obs_id not in observations_list:
1894
+ continue
1895
+ # print(obs_id)
1896
+ stop_event_idx = set()
1897
+ for idx_event, event in enumerate(pj["observations"][obs_id]["events"]):
1898
+ if idx_event in stop_event_idx:
1899
+ continue
1900
+ data["Observation id"].append(obs_id)
1901
+ # data["Observation date"].append(pj["observations"][obs_id]["date"])
1902
+ # data["Description"].append(pj["observations"][obs_id]["description"])
1903
+ # data["Observation type"].append(pj["observations"][obs_id]["type"])
1904
+ # data["Source"].append("")
1905
+ # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
1906
+ # data["Coding duration"].append("")
1907
+ # data["Media duration (s)"].append("")
1908
+ # data["FPS (frame/s)"].append("")
1909
+
1910
+ for indep_var in indep_variables:
1911
+ data[f"independent variable '{indep_var}'"].append(pj["observations"][obs_id]["independent_variables"][indep_var])
1912
+
1913
+ data["Subject"].append(event[1])
1914
+ data["Observation duration by subject by observation"].append(-1)
1915
+ data["Behavior"].append(event[2])
1916
+ data["Behavioral category"].append(behavioral_category[event[2]])
1917
+
1918
+ count_set = 0
1919
+ for modifier_set in all_modifier_sets:
1920
+ if event[2] == modifier_set[0]:
1921
+ data[modifier_set].append(event[3].split("|")[count_set])
1922
+ count_set += 1
1923
+ else:
1924
+ data[modifier_set].append(np.nan)
1925
+
1926
+ data["Behavior type"].append("State event" if event[2] in state_behaviors else "Point event")
1927
+ data["Start (s)"].append(event[0])
1928
+ if event[2] in state_behaviors:
1929
+ # search stop
1930
+ # print(f"==> {idx_event=} {event[1:4]=}")
1931
+ for idx_event2, event2 in enumerate(pj["observations"][obs_id]["events"][idx_event + 1 :], start=idx_event + 1):
1932
+ # print(f"{idx_event2=} {event2[1:4]=}")
1933
+ if event2[1:4] == event[1:4]:
1934
+ # print("found")
1935
+ stop_event_idx.add(idx_event2)
1936
+ data["Stop (s)"].append(event2[0])
1937
+ data["Duration (s)"].append(event2[0] - event[0])
1938
+ data["Comment start"].append(event[4])
1939
+ data["Comment stop"].append(event2[4])
1940
+ break
1941
+ else:
1942
+ # print("not paired")
1943
+ raise ("not paired")
1944
+
1945
+ else: # point
1946
+ data["Stop (s)"].append(event[0])
1947
+ data["Duration (s)"].append(np.nan)
1948
+ data["Comment start"].append(event[4])
1949
+ data["Comment stop"].append(event[4])
1950
+
1951
+ return pd.DataFrame(data)