boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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 (126) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +266 -144
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_export_to_feral.py +225 -0
  9. boris/analysis_plugins/_latency.py +59 -0
  10. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  11. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  13. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  14. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  15. boris/analysis_plugins/number_of_occurences.py +22 -0
  16. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  17. boris/analysis_plugins/time_budget.py +61 -0
  18. boris/behav_coding_map_creator.py +235 -236
  19. boris/behavior_binary_table.py +33 -50
  20. boris/behaviors_coding_map.py +17 -18
  21. boris/boris_cli.py +6 -25
  22. boris/cmd_arguments.py +12 -1
  23. boris/coding_pad.py +19 -36
  24. boris/config.py +109 -50
  25. boris/config_file.py +58 -67
  26. boris/connections.py +105 -58
  27. boris/converters.py +13 -37
  28. boris/converters_ui.py +187 -110
  29. boris/cooccurence.py +250 -0
  30. boris/core.py +2174 -1303
  31. boris/core_qrc.py +15892 -10829
  32. boris/core_ui.py +941 -806
  33. boris/db_functions.py +17 -42
  34. boris/dev.py +27 -7
  35. boris/dialog.py +461 -242
  36. boris/duration_widget.py +9 -14
  37. boris/edit_event.py +61 -31
  38. boris/edit_event_ui.py +208 -97
  39. boris/event_operations.py +405 -281
  40. boris/events_cursor.py +25 -17
  41. boris/events_snapshots.py +36 -82
  42. boris/exclusion_matrix.py +4 -9
  43. boris/export_events.py +180 -203
  44. boris/export_observation.py +60 -73
  45. boris/external_processes.py +123 -98
  46. boris/geometric_measurement.py +427 -218
  47. boris/gui_utilities.py +91 -14
  48. boris/image_overlay.py +4 -4
  49. boris/import_observations.py +190 -98
  50. boris/ipc_mpv.py +325 -0
  51. boris/irr.py +20 -57
  52. boris/latency.py +31 -24
  53. boris/measurement_widget.py +14 -18
  54. boris/media_file.py +17 -19
  55. boris/menu_options.py +16 -6
  56. boris/modifier_coding_map_creator.py +1013 -0
  57. boris/modifiers_coding_map.py +7 -9
  58. boris/mpv2.py +128 -35
  59. boris/observation.py +501 -211
  60. boris/observation_operations.py +1037 -393
  61. boris/observation_ui.py +573 -363
  62. boris/observations_list.py +51 -58
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +45 -59
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +91 -56
  67. boris/plot_data_module.py +20 -53
  68. boris/plot_events.py +56 -153
  69. boris/plot_events_rt.py +16 -30
  70. boris/plot_spectrogram_rt.py +83 -56
  71. boris/plot_waveform_rt.py +27 -49
  72. boris/plugins.py +468 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +307 -123
  80. boris/preferences_ui.py +686 -227
  81. boris/project.py +294 -271
  82. boris/project_functions.py +626 -537
  83. boris/project_import_export.py +204 -213
  84. boris/project_ui.py +673 -441
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +62 -90
  88. boris/select_observations.py +19 -197
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +51 -33
  91. boris/subjects_pad.py +7 -9
  92. boris/synthetic_time_budget.py +42 -26
  93. boris/time_budget_functions.py +169 -169
  94. boris/time_budget_widget.py +77 -89
  95. boris/transitions.py +41 -41
  96. boris/utilities.py +594 -226
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +86 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +240 -136
  104. boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
  106. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -37
  112. boris/core.ui +0 -1571
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -982
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1074
  120. boris/vlc_local.py +0 -90
  121. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  122. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  123. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  124. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  125. {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
  126. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -22,24 +22,25 @@ Copyright 2012-2023 Olivier Friard
22
22
  import gzip
23
23
  import json
24
24
  import logging
25
- import os
26
- import pathlib as pl
25
+ import pandas as pd
26
+ import numpy as np
27
+ from pathlib import Path
27
28
  import sys
28
29
  from decimal import Decimal as dec
29
30
  from shutil import copyfile
30
31
  from typing import List, Tuple, Dict
31
32
 
32
33
  import tablib
33
- from PyQt5.QtWidgets import QMessageBox, QTableWidgetItem
34
- from PyQt5.QtCore import Qt
34
+ from PySide6.QtWidgets import QMessageBox, QTableWidgetItem, QAbstractItemView
35
+ from PySide6.QtCore import Qt
35
36
 
36
37
  from . import config as cfg
37
38
  from . import db_functions
38
39
  from . import dialog
40
+ from . import observation_operations
39
41
  from . import portion as I
40
42
  from . import utilities as util
41
43
  from . import version
42
- from . import observation_operations
43
44
 
44
45
 
45
46
  def check_observation_exhaustivity(
@@ -56,6 +57,9 @@ def check_observation_exhaustivity(
56
57
  """
57
58
 
58
59
  def interval_len(interval: I) -> dec:
60
+ """ "
61
+ returns duration of an interval or a set of intervals
62
+ """
59
63
  if interval.empty:
60
64
  return dec(0)
61
65
  else:
@@ -69,61 +73,36 @@ def check_observation_exhaustivity(
69
73
  events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]] = {}
70
74
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]] = {}
71
75
 
72
- if (
73
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
74
- not in events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]]
75
- ):
76
- events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
77
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
78
- ] = I.empty()
79
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
80
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
81
- ] = []
76
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]]:
77
+ events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = I.empty()
78
+ mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
82
79
 
83
80
  # state event
84
81
  if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_events_list:
85
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
86
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
87
- ].append(event[cfg.EVENT_TIME_FIELD_IDX])
88
- if (
89
- len(
90
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
91
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
92
- ]
93
- )
94
- == 2
95
- ):
96
- events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
97
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
98
- ] |= I.closedopen(
99
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
100
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
101
- ][0],
102
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
103
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
104
- ][1],
82
+ mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]].append(
83
+ event[cfg.EVENT_TIME_FIELD_IDX]
84
+ )
85
+ if len(mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]]) == 2:
86
+ events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.closedopen(
87
+ mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][0],
88
+ mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][1],
105
89
  )
106
- mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
107
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
108
- ] = []
90
+ mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
109
91
  # point event
110
92
  else:
111
- events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
112
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
113
- ] |= I.singleton(event[cfg.EVENT_TIME_FIELD_IDX])
93
+ events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.singleton(
94
+ event[cfg.EVENT_TIME_FIELD_IDX]
95
+ )
114
96
 
115
97
  if events:
116
98
  # coding duration
117
- obs_theo_dur = (
118
- max(events)[cfg.EVENT_TIME_FIELD_IDX]
119
- - min(events)[cfg.EVENT_TIME_FIELD_IDX]
120
- )
99
+ event_timestamps = [event[cfg.EVENT_TIME_FIELD_IDX] for event in events]
100
+ obs_theo_dur = max(event_timestamps) - min(event_timestamps)
121
101
  else:
122
102
  obs_theo_dur = dec("0")
123
103
 
124
104
  total_duration = 0
125
105
  for subject in events_interval:
126
-
127
106
  tot_behav_for_subject = I.empty()
128
107
  for behav in events_interval[subject]:
129
108
  tot_behav_for_subject |= events_interval[subject][behav]
@@ -136,9 +115,7 @@ def check_observation_exhaustivity(
136
115
  total_duration += obs_real_dur
137
116
 
138
117
  if len(events_interval) and obs_theo_dur:
139
- exhausivity_percent = (
140
- total_duration / (len(events_interval) * obs_theo_dur) * 100
141
- )
118
+ exhausivity_percent = total_duration / (len(events_interval) * obs_theo_dur) * 100
142
119
  else:
143
120
  exhausivity_percent = 0
144
121
 
@@ -161,9 +138,7 @@ def check_observation_exhaustivity_pictures(obs) -> float:
161
138
  return "No pictures found"
162
139
 
163
140
  # list of paths of coded images
164
- coded_images_number = len(
165
- set([x[cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_PATH]] for x in obs[cfg.EVENTS]])
166
- )
141
+ coded_images_number = len(set([x[cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_PATH]] for x in obs[cfg.EVENTS]]))
167
142
 
168
143
  return round(coded_images_number / tot_images_number * 100, 1)
169
144
 
@@ -182,17 +157,13 @@ def behavior_category(ethogram: dict) -> Dict[str, str]:
182
157
  behavioral_category = {}
183
158
  for idx in ethogram:
184
159
  if cfg.BEHAVIOR_CATEGORY in ethogram[idx]:
185
- behavioral_category[ethogram[idx][cfg.BEHAVIOR_CODE]] = ethogram[idx][
186
- cfg.BEHAVIOR_CATEGORY
187
- ]
160
+ behavioral_category[ethogram[idx][cfg.BEHAVIOR_CODE]] = ethogram[idx][cfg.BEHAVIOR_CATEGORY]
188
161
  else:
189
162
  behavioral_category[ethogram[idx][cfg.BEHAVIOR_CODE]] = ""
190
163
  return behavioral_category
191
164
 
192
165
 
193
- def check_if_media_available(
194
- observation: dict, project_file_name: str
195
- ) -> Tuple[bool, str]:
166
+ def check_if_media_available(observation: dict, project_file_name: str) -> Tuple[bool, str]:
196
167
  """
197
168
  check if media files available for media and images observations
198
169
 
@@ -227,9 +198,7 @@ def check_if_media_available(
227
198
  return (False, "Observation type not found")
228
199
 
229
200
 
230
- def check_directories_availability(
231
- observation: dict, project_file_name: str
232
- ) -> Tuple[bool, str]:
201
+ def check_directories_availability(observation: dict, project_file_name: str) -> Tuple[bool, str]:
233
202
  """
234
203
  check if directories are available
235
204
 
@@ -256,9 +225,7 @@ def check_coded_behaviors_in_obs_list(pj: dict, observations_list: list) -> bool
256
225
  check if coded behaviors in a list of observations are defined in the ethogram
257
226
  """
258
227
  out = ""
259
- ethogram_behavior_codes = {
260
- pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]
261
- }
228
+ ethogram_behavior_codes = {pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]}
262
229
  behaviors_not_defined = []
263
230
  out = "" # will contain the output
264
231
  for obs_id in observations_list:
@@ -278,6 +245,18 @@ def check_coded_behaviors_in_obs_list(pj: dict, observations_list: list) -> bool
278
245
  return False
279
246
 
280
247
 
248
+ def get_modifiers_of_behavior(ethogram, behavior: str) -> list:
249
+ """
250
+ get all modifiers for a behavior (if any)
251
+ """
252
+
253
+ return [
254
+ [ethogram[x][cfg.MODIFIERS][y]["values"] for y in ethogram[x][cfg.MODIFIERS]]
255
+ for x in ethogram
256
+ if ethogram[x][cfg.BEHAVIOR_CODE] == behavior
257
+ ]
258
+
259
+
281
260
  def check_coded_behaviors(pj: dict) -> set:
282
261
  """
283
262
  check if behaviors coded in events are defined in ethogram for all observations
@@ -286,13 +265,11 @@ def check_coded_behaviors(pj: dict) -> set:
286
265
  pj (dict): project dictionary
287
266
 
288
267
  Returns:
289
- set: behaviors present in observations that are not define in ethogram
268
+ set: behaviors present in observations that are not defined in ethogram
290
269
  """
291
270
 
292
271
  # set of behaviors defined in ethogram
293
- ethogram_behavior_codes = {
294
- pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]
295
- }
272
+ ethogram_behavior_codes = {pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]}
296
273
  behaviors_not_defined = []
297
274
 
298
275
  for obs_id in pj[cfg.OBSERVATIONS]:
@@ -302,9 +279,7 @@ def check_coded_behaviors(pj: dict) -> set:
302
279
  return set(sorted(behaviors_not_defined))
303
280
 
304
281
 
305
- def check_state_events_obs(
306
- obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS
307
- ) -> Tuple[bool, str]:
282
+ def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS) -> Tuple[bool, str]:
308
283
  """
309
284
  check state events for the observation obsId
310
285
  check if behaviors in observation are defined in ethogram
@@ -320,7 +295,7 @@ def check_state_events_obs(
320
295
  tuple (bool, str): if OK True else False , message
321
296
  """
322
297
 
323
- out = ""
298
+ out: str = ""
324
299
 
325
300
  # check if behaviors are defined as "state event"
326
301
  event_types = {ethogram[idx]["type"] for idx in ethogram}
@@ -330,13 +305,11 @@ def check_state_events_obs(
330
305
 
331
306
  subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
332
307
  ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
308
+ state_behaviors = set(util.state_behavior_codes(ethogram))
333
309
 
334
310
  for subject in sorted(set(subjects)):
335
-
336
311
  behaviors = [
337
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
338
- for event in observation[cfg.EVENTS]
339
- if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
312
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
340
313
  ]
341
314
 
342
315
  for behavior in sorted(set(behaviors)):
@@ -344,35 +317,35 @@ def check_state_events_obs(
344
317
  # return (False, "The behaviour <b>{}</b> is not defined in the ethogram.<br>".format(behavior))
345
318
  continue
346
319
  else:
347
- if cfg.STATE in event_type(behavior, ethogram).upper():
348
- lst: list = []
349
- memTime: dict = {}
350
- for event in [
351
- event
352
- for event in observation[cfg.EVENTS]
353
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior
354
- and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
355
- ]:
356
-
357
- behav_modif = [
358
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
359
- event[cfg.EVENT_MODIFIER_FIELD_IDX],
360
- ]
320
+ if behavior not in state_behaviors:
321
+ continue
361
322
 
362
- if behav_modif in lst:
363
- lst.remove(behav_modif)
364
- del memTime[str(behav_modif)]
365
- else:
366
- lst.append(behav_modif)
367
- memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
368
-
369
- for event in lst:
370
- out += (
371
- f"The behavior <b>{behavior}</b> "
372
- f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
373
- f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
374
- f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
375
- )
323
+ lst: list = []
324
+ memTime: dict = {}
325
+ for event in [
326
+ event
327
+ for event in observation[cfg.EVENTS]
328
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
329
+ ]:
330
+ behav_modif = [
331
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
332
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
333
+ ]
334
+
335
+ if behav_modif in lst:
336
+ lst.remove(behav_modif)
337
+ del memTime[str(behav_modif)]
338
+ else:
339
+ lst.append(behav_modif)
340
+ memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
341
+
342
+ for event in lst:
343
+ out += (
344
+ f"The behavior <b>{behavior}</b> "
345
+ f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
346
+ f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
347
+ f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
348
+ )
376
349
 
377
350
  return (False, out) if out else (True, "No problem detected")
378
351
 
@@ -383,12 +356,12 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
383
356
  use check_state_events_obs function
384
357
  """
385
358
 
359
+ logging.info("Check state events function")
360
+
386
361
  out = ""
387
362
  not_paired_obs_list = []
388
363
  for obs_id in observations_list:
389
- r, msg = check_state_events_obs(
390
- obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id]
391
- )
364
+ r, msg = check_state_events_obs(obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id])
392
365
 
393
366
  if not r:
394
367
  out += f"Observation: <strong>{obs_id}</strong><br>{msg}<br>"
@@ -406,12 +379,12 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
406
379
  return True, []
407
380
 
408
381
  # remove observations with unpaired state events
409
- new_observations_list = [
410
- x for x in observations_list if x not in not_paired_obs_list
411
- ]
382
+ new_observations_list = [x for x in observations_list if x not in not_paired_obs_list]
412
383
  if not new_observations_list:
413
384
  QMessageBox.warning(None, cfg.programName, "The observation list is empty")
414
385
 
386
+ logging.info("Check state events done")
387
+
415
388
  return False, new_observations_list # no state events are unpaired
416
389
 
417
390
 
@@ -422,14 +395,16 @@ def check_project_integrity(
422
395
  media_file_available: bool = True,
423
396
  ) -> str:
424
397
  """
425
- check project integrity
426
- check if behaviors in observations are in ethogram
427
- check unpaired state events
428
- check if behavior belong to behavioral category that do not more exist
429
- check for leading and trailing spaces and special chars in modifiers
430
- check if media file are available
431
- check if media length available
432
- check independent variables
398
+ check project integrity:
399
+ * check if coded behaviors are defined in ethogram
400
+ * check unpaired state events
401
+ * check if timestamp between -2147483647 and 2147483647 (2**31 - 1)
402
+ * check if behavior belong to behavioral category that do not more exist
403
+ * check for leading and trailing spaces and special chars in modifiers
404
+ * check if media file are available (optional)
405
+ * check if media length available
406
+ * check independent variables
407
+ * check if coded subjects are defined
433
408
 
434
409
  Args:
435
410
  pj (dict): BORIS project
@@ -439,19 +414,24 @@ def check_project_integrity(
439
414
 
440
415
  Returns:
441
416
  str: message
417
+
418
+
419
+ TODO: implement check on order of events (for live and media)
420
+
442
421
  """
443
- out = ""
422
+ out: str = ""
444
423
 
445
424
  # check if coded behaviors are defined in ethogram
446
425
  r = check_coded_behaviors(pj)
447
426
  if r:
448
427
  out += f"The following behaviors are not defined in the ethogram: <b>{', '.join(r)}</b><br>"
428
+ flag_all_behaviors_defined = False
429
+ else:
430
+ flag_all_behaviors_defined = True
449
431
 
450
432
  # check for unpaired state events
451
433
  for obs_id in pj[cfg.OBSERVATIONS]:
452
- ok, msg = check_state_events_obs(
453
- obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id], time_format
454
- )
434
+ ok, msg = check_state_events_obs(obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id], time_format)
455
435
  if not ok:
456
436
  out += "<br><br>" if out else ""
457
437
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
@@ -460,10 +440,7 @@ def check_project_integrity(
460
440
  for idx in pj[cfg.ETHOGRAM]:
461
441
  if cfg.BEHAVIOR_CATEGORY in pj[cfg.ETHOGRAM][idx]:
462
442
  if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]:
463
- if (
464
- pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]
465
- not in pj[cfg.BEHAVIORAL_CATEGORIES]
466
- ):
443
+ if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] not in pj[cfg.BEHAVIORAL_CATEGORIES]:
467
444
  out += "<br><br>" if out else ""
468
445
  out += (
469
446
  f"The behavior <b>{pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE]}</b> belongs "
@@ -481,37 +458,40 @@ def check_project_integrity(
481
458
  out += (
482
459
  "The following <b>modifier</b> defined in ethogram "
483
460
  "has leading/trailing spaces or special chars: "
484
- f"<b>{util.replace_leading_trailing_chars(modifier_code.replace, ' ', '&#9608;')}</b>"
461
+ f"<b>{util.replace_leading_trailing_chars(modifier_code, old_char=' ', new_char='&#9608;')}</b>"
485
462
  )
486
463
 
487
464
  # check if all media are available
488
465
  if media_file_available:
489
466
  for obs_id in pj[cfg.OBSERVATIONS]:
490
- ok, msg = check_if_media_available(
491
- pj[cfg.OBSERVATIONS][obs_id], project_file_name
492
- )
467
+ ok, msg = check_if_media_available(pj[cfg.OBSERVATIONS][obs_id], project_file_name)
493
468
  if not ok:
494
469
  out += "<br><br>" if out else ""
495
470
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
496
471
 
497
- # check if media length available
472
+ out_events: str = ""
498
473
  for obs_id in pj[cfg.OBSERVATIONS]:
474
+ # check if timestamp between -2147483647 and 2147483647
475
+ for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
476
+ timestamp = event[cfg.PJ_OBS_FIELDS[pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]][cfg.TIME]]
477
+ if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
478
+ out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
499
479
 
500
- # TODO: add images observations
501
- if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE]:
502
- continue
503
-
480
+ """
481
+ # check if media length available
504
482
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
505
483
  for nplayer in cfg.ALL_PLAYERS:
506
484
  if nplayer in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
507
485
  for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
508
486
  try:
509
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][
510
- media_file
511
- ]
487
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
512
488
  except KeyError:
513
489
  out += "<br><br>" if out else ""
514
490
  out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
491
+ """
492
+
493
+ out += "<br><br>" if out else ""
494
+ out += out_events
515
495
 
516
496
  # check for leading/trailing spaces/special chars in observation id
517
497
  for obs_id in pj[cfg.OBSERVATIONS]:
@@ -524,10 +504,7 @@ def check_project_integrity(
524
504
  )
525
505
 
526
506
  # check independent variables present in observations are defined
527
- defined_var_label = [
528
- pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
529
- for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})
530
- ]
507
+ defined_var_label = [pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})]
531
508
  not_defined: dict = {}
532
509
  for obs_id in pj[cfg.OBSERVATIONS]:
533
510
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
@@ -558,27 +535,87 @@ def check_project_integrity(
558
535
  ]
559
536
  )
560
537
 
561
- out += "<br><br>" if out else ""
538
+ tmp_out: str = ""
562
539
  for obs_id in pj[cfg.OBSERVATIONS]:
563
540
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
564
541
  continue
565
542
  for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
566
543
  if var_label in defined_set_var_label:
567
- if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][
568
- var_label
569
- ] not in defined_set_var_label[var_label].split(","):
570
-
571
- out += (
544
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
545
+ tmp_out += (
572
546
  f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
573
547
  f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
574
548
  )
549
+ if tmp_out:
550
+ out += "<br><br>" if out else ""
551
+ out += tmp_out
552
+
553
+ # check if coded subjects are defined in the subjects list
554
+ tmp_out: str = ""
555
+ subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
556
+ coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
557
+
558
+ for subject in coded_subjects:
559
+ if subject and subject not in subjects_list:
560
+ tmp_out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
561
+ if tmp_out:
562
+ out += "<br><br>" if out else ""
563
+ out += tmp_out
564
+
565
+ # check if media file have info in media_info section of project
566
+ tmp_out: str = ""
567
+ for obs_id in pj[cfg.OBSERVATIONS]:
568
+ for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
569
+ for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
570
+ for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
571
+ if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO].get(info, {}):
572
+ tmp_out += f"Observation <b>{obs_id}</b>:<br>"
573
+ tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
574
+ if tmp_out:
575
+ tmp_out += "<br>You should repick the media file to fix this issue."
576
+ out += "<br><br>" if out else ""
577
+ out += tmp_out
578
+
579
+ # check if the number of coded modifiers correspond to the number of sets of modifier
580
+ obs_results: dict = {}
581
+ for obs_id in pj[cfg.OBSERVATIONS]:
582
+ for event_idx, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
583
+ for idx in pj[cfg.ETHOGRAM]:
584
+ if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == event[cfg.EVENT_BEHAVIOR_FIELD_IDX]:
585
+ break
586
+ else:
587
+ # behavior not defined in ethogram
588
+ continue
589
+
590
+ if (not event[cfg.EVENT_MODIFIER_FIELD_IDX]) and not pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]: # no modifiers
591
+ continue
592
+
593
+ if len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")) != len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]):
594
+ # print("behavior", event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
595
+ # print(f"modifier(s) #{event[cfg.EVENT_MODIFIER_FIELD_IDX]}#", len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")))
596
+ # print(pj[cfg.ETHOGRAM][idx]["code"], pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
597
+ # print()
598
+ if obs_id not in obs_results:
599
+ obs_results[obs_id] = []
600
+
601
+ obs_results[obs_id].append(
602
+ (
603
+ f"Event #{event_idx}: the coded modifiers for {event[cfg.EVENT_BEHAVIOR_FIELD_IDX]} are {len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split('|'))} "
604
+ f"but {len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])} sets were defined in ethogram."
605
+ )
606
+ )
607
+
608
+ if obs_results:
609
+ out += "<br><br>" if out else ""
610
+ for o in obs_results:
611
+ out += f"<br>Observation <b>{o}</b>:<br>"
612
+ out += "<br>".join(obs_results[o])
613
+ out += "<br><br>"
575
614
 
576
615
  return out
577
616
 
578
617
 
579
- def create_subtitles(
580
- pj: dict, selected_observations: list, parameters: dict, export_dir: str
581
- ) -> Tuple[bool, str]:
618
+ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, export_dir: str) -> Tuple[bool, str]:
582
619
  """
583
620
  create subtitles for selected observations, subjects and behaviors
584
621
 
@@ -608,9 +645,9 @@ def create_subtitles(
608
645
  return "", ""
609
646
  else:
610
647
  return (
611
- f"""<font color="{cfg.subtitlesColors[
612
- parameters[cfg.SELECTED_SUBJECTS].index(row['subject']) % len(cfg.subtitlesColors)
613
- ]}">""",
648
+ f"""<font color="{
649
+ cfg.subtitlesColors[parameters[cfg.SELECTED_SUBJECTS].index(row["subject"]) % len(cfg.subtitlesColors)]
650
+ }">""",
614
651
  "</font>",
615
652
  )
616
653
 
@@ -677,20 +714,12 @@ def create_subtitles(
677
714
  modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
678
715
  else:
679
716
  modifiers_str = ""
680
- out += (
681
- "{idx}\n"
682
- "{start} --> {stop}\n"
683
- "{col1}{subject}: {behavior}"
684
- "{modifiers}"
685
- "{col2}\n\n"
686
- ).format(
717
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
687
718
  idx=idx + 1,
688
719
  start=util.seconds2time(row["start"]).replace(".", ","),
689
- stop=util.seconds2time(
690
- row["stop"]
691
- if row["type"] == cfg.STATE
692
- else row["stop"] + cfg.POINT_EVENT_ST_DURATION
693
- ).replace(".", ","),
720
+ stop=util.seconds2time(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION).replace(
721
+ ".", ","
722
+ ),
694
723
  col1=col1,
695
724
  col2=col2,
696
725
  subject=row["subject"],
@@ -698,14 +727,9 @@ def create_subtitles(
698
727
  modifiers=modifiers_str,
699
728
  )
700
729
 
701
- file_name = pl.Path(export_dir) / pl.Path(
702
- util.safeFileName(obs_id)
703
- ).with_suffix(".srt")
730
+ file_name = Path(export_dir) / Path(util.safeFileName(obs_id)).with_suffix(".srt")
704
731
 
705
- if (
706
- mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL)
707
- and file_name.is_file()
708
- ):
732
+ if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
709
733
  mem_command = dialog.MessageDialog(
710
734
  cfg.programName,
711
735
  f"The file {file_name} already exists.",
@@ -730,19 +754,13 @@ def create_subtitles(
730
754
  msg += f"observation: {obs_id}\ngave the following error:\n{str(sys.exc_info()[1])}\n"
731
755
 
732
756
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
733
-
734
757
  for nplayer in cfg.ALL_PLAYERS:
735
758
  if not pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
736
759
  continue
737
760
  init = 0
738
761
  for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
739
762
  try:
740
- end = (
741
- init
742
- + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][
743
- media_file
744
- ]
745
- )
763
+ end = init + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
746
764
  except KeyError:
747
765
  return (
748
766
  False,
@@ -760,12 +778,8 @@ def create_subtitles(
760
778
  "AND behavior in ({}) "
761
779
  "ORDER BY start"
762
780
  ).format(
763
- ",".join(
764
- ["?"] * len(parameters[cfg.SELECTED_SUBJECTS])
765
- ),
766
- ",".join(
767
- ["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])
768
- ),
781
+ ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
782
+ ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
769
783
  ),
770
784
  [
771
785
  obs_id,
@@ -777,7 +791,6 @@ def create_subtitles(
777
791
  )
778
792
 
779
793
  else: # arbitrary 'time interval'
780
-
781
794
  cursor.execute(
782
795
  (
783
796
  "SELECT subject, behavior, type, start, stop, modifiers FROM aggregated_events "
@@ -788,12 +801,8 @@ def create_subtitles(
788
801
  "AND behavior in ({}) "
789
802
  "ORDER BY start"
790
803
  ).format(
791
- ",".join(
792
- ["?"] * len(parameters[cfg.SELECTED_SUBJECTS])
793
- ),
794
- ",".join(
795
- ["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])
796
- ),
804
+ ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
805
+ ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
797
806
  ),
798
807
  [
799
808
  obs_id,
@@ -813,24 +822,11 @@ def create_subtitles(
813
822
  else:
814
823
  modifiers_str = ""
815
824
 
816
- out += (
817
- "{idx}\n"
818
- "{start} --> {stop}\n"
819
- "{col1}{subject}: {behavior}"
820
- "{modifiers}"
821
- "{col2}\n\n"
822
- ).format(
825
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
823
826
  idx=idx + 1,
824
- start=util.seconds2time(row["start"] - init).replace(
825
- ".", ","
826
- ),
827
+ start=util.seconds2time(row["start"] - init).replace(".", ","),
827
828
  stop=util.seconds2time(
828
- (
829
- row["stop"]
830
- if row["type"] == cfg.STATE
831
- else row["stop"] + cfg.POINT_EVENT_ST_DURATION
832
- )
833
- - init
829
+ (row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION) - init
834
830
  ).replace(".", ","),
835
831
  col1=col1,
836
832
  col2=col2,
@@ -838,14 +834,9 @@ def create_subtitles(
838
834
  behavior=row["behavior"],
839
835
  modifiers=modifiers_str,
840
836
  )
841
- file_name = pl.Path(export_dir) / pl.Path(
842
- pl.Path(media_file).stem
843
- ).with_suffix(".srt")
837
+ file_name = Path(export_dir) / Path(Path(media_file).stem).with_suffix(".srt")
844
838
 
845
- if (
846
- mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL)
847
- and file_name.is_file()
848
- ):
839
+ if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
849
840
  mem_command = dialog.MessageDialog(
850
841
  cfg.programName,
851
842
  f"The file {file_name} already exists.",
@@ -873,9 +864,7 @@ def create_subtitles(
873
864
  return flag_ok, msg
874
865
 
875
866
 
876
- def export_observations_list(
877
- pj: dict, selected_observations: list, file_name: str, output_format: str
878
- ) -> bool:
867
+ def export_observations_list(pj: dict, selected_observations: list, file_name: str, output_format: str) -> bool:
879
868
  """
880
869
  create file with a list of selected observations
881
870
 
@@ -905,17 +894,7 @@ def export_observations_list(
905
894
  data.headers.extend(indep_var_header)
906
895
 
907
896
  for obs_id in selected_observations:
908
-
909
- subjects_list = sorted(
910
- list(
911
- set(
912
- [
913
- x[cfg.EVENT_SUBJECT_FIELD_IDX]
914
- for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
915
- ]
916
- )
917
- )
918
- )
897
+ subjects_list = sorted(list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]])))
919
898
  if "" in subjects_list:
920
899
  subjects_list = [cfg.NO_FOCAL_SUBJECT] + subjects_list
921
900
  subjects_list.remove("")
@@ -935,11 +914,7 @@ def export_observations_list(
935
914
  if cfg.INDEPENDENT_VARIABLES in pj[cfg.OBSERVATIONS][obs_id]:
936
915
  for var_label in indep_var_header:
937
916
  if var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
938
- indep_var.append(
939
- pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][
940
- var_label
941
- ]
942
- )
917
+ indep_var.append(pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label])
943
918
  else:
944
919
  indep_var.append("")
945
920
 
@@ -954,13 +929,13 @@ def export_observations_list(
954
929
  + indep_var
955
930
  )
956
931
 
957
- if output_format in ["tsv", "csv", "html"]:
932
+ if output_format in (cfg.TSV_EXT, cfg.CSV_EXT, cfg.HTML_EXT):
958
933
  try:
959
934
  with open(file_name, "wb") as f:
960
935
  f.write(str.encode(data.export(output_format)))
961
936
  except Exception:
962
937
  return False
963
- if output_format in ["ods", "xlsx", "xls"]:
938
+ if output_format in [cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT]:
964
939
  try:
965
940
  with open(file_name, "wb") as f:
966
941
  f.write(data.export(output_format))
@@ -987,15 +962,9 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
987
962
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
988
963
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
989
964
  try:
990
- pl.Path(img_dir).relative_to(pl.Path(project_file_name).parent)
965
+ Path(img_dir).relative_to(Path(project_file_name).parent)
991
966
  except ValueError:
992
- if (
993
- pl.Path(img_dir).is_absolute()
994
- or not (
995
- pl.Path(project_file_name).parent / pl.Path(img_dir)
996
- ).is_dir()
997
- ):
998
-
967
+ if Path(img_dir).is_absolute() or not (Path(project_file_name).parent / Path(img_dir)).is_dir():
999
968
  QMessageBox.critical(
1000
969
  None,
1001
970
  cfg.programName,
@@ -1006,23 +975,11 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1006
975
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1007
976
  for n_player in cfg.ALL_PLAYERS:
1008
977
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1009
- for idx, media_file in enumerate(
1010
- pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]
1011
- ):
978
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1012
979
  try:
1013
- pl.Path(media_file).relative_to(
1014
- pl.Path(project_file_name).parent
1015
- )
980
+ Path(media_file).relative_to(Path(project_file_name).parent)
1016
981
  except ValueError:
1017
-
1018
- if (
1019
- pl.Path(media_file).is_absolute()
1020
- or not (
1021
- pl.Path(project_file_name).parent
1022
- / pl.Path(media_file)
1023
- ).is_file()
1024
- ):
1025
-
982
+ if Path(media_file).is_absolute() or not (Path(project_file_name).parent / Path(media_file)).is_file():
1026
983
  QMessageBox.critical(
1027
984
  None,
1028
985
  cfg.programName,
@@ -1036,25 +993,13 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1036
993
  # set media path and image dir relative to project dir
1037
994
  flag_changed = False
1038
995
  for obs_id in pj[cfg.OBSERVATIONS]:
1039
-
1040
996
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1041
997
  new_dir_list = []
1042
998
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1043
999
  try:
1044
- new_dir_list.append(
1045
- str(
1046
- pl.Path(img_dir).relative_to(
1047
- pl.Path(project_file_name).parent
1048
- )
1049
- )
1050
- )
1000
+ new_dir_list.append(str(Path(img_dir).relative_to(Path(project_file_name).parent)))
1051
1001
  except ValueError:
1052
- if (
1053
- not pl.Path(img_dir).is_absolute()
1054
- and (
1055
- pl.Path(project_file_name).parent / pl.Path(img_dir)
1056
- ).is_dir()
1057
- ):
1002
+ if not Path(img_dir).is_absolute() and (Path(project_file_name).parent / Path(img_dir)).is_dir():
1058
1003
  new_dir_list.append(img_dir)
1059
1004
 
1060
1005
  if pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] != new_dir_list:
@@ -1064,23 +1009,11 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1064
1009
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1065
1010
  for n_player in cfg.ALL_PLAYERS:
1066
1011
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1067
- for idx, media_file in enumerate(
1068
- pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]
1069
- ):
1012
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1070
1013
  try:
1071
- p = str(
1072
- pl.Path(media_file).relative_to(
1073
- pl.Path(project_file_name).parent
1074
- )
1075
- )
1014
+ p = str(Path(media_file).relative_to(Path(project_file_name).parent))
1076
1015
  except ValueError:
1077
- if (
1078
- not pl.Path(media_file).is_absolute()
1079
- and (
1080
- pl.Path(project_file_name).parent
1081
- / pl.Path(media_file)
1082
- ).is_file()
1083
- ):
1016
+ if not Path(media_file).is_absolute() and (Path(project_file_name).parent / Path(media_file)).is_file():
1084
1017
  p = media_file
1085
1018
  if p != media_file:
1086
1019
  flag_changed = True
@@ -1093,27 +1026,15 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1093
1026
  cfg.FPS,
1094
1027
  ]:
1095
1028
  if (
1096
- info
1097
- in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1098
- and media_file
1099
- in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][
1100
- info
1101
- ]
1029
+ info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1030
+ and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
1102
1031
  ):
1103
1032
  # add new file path
1104
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][
1105
- info
1106
- ][p] = pj[cfg.OBSERVATIONS][obs_id][
1107
- cfg.MEDIA_INFO
1108
- ][
1109
- info
1110
- ][
1111
- media_file
1112
- ]
1113
- # remove old path
1114
- del pj[cfg.OBSERVATIONS][obs_id][
1033
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1115
1034
  cfg.MEDIA_INFO
1116
1035
  ][info][media_file]
1036
+ # remove old path
1037
+ del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
1117
1038
  return flag_changed
1118
1039
 
1119
1040
 
@@ -1133,18 +1054,10 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1133
1054
  for _, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
1134
1055
  if cfg.FILE_PATH in v:
1135
1056
  try:
1136
- pl.Path(v[cfg.FILE_PATH]).relative_to(
1137
- pl.Path(project_file_name).parent
1138
- )
1057
+ Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent)
1139
1058
  except ValueError:
1140
1059
  # check if file is in project dir
1141
- if (
1142
- pl.Path(v[cfg.FILE_PATH]).is_absolute()
1143
- or not (
1144
- pl.Path(project_file_name).parent
1145
- / pl.Path(v[cfg.FILE_PATH])
1146
- ).is_file()
1147
- ):
1060
+ if Path(v[cfg.FILE_PATH]).is_absolute() or not (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
1148
1061
  QMessageBox.critical(
1149
1062
  None,
1150
1063
  cfg.programName,
@@ -1158,26 +1071,15 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1158
1071
 
1159
1072
  flag_changed = False
1160
1073
  for obs_id in pj[cfg.OBSERVATIONS]:
1161
-
1162
1074
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
1163
1075
  continue
1164
1076
  for idx, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
1165
1077
  if cfg.FILE_PATH in v:
1166
1078
  try:
1167
- p = str(
1168
- pl.Path(v[cfg.FILE_PATH]).relative_to(
1169
- pl.Path(project_file_name).parent
1170
- )
1171
- )
1079
+ p = str(Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent))
1172
1080
  except ValueError:
1173
1081
  # check if file is in project dir
1174
- if (
1175
- not pl.Path(v[cfg.FILE_PATH]).is_absolute()
1176
- and (
1177
- pl.Path(project_file_name).parent
1178
- / pl.Path(v[cfg.FILE_PATH])
1179
- ).is_file()
1180
- ):
1082
+ if not Path(v[cfg.FILE_PATH]).is_absolute() and (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
1181
1083
  p = v[cfg.FILE_PATH]
1182
1084
 
1183
1085
  if p != v[cfg.FILE_PATH]:
@@ -1199,26 +1101,14 @@ def remove_data_files_path(pj: dict) -> None:
1199
1101
  """
1200
1102
 
1201
1103
  for obs_id in pj[cfg.OBSERVATIONS]:
1202
-
1203
1104
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
1204
1105
  continue
1205
1106
  if cfg.PLOT_DATA in pj[cfg.OBSERVATIONS][obs_id]:
1206
1107
  for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA]:
1207
1108
  if "file_path" in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]:
1208
- p = str(
1209
- pl.Path(
1210
- pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx][
1211
- "file_path"
1212
- ]
1213
- ).name
1214
- )
1215
- if (
1216
- p
1217
- != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]
1218
- ):
1219
- pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx][
1220
- "file_path"
1221
- ] = p
1109
+ p = str(Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
1110
+ if p != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]:
1111
+ pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"] = p
1222
1112
 
1223
1113
 
1224
1114
  def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
@@ -1236,19 +1126,16 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1236
1126
  file_not_found = []
1237
1127
  # check if media and images dir
1238
1128
  for obs_id in pj[cfg.OBSERVATIONS]:
1239
-
1240
1129
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1241
1130
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1242
- if full_path(pl.Path(img_dir).name, project_file_name) == "":
1131
+ if full_path(Path(img_dir).name, project_file_name) == "":
1243
1132
  file_not_found.append(img_dir)
1244
1133
 
1245
1134
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1246
1135
  for n_player in cfg.ALL_PLAYERS:
1247
1136
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1248
- for idx, media_file in enumerate(
1249
- pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]
1250
- ):
1251
- if full_path(pl.Path(media_file).name, project_file_name) == "":
1137
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1138
+ if full_path(Path(media_file).name, project_file_name) == "":
1252
1139
  file_not_found.append(media_file)
1253
1140
 
1254
1141
  file_not_found = set(file_not_found)
@@ -1269,22 +1156,19 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1269
1156
 
1270
1157
  flag_changed = False
1271
1158
  for obs_id in pj[cfg.OBSERVATIONS]:
1272
-
1273
1159
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1274
1160
  new_img_dir_list = []
1275
1161
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1276
- if img_dir != pl.Path(img_dir).name:
1162
+ if img_dir != Path(img_dir).name:
1277
1163
  flag_changed = True
1278
- new_img_dir_list.append(str(pl.Path(img_dir).name))
1164
+ new_img_dir_list.append(str(Path(img_dir).name))
1279
1165
  pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_img_dir_list
1280
1166
 
1281
1167
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1282
1168
  for n_player in cfg.ALL_PLAYERS:
1283
1169
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1284
- for idx, media_file in enumerate(
1285
- pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]
1286
- ):
1287
- p = pl.Path(media_file).name
1170
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1171
+ p = Path(media_file).name
1288
1172
  if p != media_file:
1289
1173
  flag_changed = True
1290
1174
  pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
@@ -1296,27 +1180,15 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1296
1180
  cfg.FPS,
1297
1181
  ]:
1298
1182
  if (
1299
- info
1300
- in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1301
- and media_file
1302
- in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][
1303
- info
1304
- ]
1183
+ info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1184
+ and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
1305
1185
  ):
1306
1186
  # add new file path
1307
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][
1308
- info
1309
- ][p] = pj[cfg.OBSERVATIONS][obs_id][
1310
- cfg.MEDIA_INFO
1311
- ][
1312
- info
1313
- ][
1314
- media_file
1315
- ]
1316
- # remove old path
1317
- del pj[cfg.OBSERVATIONS][obs_id][
1187
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1318
1188
  cfg.MEDIA_INFO
1319
1189
  ][info][media_file]
1190
+ # remove old path
1191
+ del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
1320
1192
 
1321
1193
  return flag_changed
1322
1194
 
@@ -1324,7 +1196,7 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1324
1196
  def full_path(path: str, project_file_name: str) -> str:
1325
1197
  """
1326
1198
  returns the media/data full path or the images directory full path
1327
- add path of BORIS project if media/data with relative path
1199
+ add path of BORIS project if media/data/pictures dir with relative path
1328
1200
 
1329
1201
  Args:
1330
1202
  path (str): file path or images directory path
@@ -1334,12 +1206,12 @@ def full_path(path: str, project_file_name: str) -> str:
1334
1206
  str: full path
1335
1207
  """
1336
1208
 
1337
- source_path = pl.Path(path)
1209
+ source_path = Path(path)
1338
1210
  if source_path.exists():
1339
1211
  return str(source_path)
1340
1212
  else:
1341
1213
  # check relative path (to project path)
1342
- project_path = pl.Path(project_file_name)
1214
+ project_path = Path(project_file_name)
1343
1215
  if (project_path.parent / source_path).exists():
1344
1216
  return str(project_path.parent / source_path)
1345
1217
  else:
@@ -1358,22 +1230,22 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1358
1230
  """
1359
1231
  if not observation[cfg.EVENTS]:
1360
1232
  return (dec("0.0"), dec("0.0"))
1233
+
1361
1234
  if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1235
+ event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
1236
+
1362
1237
  return (
1363
- min(observation[cfg.EVENTS])[
1364
- cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]
1365
- ],
1366
- max(observation[cfg.EVENTS])[
1367
- cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]
1368
- ],
1238
+ min(event_timestamp),
1239
+ max(event_timestamp),
1369
1240
  )
1370
1241
  if observation[cfg.TYPE] == cfg.IMAGES:
1371
- events = [
1372
- x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]]
1373
- for x in observation[cfg.EVENTS]
1374
- ]
1375
-
1376
- return (dec(min(events)), dec(max(events)))
1242
+ events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
1243
+ # test if indexes contain NA
1244
+ try:
1245
+ dec(min(events))
1246
+ return (dec(min(events)), dec(max(events)))
1247
+ except Exception:
1248
+ return (dec("NaN"), dec("NaN"))
1377
1249
 
1378
1250
 
1379
1251
  def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
@@ -1391,7 +1263,6 @@ def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple
1391
1263
 
1392
1264
  events_flagged: list = []
1393
1265
  for idx, event in enumerate(events):
1394
-
1395
1266
  _, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
1396
1267
 
1397
1268
  # check if code is state
@@ -1441,11 +1312,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1441
1312
  observed_subjects = []
1442
1313
 
1443
1314
  # extract events from selected observations
1444
- for events in [
1445
- pj[cfg.OBSERVATIONS][x][cfg.EVENTS]
1446
- for x in pj[cfg.OBSERVATIONS]
1447
- if x in selected_observations
1448
- ]:
1315
+ for events in [pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in pj[cfg.OBSERVATIONS] if x in selected_observations]:
1449
1316
  for event in events:
1450
1317
  observed_subjects.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
1451
1318
 
@@ -1453,7 +1320,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1453
1320
  return list(set(observed_subjects))
1454
1321
 
1455
1322
 
1456
- def open_project_json(projectFileName: str) -> tuple:
1323
+ def open_project_json(project_file_name: str) -> tuple:
1457
1324
  """
1458
1325
  open BORIS project file in json format or GZ compressed json format
1459
1326
 
@@ -1467,37 +1334,37 @@ def open_project_json(projectFileName: str) -> tuple:
1467
1334
  str: message
1468
1335
  """
1469
1336
 
1470
- logging.debug(f"open project: {projectFileName}")
1337
+ logging.debug(f"open_project_json function: {project_file_name}")
1471
1338
 
1472
- projectChanged = False
1473
- msg = ""
1339
+ projectChanged: bool = False
1340
+ msg: str = ""
1474
1341
 
1475
- if not os.path.isfile(projectFileName):
1342
+ if not Path(project_file_name).is_file():
1476
1343
  return (
1477
- projectFileName,
1344
+ project_file_name,
1478
1345
  projectChanged,
1479
- {"error": f"File {projectFileName} not found"},
1346
+ {"error": f"File {project_file_name} not found"},
1480
1347
  msg,
1481
1348
  )
1482
1349
 
1483
1350
  try:
1484
- if projectFileName.endswith(".boris.gz"):
1485
- file_in = gzip.open(projectFileName, mode="rt", encoding="utf-8")
1351
+ if project_file_name.endswith(".boris.gz"):
1352
+ file_in = gzip.open(project_file_name, mode="rt", encoding="utf-8")
1486
1353
  else:
1487
- file_in = open(projectFileName, "r")
1354
+ file_in = open(project_file_name, "r")
1488
1355
  file_content = file_in.read()
1489
1356
  except PermissionError:
1490
1357
  return (
1491
- projectFileName,
1358
+ project_file_name,
1492
1359
  projectChanged,
1493
- {"error": f"File {projectFileName}: Permission denied"},
1360
+ {"error": f"File {project_file_name}: Permission denied"},
1494
1361
  msg,
1495
1362
  )
1496
1363
  except Exception:
1497
1364
  return (
1498
- projectFileName,
1365
+ project_file_name,
1499
1366
  projectChanged,
1500
- {"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"},
1367
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1501
1368
  msg,
1502
1369
  )
1503
1370
 
@@ -1505,16 +1372,16 @@ def open_project_json(projectFileName: str) -> tuple:
1505
1372
  pj = json.loads(file_content)
1506
1373
  except json.decoder.JSONDecodeError:
1507
1374
  return (
1508
- projectFileName,
1375
+ project_file_name,
1509
1376
  projectChanged,
1510
1377
  {"error": "This project file seems corrupted"},
1511
1378
  msg,
1512
1379
  )
1513
1380
  except Exception:
1514
1381
  return (
1515
- projectFileName,
1382
+ project_file_name,
1516
1383
  projectChanged,
1517
- {"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"},
1384
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1518
1385
  msg,
1519
1386
  )
1520
1387
 
@@ -1534,11 +1401,9 @@ def open_project_json(projectFileName: str) -> tuple:
1534
1401
  projectChanged = True
1535
1402
 
1536
1403
  # check if project file version is newer than current BORIS project file version
1537
- if cfg.PROJECT_VERSION in pj and util.versiontuple(
1538
- pj[cfg.PROJECT_VERSION]
1539
- ) > util.versiontuple(version.__version__):
1404
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
1540
1405
  return (
1541
- projectFileName,
1406
+ project_file_name,
1542
1407
  projectChanged,
1543
1408
  {
1544
1409
  "error": (
@@ -1551,13 +1416,11 @@ def open_project_json(projectFileName: str) -> tuple:
1551
1416
 
1552
1417
  # check if old version v. 0 *.obs
1553
1418
  if cfg.PROJECT_VERSION not in pj:
1554
-
1555
1419
  # convert VIDEO, AUDIO -> MEDIA
1556
1420
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1557
1421
  projectChanged = True
1558
1422
 
1559
1423
  for obs in [x for x in pj[cfg.OBSERVATIONS]]:
1560
-
1561
1424
  # remove 'replace audio' key
1562
1425
  if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
1563
1426
  del pj[cfg.OBSERVATIONS][obs]["replace audio"]
@@ -1566,6 +1429,7 @@ def open_project_json(projectFileName: str) -> tuple:
1566
1429
  pj[cfg.OBSERVATIONS][obs][cfg.TYPE] = cfg.MEDIA
1567
1430
 
1568
1431
  # convert old media list in new one
1432
+ d1: dict = {}
1569
1433
  if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]):
1570
1434
  d1 = {cfg.PLAYER1: [pj[cfg.OBSERVATIONS][obs][cfg.FILE][0]]}
1571
1435
 
@@ -1583,15 +1447,13 @@ def open_project_json(projectFileName: str) -> tuple:
1583
1447
  f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
1584
1448
  "Choose a new file name for saving it."
1585
1449
  )
1586
- projectFileName = ""
1450
+ project_file_name = ""
1587
1451
 
1588
1452
  # update modifiers to JSON format
1589
1453
 
1590
1454
  # check if project format version < 4 (modifiers were str)
1591
1455
  project_lowerthan4 = False
1592
- if cfg.PROJECT_VERSION in pj and util.versiontuple(
1593
- pj[cfg.PROJECT_VERSION]
1594
- ) < util.versiontuple("4.0"):
1456
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) < util.versiontuple("4.0"):
1595
1457
  for idx in pj[cfg.ETHOGRAM]:
1596
1458
  if pj[cfg.ETHOGRAM][idx]["modifiers"]:
1597
1459
  if isinstance(pj[cfg.ETHOGRAM][idx]["modifiers"], str):
@@ -1609,40 +1471,46 @@ def open_project_json(projectFileName: str) -> tuple:
1609
1471
  pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
1610
1472
 
1611
1473
  if not project_lowerthan4:
1612
- msg = "The project version was updated from {} to {}".format(
1613
- pj[cfg.PROJECT_VERSION], cfg.project_format_version
1614
- )
1474
+ msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
1615
1475
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1616
1476
  projectChanged = True
1617
1477
 
1478
+ # check if behavioral categories are stored as a list
1479
+ if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
1480
+ if isinstance(pj[cfg.BEHAVIORAL_CATEGORIES_CONF], list):
1481
+ # convert to dict
1482
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {str(idx): {"name": bc} for idx, bc in enumerate(pj[cfg.BEHAVIORAL_CATEGORIES_CONF])}
1483
+ logging.info("Behavioral categories was converted from a list to a dictionary")
1484
+ projectChanged = True
1485
+ else:
1486
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict()
1487
+ projectChanged = True
1488
+
1618
1489
  # add category key if not found
1619
1490
  for idx in pj[cfg.ETHOGRAM]:
1620
- if "category" not in pj[cfg.ETHOGRAM][idx]:
1621
- pj[cfg.ETHOGRAM][idx]["category"] = ""
1491
+ if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
1492
+ pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
1622
1493
 
1623
1494
  # if one file is present in player #1 -> set "media_info" key with value of media_file_info
1624
1495
  for obs in pj[cfg.OBSERVATIONS]:
1625
- if (
1626
- pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in [cfg.MEDIA]
1627
- and cfg.MEDIA_INFO not in pj[cfg.OBSERVATIONS][obs]
1628
- ):
1496
+ if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in [cfg.MEDIA] and cfg.MEDIA_INFO not in pj[cfg.OBSERVATIONS][obs]:
1629
1497
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1630
1498
  cfg.LENGTH: {},
1631
1499
  cfg.FPS: {},
1632
1500
  cfg.HAS_VIDEO: {},
1633
1501
  cfg.HAS_AUDIO: {},
1634
1502
  }
1635
- for player in [cfg.PLAYER1, cfg.PLAYER2]:
1503
+ for player in (cfg.PLAYER1, cfg.PLAYER2):
1636
1504
  # fix bug Anne Maijer 2017-07-17
1637
1505
  if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
1638
1506
  pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
1639
1507
 
1640
1508
  for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
1641
1509
  # FIX: ffmpeg path
1642
- ret, msg = util.check_ffmpeg_path()
1510
+ ret, ffmpeg_bin = util.check_ffmpeg_path()
1643
1511
  if not ret:
1644
1512
  return (
1645
- projectFileName,
1513
+ project_file_name,
1646
1514
  projectChanged,
1647
1515
  {"error": "FFmpeg path not found"},
1648
1516
  "",
@@ -1653,64 +1521,35 @@ def open_project_json(projectFileName: str) -> tuple:
1653
1521
  r = util.accurate_media_analysis(ffmpeg_bin, media_file_path)
1654
1522
 
1655
1523
  if "duration" in r and r["duration"]:
1656
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.LENGTH][
1657
- media_file_path
1658
- ] = float(r["duration"])
1659
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][
1660
- media_file_path
1661
- ] = float(r["fps"])
1662
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][
1663
- media_file_path
1664
- ] = r["has_video"]
1665
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][
1666
- media_file_path
1667
- ] = r["has_audio"]
1524
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.LENGTH][media_file_path] = float(r["duration"])
1525
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
1526
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
1527
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
1668
1528
  projectChanged = True
1669
1529
  else: # file path not found
1670
1530
  if (
1671
1531
  cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
1672
1532
  and len(pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO]) == 1
1673
- and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER1])
1674
- == 1
1675
- and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER2])
1676
- == 0
1533
+ and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER1]) == 1
1534
+ and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER2]) == 0
1677
1535
  ):
1678
- media_md5_key = list(
1679
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO].keys()
1680
- )[0]
1536
+ media_md5_key = list(pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO].keys())[0]
1681
1537
  # duration
1682
1538
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1683
1539
  cfg.LENGTH: {
1684
- media_file_path: pj[cfg.OBSERVATIONS][obs][
1685
- cfg.MEDIA_FILE_INFO
1686
- ][media_md5_key]["video_length"]
1687
- / 1000
1540
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
1688
1541
  }
1689
1542
  }
1690
1543
  projectChanged = True
1691
1544
 
1692
1545
  # FPS
1693
- if (
1694
- "nframe"
1695
- in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][
1696
- media_md5_key
1697
- ]
1698
- ):
1546
+ if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
1699
1547
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
1700
- media_file_path: pj[cfg.OBSERVATIONS][obs][
1701
- cfg.MEDIA_FILE_INFO
1702
- ][media_md5_key]["nframe"]
1703
- / (
1704
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][
1705
- media_md5_key
1706
- ]["video_length"]
1707
- / 1000
1708
- )
1548
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["nframe"]
1549
+ / (pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000)
1709
1550
  }
1710
1551
  else:
1711
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
1712
- media_file_path: 0
1713
- }
1552
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
1714
1553
 
1715
1554
  # update project to v.7 for time offset second player
1716
1555
  project_lowerthan7 = False
@@ -1723,9 +1562,7 @@ def open_project_json(projectFileName: str) -> tuple:
1723
1562
  for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
1724
1563
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
1725
1564
  if pj[cfg.OBSERVATIONS][obs]["time offset second player"]:
1726
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(
1727
- pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1728
- )
1565
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
1729
1566
 
1730
1567
  del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1731
1568
  project_lowerthan7 = True
@@ -1739,49 +1576,49 @@ def open_project_json(projectFileName: str) -> tuple:
1739
1576
  projectChanged = True
1740
1577
 
1741
1578
  if project_lowerthan7:
1742
-
1743
1579
  msg = f"The project was updated to the current project version ({cfg.project_format_version})."
1744
1580
 
1745
1581
  try:
1746
- old_project_file_name = projectFileName.replace(
1747
- ".boris", f".v{pj['project_format_version']}.boris"
1748
- )
1749
- copyfile(projectFileName, old_project_file_name)
1582
+ old_project_file_name = project_file_name.replace(".boris", f".v{pj['project_format_version']}.boris")
1583
+ copyfile(project_file_name, old_project_file_name)
1750
1584
  msg += f"\n\nThe old file project was saved as {old_project_file_name}"
1751
1585
  except Exception:
1752
- pass
1586
+ QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
1753
1587
 
1754
1588
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1755
1589
 
1756
- return projectFileName, projectChanged, pj, msg
1590
+ # sort events by time asc
1591
+ for obs_id in pj[cfg.OBSERVATIONS]:
1592
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
1593
+ # sort events list using the first 3 items (time, subject, behavior)
1594
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
1595
+
1596
+ return project_file_name, projectChanged, pj, msg
1757
1597
 
1758
1598
 
1759
- def event_type(code: str, ethogram: dict) -> str:
1599
+ def event_type(code: str, ethogram: dict) -> str | None:
1760
1600
  """
1761
- returns type of event for code
1601
+ returns type of event for behavior code
1762
1602
 
1763
1603
  Args:
1764
1604
  ethogram (dict); ethogram of project
1765
1605
  code (str): behavior code
1766
1606
 
1767
1607
  Returns:
1768
- str: "STATE EVENT", "POINT EVENT" or None if code not found in ethogram
1608
+ str: behavior type
1769
1609
  """
1770
1610
 
1771
1611
  for idx in ethogram:
1772
1612
  if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
1773
- return ethogram[idx][cfg.TYPE].upper()
1613
+ return ethogram[idx][cfg.TYPE]
1774
1614
  return None
1775
1615
 
1776
1616
 
1777
- def fix_unpaired_state_events(
1778
- ethogram: dict, observation: dict, fix_at_time: dec
1779
- ) -> list:
1617
+ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: dec) -> list:
1780
1618
  """
1781
1619
  fix unpaired state events in observation
1782
1620
 
1783
1621
  Args:
1784
- obsId (str): observation id
1785
1622
  ethogram (dict): ethogram dictionary
1786
1623
  observation (dict): observation dictionary
1787
1624
  fix_at_time (Decimal): time to fix the unpaired events
@@ -1790,31 +1627,23 @@ def fix_unpaired_state_events(
1790
1627
  list: list of events with state events fixed
1791
1628
  """
1792
1629
 
1793
- closing_events_to_add = []
1794
- subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1795
- ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1630
+ closing_events_to_add: list = []
1631
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1632
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1796
1633
 
1797
1634
  for subject in sorted(set(subjects)):
1798
-
1799
- behaviors = [
1800
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1801
- for event in observation[cfg.EVENTS]
1802
- if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1635
+ behaviors: list = [
1636
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1803
1637
  ]
1804
1638
 
1805
1639
  for behavior in sorted(set(behaviors)):
1806
- if (behavior in ethogram_behaviors) and (
1807
- cfg.STATE in event_type(behavior, ethogram).upper()
1808
- ):
1809
-
1640
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1810
1641
  lst, memTime = [], {}
1811
1642
  for event in [
1812
1643
  event
1813
1644
  for event in observation[cfg.EVENTS]
1814
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior
1815
- and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1645
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1816
1646
  ]:
1817
-
1818
1647
  behav_modif = [
1819
1648
  event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1820
1649
  event[cfg.EVENT_MODIFIER_FIELD_IDX],
@@ -1828,19 +1657,78 @@ def fix_unpaired_state_events(
1828
1657
  memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1829
1658
 
1830
1659
  for event in lst:
1660
+ last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1831
1661
 
1832
- last_event_time = max(
1833
- [fix_at_time] + [x[0] for x in closing_events_to_add]
1662
+ closing_events_to_add.append(
1663
+ [
1664
+ last_event_time + dec("0.001"),
1665
+ subject,
1666
+ behavior,
1667
+ event[1], # modifiers
1668
+ "Event automatically added by the fix unpaired state events function",
1669
+ cfg.NA, # frame index
1670
+ ]
1834
1671
  )
1835
1672
 
1673
+ return closing_events_to_add
1674
+
1675
+
1676
+ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -> list:
1677
+ """
1678
+ fix unpaired state events in events list
1679
+
1680
+ Args:
1681
+ ethogram (dict): ethogram dictionary
1682
+ events (list): list of events
1683
+ fix_at_time (Decimal): time to fix the unpaired events
1684
+
1685
+ Returns:
1686
+ list: list of events with state events fixed
1687
+ """
1688
+
1689
+ logging.debug("fix_unpaired_state_events2 function")
1690
+
1691
+ closing_events_to_add: list = []
1692
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
1693
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1694
+
1695
+ for subject in sorted(set(subjects)):
1696
+ behaviors: list = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in events if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject]
1697
+
1698
+ for behavior in sorted(set(behaviors)):
1699
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1700
+ lst: list = []
1701
+ memTime: dict = {}
1702
+ for event in [
1703
+ event
1704
+ for event in events
1705
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1706
+ ]:
1707
+ behav_modif = [
1708
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1709
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
1710
+ ]
1711
+
1712
+ if behav_modif in lst:
1713
+ lst.remove(behav_modif)
1714
+ del memTime[str(behav_modif)]
1715
+ else:
1716
+ lst.append(behav_modif)
1717
+ memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1718
+
1719
+ for event in lst:
1720
+ last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1721
+
1836
1722
  closing_events_to_add.append(
1837
1723
  [
1838
- last_event_time + dec("0.001"),
1724
+ # last_event_time + dec("0.001"),
1725
+ last_event_time,
1839
1726
  subject,
1840
1727
  behavior,
1841
- event[1],
1842
- "",
1843
- ] # modifiers # comment
1728
+ event[1], # modifiers
1729
+ "Event automatically added by the fix unpaired state events function",
1730
+ cfg.NA, # frame index
1731
+ ]
1844
1732
  )
1845
1733
 
1846
1734
  return closing_events_to_add
@@ -1867,8 +1755,11 @@ def explore_project(self) -> None:
1867
1755
  manage double-click on tablewidget of explore project results
1868
1756
  """
1869
1757
  observation_operations.load_observation(self, obs_id, cfg.VIEW)
1870
- self.twEvents.selectRow(event_idx - 1)
1871
- self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
1758
+
1759
+ self.tv_events.selectRow(event_idx - 1)
1760
+ index = self.tv_events.model().index(event_idx - 1, 0)
1761
+ self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
1762
+ # self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
1872
1763
 
1873
1764
  elements_list = ("Subject", "Behavior", "Modifier", "Comment")
1874
1765
  elements = []
@@ -1891,9 +1782,7 @@ def explore_project(self) -> None:
1891
1782
  nb_fields += explore_dlg.elements[element].text() != ""
1892
1783
 
1893
1784
  for obs_id in sorted(self.pj[cfg.OBSERVATIONS]):
1894
- for event_idx, event in enumerate(
1895
- self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
1896
- ):
1785
+ for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
1897
1786
  nb_results = 0
1898
1787
  for text, idx in (
1899
1788
  (explore_dlg.elements["Subject"].text(), cfg.EVENT_SUBJECT_FIELD_IDX),
@@ -1904,14 +1793,8 @@ def explore_project(self) -> None:
1904
1793
  if text:
1905
1794
  if any(
1906
1795
  (
1907
- (
1908
- explore_dlg.elements["Case sensitive"].isChecked()
1909
- and text in event[idx]
1910
- ),
1911
- (
1912
- not explore_dlg.elements["Case sensitive"].isChecked()
1913
- and text.upper() in event[idx].upper()
1914
- ),
1796
+ (explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
1797
+ (not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
1915
1798
  )
1916
1799
  ):
1917
1800
  nb_results += 1
@@ -1928,26 +1811,232 @@ def explore_project(self) -> None:
1928
1811
  txt2 = ""
1929
1812
  for element in elements_list:
1930
1813
  if explore_dlg.elements[element].text():
1931
- txt2 += (
1932
- f"<b>{explore_dlg.elements[element].text()}</b> in {element}<br>"
1933
- )
1814
+ txt2 += f"<b>{explore_dlg.elements[element].text()}</b> in {element}<br>"
1934
1815
  if txt2:
1935
1816
  txt += " for<br>"
1936
1817
  self.results_dialog.lb.setText(txt + txt2)
1937
1818
  self.results_dialog.tw.setColumnCount(2)
1938
1819
  self.results_dialog.tw.setRowCount(len(results))
1939
- self.results_dialog.tw.setHorizontalHeaderLabels(
1940
- ["Observation id", "row index"]
1941
- )
1820
+ self.results_dialog.tw.setHorizontalHeaderLabels(["Observation id", "row index"])
1942
1821
 
1943
1822
  for row, result in enumerate(results):
1944
1823
  for i in range(0, 2):
1945
1824
  self.results_dialog.tw.setItem(row, i, QTableWidgetItem(str(result[i])))
1946
- self.results_dialog.tw.item(row, i).setFlags(
1947
- Qt.ItemIsSelectable | Qt.ItemIsEnabled
1948
- )
1825
+ self.results_dialog.tw.item(row, i).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
1949
1826
 
1950
1827
  self.results_dialog.show()
1951
1828
 
1952
1829
  else:
1953
1830
  QMessageBox.information(self, cfg.programName, "No events found")
1831
+
1832
+
1833
+ def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
1834
+ """
1835
+ returns a pandas dataframe containing observations data
1836
+ """
1837
+ # print(pj.keys())
1838
+
1839
+ # print(pj["independent_variables"])
1840
+
1841
+ # indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
1842
+
1843
+ indep_variables = dict(
1844
+ [(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["type"]) for idx in pj[cfg.INDEPENDENT_VARIABLES]]
1845
+ )
1846
+
1847
+ # print()
1848
+ # print(f"{indep_variables=}")
1849
+
1850
+ # n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
1851
+
1852
+ # behavioral_categories
1853
+ behavioral_category = dict(
1854
+ [(pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE], pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]) for x in pj[cfg.ETHOGRAM]]
1855
+ )
1856
+
1857
+ # print(f"{pj["behaviors_conf"]=}")
1858
+
1859
+ # check all modifiers
1860
+ all_modifier_sets: list = []
1861
+ for behavior_id in pj[cfg.ETHOGRAM]:
1862
+ modifier_names: list = []
1863
+ set_count = 0
1864
+ if pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS] == "":
1865
+ continue
1866
+ for modifier in pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS].values():
1867
+ if modifier["name"]:
1868
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], modifier["name"]))
1869
+ else:
1870
+ set_count += 1
1871
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], f"set #{set_count}"))
1872
+
1873
+ # print(modifier_names)
1874
+ if modifier_names:
1875
+ all_modifier_sets.extend(modifier_names)
1876
+
1877
+ # print()
1878
+ # print(f"{all_modifier_sets=}")
1879
+
1880
+ # create df
1881
+
1882
+ data = {
1883
+ "Observation id": [],
1884
+ "Observation date": [],
1885
+ "Description": [],
1886
+ "Observation type": [],
1887
+ "Observation interval start": [],
1888
+ "Observation interval stop": [],
1889
+ # "Source": [],
1890
+ # "Time offset (s)": [],
1891
+ # "Coding duration": [],
1892
+ # "Media duration (s)": [],
1893
+ # "FPS (frame/s)": [],
1894
+ }
1895
+
1896
+ for indep_var in indep_variables:
1897
+ data[f"independent variable '{indep_var}'"] = []
1898
+
1899
+ data = data | {
1900
+ "Subject": [],
1901
+ "Observation duration by subject by observation": [],
1902
+ "Behavior": [],
1903
+ "Behavioral category": [],
1904
+ }
1905
+
1906
+ for modifier_set in all_modifier_sets:
1907
+ data[modifier_set] = []
1908
+
1909
+ data = data | {
1910
+ "Behavior type": [],
1911
+ "Start (s)": [],
1912
+ "Stop (s)": [],
1913
+ "Duration (s)": [],
1914
+ # "Media file name": [],
1915
+ # "Image index start": [],
1916
+ # "Image index stop": [],
1917
+ # "Image file path start": [],
1918
+ # "Image file path stop": [],
1919
+ "Comment start": [],
1920
+ "Comment stop": [],
1921
+ }
1922
+
1923
+ #
1924
+
1925
+ type_ = {
1926
+ "Observation id": "string",
1927
+ "Observation date": "string",
1928
+ "Description": "string",
1929
+ "Observation type": "string",
1930
+ "Observation interval start": "float64",
1931
+ "Observation interval stop": "float64",
1932
+ # "Source": "string",
1933
+ # "Time offset (s)": "string",
1934
+ # "Coding duration": "float64",
1935
+ # "Media duration (s)": "string",
1936
+ # "FPS (frame/s)": "float64",
1937
+ }
1938
+
1939
+ # TODO: set correct type in base of the var type
1940
+ for indep_var in indep_variables:
1941
+ type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == cfg.NUMERIC else "string"
1942
+
1943
+ type_ = type_ | {
1944
+ "Subject": "string",
1945
+ "Observation duration by subject by observation": "float64",
1946
+ "Behavior": "string",
1947
+ "Behavioral category": "string",
1948
+ }
1949
+
1950
+ for modifer_set in all_modifier_sets:
1951
+ type_[modifer_set] = "string"
1952
+
1953
+ type_ = type_ | {
1954
+ "Behavior type": "string",
1955
+ "Start (s)": "float64",
1956
+ "Stop (s)": "float64",
1957
+ "Duration (s)": "float64",
1958
+ # "Media file name": "string",
1959
+ # "Image index start": "float64",
1960
+ # "Image index stop": "float64",
1961
+ # "Image file path start": "string",
1962
+ # "Image file path stop": "string",
1963
+ "Comment start": "string",
1964
+ "Comment stop": "string",
1965
+ }
1966
+
1967
+ state_behaviors = util.state_behavior_codes(pj[cfg.ETHOGRAM])
1968
+
1969
+ for obs_id in pj[cfg.OBSERVATIONS]:
1970
+ if observations_list and obs_id not in observations_list:
1971
+ continue
1972
+ # print(obs_id)
1973
+ stop_event_idx = set()
1974
+ for idx_event, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
1975
+ if idx_event in stop_event_idx:
1976
+ continue
1977
+ data["Observation id"].append(obs_id)
1978
+ data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
1979
+ data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
1980
+ data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
1981
+
1982
+ data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
1983
+ data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
1984
+
1985
+ # data["Source"].append("")
1986
+ # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
1987
+ # data["Coding duration"].append("")
1988
+ # data["Media duration (s)"].append("")
1989
+ # data["FPS (frame/s)"].append("")
1990
+
1991
+ for indep_var in indep_variables:
1992
+ data[f"independent variable '{indep_var}'"].append(
1993
+ pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES].get(indep_var, None)
1994
+ )
1995
+
1996
+ data["Subject"].append(event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] != "" else cfg.NO_FOCAL_SUBJECT)
1997
+ data["Observation duration by subject by observation"].append(-1)
1998
+ data["Behavior"].append(event[2])
1999
+ data["Behavioral category"].append(behavioral_category[event[2]])
2000
+
2001
+ count_set = 0
2002
+ for modifier_set in all_modifier_sets:
2003
+ if event[2] == modifier_set[0]:
2004
+ try:
2005
+ data[modifier_set].append(event[3].split("|")[count_set])
2006
+ except Exception:
2007
+ return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
2008
+ count_set += 1
2009
+ else:
2010
+ data[modifier_set].append(np.nan)
2011
+
2012
+ data["Behavior type"].append(cfg.STATE_EVENT if event[2] in state_behaviors else cfg.POINT_EVENT)
2013
+ data["Start (s)"].append(float(event[0]))
2014
+ if event[2] in state_behaviors:
2015
+ # search stop
2016
+ # print(f"==> {idx_event=} {event[1:4]=}")
2017
+ for idx_event2, event2 in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx_event + 1 :], start=idx_event + 1):
2018
+ # print(f"{idx_event2=} {event2[1:4]=}")
2019
+ if event2[1:4] == event[1:4]:
2020
+ # print("found")
2021
+ stop_event_idx.add(idx_event2)
2022
+ data["Stop (s)"].append(float(event2[0]))
2023
+ data["Duration (s)"].append(float(event2[0] - event[0]))
2024
+ data["Comment start"].append(event[4])
2025
+ data["Comment stop"].append(event2[4])
2026
+ break
2027
+ else:
2028
+ return f"Some events are not paired in {obs_id}", pd.DataFrame()
2029
+
2030
+ else: # point
2031
+ data["Stop (s)"].append(float(event[0]))
2032
+ data["Duration (s)"].append(np.nan)
2033
+ data["Comment start"].append(event[4])
2034
+ data["Comment stop"].append(event[4])
2035
+
2036
+ # Set the display option to show all rows and columns
2037
+ pd.set_option("display.max_rows", None)
2038
+ pd.set_option("display.max_columns", None)
2039
+
2040
+ pd.DataFrame(data).info()
2041
+
2042
+ return "", pd.DataFrame(data)