boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.1__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 (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +16 -34
  23. boris/config.py +101 -49
  24. boris/config_file.py +55 -64
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2108 -1275
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +134 -0
  34. boris/dialog.py +461 -242
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +405 -281
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +304 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +127 -36
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +298 -123
  79. boris/preferences_ui.py +664 -225
  80. boris/project.py +293 -270
  81. boris/project_functions.py +610 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +25 -17
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +71 -86
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.1.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.1.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
  123. boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.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,8 +414,12 @@ 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)
@@ -449,9 +428,7 @@ def check_project_integrity(
449
428
 
450
429
  # check for unpaired state events
451
430
  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
- )
431
+ ok, msg = check_state_events_obs(obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id], time_format)
455
432
  if not ok:
456
433
  out += "<br><br>" if out else ""
457
434
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
@@ -460,10 +437,7 @@ def check_project_integrity(
460
437
  for idx in pj[cfg.ETHOGRAM]:
461
438
  if cfg.BEHAVIOR_CATEGORY in pj[cfg.ETHOGRAM][idx]:
462
439
  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
- ):
440
+ if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] not in pj[cfg.BEHAVIORAL_CATEGORIES]:
467
441
  out += "<br><br>" if out else ""
468
442
  out += (
469
443
  f"The behavior <b>{pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE]}</b> belongs "
@@ -481,37 +455,40 @@ def check_project_integrity(
481
455
  out += (
482
456
  "The following <b>modifier</b> defined in ethogram "
483
457
  "has leading/trailing spaces or special chars: "
484
- f"<b>{util.replace_leading_trailing_chars(modifier_code.replace, ' ', '&#9608;')}</b>"
458
+ f"<b>{util.replace_leading_trailing_chars(modifier_code, old_char=' ', new_char='&#9608;')}</b>"
485
459
  )
486
460
 
487
461
  # check if all media are available
488
462
  if media_file_available:
489
463
  for obs_id in pj[cfg.OBSERVATIONS]:
490
- ok, msg = check_if_media_available(
491
- pj[cfg.OBSERVATIONS][obs_id], project_file_name
492
- )
464
+ ok, msg = check_if_media_available(pj[cfg.OBSERVATIONS][obs_id], project_file_name)
493
465
  if not ok:
494
466
  out += "<br><br>" if out else ""
495
467
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
496
468
 
497
- # check if media length available
469
+ out_events: str = ""
498
470
  for obs_id in pj[cfg.OBSERVATIONS]:
471
+ # check if timestamp between -2147483647 and 2147483647
472
+ for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
473
+ timestamp = event[cfg.PJ_OBS_FIELDS[pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]][cfg.TIME]]
474
+ if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
475
+ out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
499
476
 
500
- # TODO: add images observations
501
- if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE]:
502
- continue
503
-
477
+ """
478
+ # check if media length available
504
479
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
505
480
  for nplayer in cfg.ALL_PLAYERS:
506
481
  if nplayer in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
507
482
  for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
508
483
  try:
509
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][
510
- media_file
511
- ]
484
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
512
485
  except KeyError:
513
486
  out += "<br><br>" if out else ""
514
487
  out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
488
+ """
489
+
490
+ out += "<br><br>" if out else ""
491
+ out += out_events
515
492
 
516
493
  # check for leading/trailing spaces/special chars in observation id
517
494
  for obs_id in pj[cfg.OBSERVATIONS]:
@@ -524,10 +501,7 @@ def check_project_integrity(
524
501
  )
525
502
 
526
503
  # 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
- ]
504
+ defined_var_label = [pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})]
531
505
  not_defined: dict = {}
532
506
  for obs_id in pj[cfg.OBSERVATIONS]:
533
507
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
@@ -558,27 +532,86 @@ def check_project_integrity(
558
532
  ]
559
533
  )
560
534
 
561
- out += "<br><br>" if out else ""
535
+ tmp_out: str = ""
562
536
  for obs_id in pj[cfg.OBSERVATIONS]:
563
537
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
564
538
  continue
565
539
  for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
566
540
  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 += (
541
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
542
+ tmp_out += (
572
543
  f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
573
544
  f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
574
545
  )
546
+ if tmp_out:
547
+ out += "<br><br>" if out else ""
548
+ out += tmp_out
549
+
550
+ # check if coded subjects are defined in the subjects list
551
+ tmp_out: str = ""
552
+ subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
553
+ coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
554
+
555
+ for subject in coded_subjects:
556
+ if subject and subject not in subjects_list:
557
+ 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>"
558
+ if tmp_out:
559
+ out += "<br><br>" if out else ""
560
+ out += tmp_out
561
+
562
+ # check if media file have info in media_info section of project
563
+ tmp_out: str = ""
564
+ for obs_id in pj[cfg.OBSERVATIONS]:
565
+ for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
566
+ for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
567
+ for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
568
+ if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO].get(info, {}):
569
+ tmp_out += f"Observation <b>{obs_id}</b>:<br>"
570
+ tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
571
+ if tmp_out:
572
+ tmp_out += "<br>You should repick the media file to fix this issue."
573
+ out += "<br><br>" if out else ""
574
+ out += tmp_out
575
+
576
+ # check if the number of coded modifiers correspond to the number of sets of modifier
577
+ obs_results: dict = {}
578
+ for obs_id in pj[cfg.OBSERVATIONS]:
579
+ for event_idx, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
580
+ # event[2]
581
+ for idx in pj[cfg.ETHOGRAM]:
582
+ if pj[cfg.ETHOGRAM][idx]["code"] == event[2]:
583
+ break
584
+ else:
585
+ raise
586
+ if (not event[3]) and not pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]:
587
+ continue
588
+
589
+ if len(event[3].split("|")) != len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]):
590
+ print("behavior", event[2])
591
+ print(f"modifier(s) #{event[3]}#", len(event[3].split("|")))
592
+ print(pj[cfg.ETHOGRAM][idx]["code"], pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
593
+ print()
594
+ if obs_id not in obs_results:
595
+ obs_results[obs_id] = []
596
+
597
+ obs_results[obs_id].append(
598
+ (
599
+ f"Event #{event_idx}: the coded modifiers for {event[2]} are {len(event[3].split('|'))} "
600
+ f"but {len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])} sets were defined in ethogram."
601
+ )
602
+ )
603
+
604
+ if obs_results:
605
+ out += "<br><br>" if out else ""
606
+ for o in obs_results:
607
+ out += f"<br>Observation <b>{o}</b>:<br>"
608
+ out += "<br>".join(obs_results[o])
609
+ out += "<br><br>"
575
610
 
576
611
  return out
577
612
 
578
613
 
579
- def create_subtitles(
580
- pj: dict, selected_observations: list, parameters: dict, export_dir: str
581
- ) -> Tuple[bool, str]:
614
+ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, export_dir: str) -> Tuple[bool, str]:
582
615
  """
583
616
  create subtitles for selected observations, subjects and behaviors
584
617
 
@@ -608,9 +641,9 @@ def create_subtitles(
608
641
  return "", ""
609
642
  else:
610
643
  return (
611
- f"""<font color="{cfg.subtitlesColors[
612
- parameters[cfg.SELECTED_SUBJECTS].index(row['subject']) % len(cfg.subtitlesColors)
613
- ]}">""",
644
+ f"""<font color="{
645
+ cfg.subtitlesColors[parameters[cfg.SELECTED_SUBJECTS].index(row["subject"]) % len(cfg.subtitlesColors)]
646
+ }">""",
614
647
  "</font>",
615
648
  )
616
649
 
@@ -677,20 +710,12 @@ def create_subtitles(
677
710
  modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
678
711
  else:
679
712
  modifiers_str = ""
680
- out += (
681
- "{idx}\n"
682
- "{start} --> {stop}\n"
683
- "{col1}{subject}: {behavior}"
684
- "{modifiers}"
685
- "{col2}\n\n"
686
- ).format(
713
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
687
714
  idx=idx + 1,
688
715
  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(".", ","),
716
+ stop=util.seconds2time(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION).replace(
717
+ ".", ","
718
+ ),
694
719
  col1=col1,
695
720
  col2=col2,
696
721
  subject=row["subject"],
@@ -698,14 +723,9 @@ def create_subtitles(
698
723
  modifiers=modifiers_str,
699
724
  )
700
725
 
701
- file_name = pl.Path(export_dir) / pl.Path(
702
- util.safeFileName(obs_id)
703
- ).with_suffix(".srt")
726
+ file_name = Path(export_dir) / Path(util.safeFileName(obs_id)).with_suffix(".srt")
704
727
 
705
- if (
706
- mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL)
707
- and file_name.is_file()
708
- ):
728
+ if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
709
729
  mem_command = dialog.MessageDialog(
710
730
  cfg.programName,
711
731
  f"The file {file_name} already exists.",
@@ -730,19 +750,13 @@ def create_subtitles(
730
750
  msg += f"observation: {obs_id}\ngave the following error:\n{str(sys.exc_info()[1])}\n"
731
751
 
732
752
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
733
-
734
753
  for nplayer in cfg.ALL_PLAYERS:
735
754
  if not pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
736
755
  continue
737
756
  init = 0
738
757
  for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
739
758
  try:
740
- end = (
741
- init
742
- + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][
743
- media_file
744
- ]
745
- )
759
+ end = init + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
746
760
  except KeyError:
747
761
  return (
748
762
  False,
@@ -760,12 +774,8 @@ def create_subtitles(
760
774
  "AND behavior in ({}) "
761
775
  "ORDER BY start"
762
776
  ).format(
763
- ",".join(
764
- ["?"] * len(parameters[cfg.SELECTED_SUBJECTS])
765
- ),
766
- ",".join(
767
- ["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])
768
- ),
777
+ ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
778
+ ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
769
779
  ),
770
780
  [
771
781
  obs_id,
@@ -777,7 +787,6 @@ def create_subtitles(
777
787
  )
778
788
 
779
789
  else: # arbitrary 'time interval'
780
-
781
790
  cursor.execute(
782
791
  (
783
792
  "SELECT subject, behavior, type, start, stop, modifiers FROM aggregated_events "
@@ -788,12 +797,8 @@ def create_subtitles(
788
797
  "AND behavior in ({}) "
789
798
  "ORDER BY start"
790
799
  ).format(
791
- ",".join(
792
- ["?"] * len(parameters[cfg.SELECTED_SUBJECTS])
793
- ),
794
- ",".join(
795
- ["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])
796
- ),
800
+ ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
801
+ ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
797
802
  ),
798
803
  [
799
804
  obs_id,
@@ -813,24 +818,11 @@ def create_subtitles(
813
818
  else:
814
819
  modifiers_str = ""
815
820
 
816
- out += (
817
- "{idx}\n"
818
- "{start} --> {stop}\n"
819
- "{col1}{subject}: {behavior}"
820
- "{modifiers}"
821
- "{col2}\n\n"
822
- ).format(
821
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
823
822
  idx=idx + 1,
824
- start=util.seconds2time(row["start"] - init).replace(
825
- ".", ","
826
- ),
823
+ start=util.seconds2time(row["start"] - init).replace(".", ","),
827
824
  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
825
+ (row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION) - init
834
826
  ).replace(".", ","),
835
827
  col1=col1,
836
828
  col2=col2,
@@ -838,14 +830,9 @@ def create_subtitles(
838
830
  behavior=row["behavior"],
839
831
  modifiers=modifiers_str,
840
832
  )
841
- file_name = pl.Path(export_dir) / pl.Path(
842
- pl.Path(media_file).stem
843
- ).with_suffix(".srt")
833
+ file_name = Path(export_dir) / Path(Path(media_file).stem).with_suffix(".srt")
844
834
 
845
- if (
846
- mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL)
847
- and file_name.is_file()
848
- ):
835
+ if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
849
836
  mem_command = dialog.MessageDialog(
850
837
  cfg.programName,
851
838
  f"The file {file_name} already exists.",
@@ -873,9 +860,7 @@ def create_subtitles(
873
860
  return flag_ok, msg
874
861
 
875
862
 
876
- def export_observations_list(
877
- pj: dict, selected_observations: list, file_name: str, output_format: str
878
- ) -> bool:
863
+ def export_observations_list(pj: dict, selected_observations: list, file_name: str, output_format: str) -> bool:
879
864
  """
880
865
  create file with a list of selected observations
881
866
 
@@ -905,17 +890,7 @@ def export_observations_list(
905
890
  data.headers.extend(indep_var_header)
906
891
 
907
892
  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
- )
893
+ subjects_list = sorted(list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]])))
919
894
  if "" in subjects_list:
920
895
  subjects_list = [cfg.NO_FOCAL_SUBJECT] + subjects_list
921
896
  subjects_list.remove("")
@@ -935,11 +910,7 @@ def export_observations_list(
935
910
  if cfg.INDEPENDENT_VARIABLES in pj[cfg.OBSERVATIONS][obs_id]:
936
911
  for var_label in indep_var_header:
937
912
  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
- )
913
+ indep_var.append(pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label])
943
914
  else:
944
915
  indep_var.append("")
945
916
 
@@ -954,13 +925,13 @@ def export_observations_list(
954
925
  + indep_var
955
926
  )
956
927
 
957
- if output_format in ["tsv", "csv", "html"]:
928
+ if output_format in (cfg.TSV_EXT, cfg.CSV_EXT, cfg.HTML_EXT):
958
929
  try:
959
930
  with open(file_name, "wb") as f:
960
931
  f.write(str.encode(data.export(output_format)))
961
932
  except Exception:
962
933
  return False
963
- if output_format in ["ods", "xlsx", "xls"]:
934
+ if output_format in [cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT]:
964
935
  try:
965
936
  with open(file_name, "wb") as f:
966
937
  f.write(data.export(output_format))
@@ -987,15 +958,9 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
987
958
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
988
959
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
989
960
  try:
990
- pl.Path(img_dir).relative_to(pl.Path(project_file_name).parent)
961
+ Path(img_dir).relative_to(Path(project_file_name).parent)
991
962
  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
-
963
+ if Path(img_dir).is_absolute() or not (Path(project_file_name).parent / Path(img_dir)).is_dir():
999
964
  QMessageBox.critical(
1000
965
  None,
1001
966
  cfg.programName,
@@ -1006,23 +971,11 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1006
971
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1007
972
  for n_player in cfg.ALL_PLAYERS:
1008
973
  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
- ):
974
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1012
975
  try:
1013
- pl.Path(media_file).relative_to(
1014
- pl.Path(project_file_name).parent
1015
- )
976
+ Path(media_file).relative_to(Path(project_file_name).parent)
1016
977
  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
-
978
+ if Path(media_file).is_absolute() or not (Path(project_file_name).parent / Path(media_file)).is_file():
1026
979
  QMessageBox.critical(
1027
980
  None,
1028
981
  cfg.programName,
@@ -1036,25 +989,13 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1036
989
  # set media path and image dir relative to project dir
1037
990
  flag_changed = False
1038
991
  for obs_id in pj[cfg.OBSERVATIONS]:
1039
-
1040
992
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1041
993
  new_dir_list = []
1042
994
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1043
995
  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
- )
996
+ new_dir_list.append(str(Path(img_dir).relative_to(Path(project_file_name).parent)))
1051
997
  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
- ):
998
+ if not Path(img_dir).is_absolute() and (Path(project_file_name).parent / Path(img_dir)).is_dir():
1058
999
  new_dir_list.append(img_dir)
1059
1000
 
1060
1001
  if pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] != new_dir_list:
@@ -1064,23 +1005,11 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1064
1005
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1065
1006
  for n_player in cfg.ALL_PLAYERS:
1066
1007
  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
- ):
1008
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1070
1009
  try:
1071
- p = str(
1072
- pl.Path(media_file).relative_to(
1073
- pl.Path(project_file_name).parent
1074
- )
1075
- )
1010
+ p = str(Path(media_file).relative_to(Path(project_file_name).parent))
1076
1011
  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
- ):
1012
+ if not Path(media_file).is_absolute() and (Path(project_file_name).parent / Path(media_file)).is_file():
1084
1013
  p = media_file
1085
1014
  if p != media_file:
1086
1015
  flag_changed = True
@@ -1093,27 +1022,15 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1093
1022
  cfg.FPS,
1094
1023
  ]:
1095
1024
  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
- ]
1025
+ info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1026
+ and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
1102
1027
  ):
1103
1028
  # 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][
1029
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1115
1030
  cfg.MEDIA_INFO
1116
1031
  ][info][media_file]
1032
+ # remove old path
1033
+ del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
1117
1034
  return flag_changed
1118
1035
 
1119
1036
 
@@ -1133,18 +1050,10 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1133
1050
  for _, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
1134
1051
  if cfg.FILE_PATH in v:
1135
1052
  try:
1136
- pl.Path(v[cfg.FILE_PATH]).relative_to(
1137
- pl.Path(project_file_name).parent
1138
- )
1053
+ Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent)
1139
1054
  except ValueError:
1140
1055
  # 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
- ):
1056
+ if Path(v[cfg.FILE_PATH]).is_absolute() or not (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
1148
1057
  QMessageBox.critical(
1149
1058
  None,
1150
1059
  cfg.programName,
@@ -1158,26 +1067,15 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
1158
1067
 
1159
1068
  flag_changed = False
1160
1069
  for obs_id in pj[cfg.OBSERVATIONS]:
1161
-
1162
1070
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
1163
1071
  continue
1164
1072
  for idx, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
1165
1073
  if cfg.FILE_PATH in v:
1166
1074
  try:
1167
- p = str(
1168
- pl.Path(v[cfg.FILE_PATH]).relative_to(
1169
- pl.Path(project_file_name).parent
1170
- )
1171
- )
1075
+ p = str(Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent))
1172
1076
  except ValueError:
1173
1077
  # 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
- ):
1078
+ if not Path(v[cfg.FILE_PATH]).is_absolute() and (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
1181
1079
  p = v[cfg.FILE_PATH]
1182
1080
 
1183
1081
  if p != v[cfg.FILE_PATH]:
@@ -1199,26 +1097,14 @@ def remove_data_files_path(pj: dict) -> None:
1199
1097
  """
1200
1098
 
1201
1099
  for obs_id in pj[cfg.OBSERVATIONS]:
1202
-
1203
1100
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
1204
1101
  continue
1205
1102
  if cfg.PLOT_DATA in pj[cfg.OBSERVATIONS][obs_id]:
1206
1103
  for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA]:
1207
1104
  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
1105
+ p = str(Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
1106
+ if p != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]:
1107
+ pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"] = p
1222
1108
 
1223
1109
 
1224
1110
  def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
@@ -1236,19 +1122,16 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1236
1122
  file_not_found = []
1237
1123
  # check if media and images dir
1238
1124
  for obs_id in pj[cfg.OBSERVATIONS]:
1239
-
1240
1125
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1241
1126
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1242
- if full_path(pl.Path(img_dir).name, project_file_name) == "":
1127
+ if full_path(Path(img_dir).name, project_file_name) == "":
1243
1128
  file_not_found.append(img_dir)
1244
1129
 
1245
1130
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1246
1131
  for n_player in cfg.ALL_PLAYERS:
1247
1132
  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) == "":
1133
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1134
+ if full_path(Path(media_file).name, project_file_name) == "":
1252
1135
  file_not_found.append(media_file)
1253
1136
 
1254
1137
  file_not_found = set(file_not_found)
@@ -1269,22 +1152,19 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1269
1152
 
1270
1153
  flag_changed = False
1271
1154
  for obs_id in pj[cfg.OBSERVATIONS]:
1272
-
1273
1155
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1274
1156
  new_img_dir_list = []
1275
1157
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1276
- if img_dir != pl.Path(img_dir).name:
1158
+ if img_dir != Path(img_dir).name:
1277
1159
  flag_changed = True
1278
- new_img_dir_list.append(str(pl.Path(img_dir).name))
1160
+ new_img_dir_list.append(str(Path(img_dir).name))
1279
1161
  pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_img_dir_list
1280
1162
 
1281
1163
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1282
1164
  for n_player in cfg.ALL_PLAYERS:
1283
1165
  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
1166
+ for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1167
+ p = Path(media_file).name
1288
1168
  if p != media_file:
1289
1169
  flag_changed = True
1290
1170
  pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
@@ -1296,27 +1176,15 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1296
1176
  cfg.FPS,
1297
1177
  ]:
1298
1178
  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
- ]
1179
+ info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1180
+ and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
1305
1181
  ):
1306
1182
  # 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][
1183
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1318
1184
  cfg.MEDIA_INFO
1319
1185
  ][info][media_file]
1186
+ # remove old path
1187
+ del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
1320
1188
 
1321
1189
  return flag_changed
1322
1190
 
@@ -1324,7 +1192,7 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1324
1192
  def full_path(path: str, project_file_name: str) -> str:
1325
1193
  """
1326
1194
  returns the media/data full path or the images directory full path
1327
- add path of BORIS project if media/data with relative path
1195
+ add path of BORIS project if media/data/pictures dir with relative path
1328
1196
 
1329
1197
  Args:
1330
1198
  path (str): file path or images directory path
@@ -1334,12 +1202,12 @@ def full_path(path: str, project_file_name: str) -> str:
1334
1202
  str: full path
1335
1203
  """
1336
1204
 
1337
- source_path = pl.Path(path)
1205
+ source_path = Path(path)
1338
1206
  if source_path.exists():
1339
1207
  return str(source_path)
1340
1208
  else:
1341
1209
  # check relative path (to project path)
1342
- project_path = pl.Path(project_file_name)
1210
+ project_path = Path(project_file_name)
1343
1211
  if (project_path.parent / source_path).exists():
1344
1212
  return str(project_path.parent / source_path)
1345
1213
  else:
@@ -1358,22 +1226,22 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1358
1226
  """
1359
1227
  if not observation[cfg.EVENTS]:
1360
1228
  return (dec("0.0"), dec("0.0"))
1229
+
1361
1230
  if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1231
+ event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
1232
+
1362
1233
  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
- ],
1234
+ min(event_timestamp),
1235
+ max(event_timestamp),
1369
1236
  )
1370
1237
  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)))
1238
+ events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
1239
+ # test if indexes contain NA
1240
+ try:
1241
+ dec(min(events))
1242
+ return (dec(min(events)), dec(max(events)))
1243
+ except Exception:
1244
+ return (dec("NaN"), dec("NaN"))
1377
1245
 
1378
1246
 
1379
1247
  def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
@@ -1391,7 +1259,6 @@ def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple
1391
1259
 
1392
1260
  events_flagged: list = []
1393
1261
  for idx, event in enumerate(events):
1394
-
1395
1262
  _, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
1396
1263
 
1397
1264
  # check if code is state
@@ -1441,11 +1308,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1441
1308
  observed_subjects = []
1442
1309
 
1443
1310
  # 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
- ]:
1311
+ for events in [pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in pj[cfg.OBSERVATIONS] if x in selected_observations]:
1449
1312
  for event in events:
1450
1313
  observed_subjects.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
1451
1314
 
@@ -1453,7 +1316,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1453
1316
  return list(set(observed_subjects))
1454
1317
 
1455
1318
 
1456
- def open_project_json(projectFileName: str) -> tuple:
1319
+ def open_project_json(project_file_name: str) -> tuple:
1457
1320
  """
1458
1321
  open BORIS project file in json format or GZ compressed json format
1459
1322
 
@@ -1467,37 +1330,37 @@ def open_project_json(projectFileName: str) -> tuple:
1467
1330
  str: message
1468
1331
  """
1469
1332
 
1470
- logging.debug(f"open project: {projectFileName}")
1333
+ logging.debug(f"open_project_json function: {project_file_name}")
1471
1334
 
1472
- projectChanged = False
1473
- msg = ""
1335
+ projectChanged: bool = False
1336
+ msg: str = ""
1474
1337
 
1475
- if not os.path.isfile(projectFileName):
1338
+ if not Path(project_file_name).is_file():
1476
1339
  return (
1477
- projectFileName,
1340
+ project_file_name,
1478
1341
  projectChanged,
1479
- {"error": f"File {projectFileName} not found"},
1342
+ {"error": f"File {project_file_name} not found"},
1480
1343
  msg,
1481
1344
  )
1482
1345
 
1483
1346
  try:
1484
- if projectFileName.endswith(".boris.gz"):
1485
- file_in = gzip.open(projectFileName, mode="rt", encoding="utf-8")
1347
+ if project_file_name.endswith(".boris.gz"):
1348
+ file_in = gzip.open(project_file_name, mode="rt", encoding="utf-8")
1486
1349
  else:
1487
- file_in = open(projectFileName, "r")
1350
+ file_in = open(project_file_name, "r")
1488
1351
  file_content = file_in.read()
1489
1352
  except PermissionError:
1490
1353
  return (
1491
- projectFileName,
1354
+ project_file_name,
1492
1355
  projectChanged,
1493
- {"error": f"File {projectFileName}: Permission denied"},
1356
+ {"error": f"File {project_file_name}: Permission denied"},
1494
1357
  msg,
1495
1358
  )
1496
1359
  except Exception:
1497
1360
  return (
1498
- projectFileName,
1361
+ project_file_name,
1499
1362
  projectChanged,
1500
- {"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"},
1363
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1501
1364
  msg,
1502
1365
  )
1503
1366
 
@@ -1505,16 +1368,16 @@ def open_project_json(projectFileName: str) -> tuple:
1505
1368
  pj = json.loads(file_content)
1506
1369
  except json.decoder.JSONDecodeError:
1507
1370
  return (
1508
- projectFileName,
1371
+ project_file_name,
1509
1372
  projectChanged,
1510
1373
  {"error": "This project file seems corrupted"},
1511
1374
  msg,
1512
1375
  )
1513
1376
  except Exception:
1514
1377
  return (
1515
- projectFileName,
1378
+ project_file_name,
1516
1379
  projectChanged,
1517
- {"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"},
1380
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1518
1381
  msg,
1519
1382
  )
1520
1383
 
@@ -1534,11 +1397,9 @@ def open_project_json(projectFileName: str) -> tuple:
1534
1397
  projectChanged = True
1535
1398
 
1536
1399
  # 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__):
1400
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
1540
1401
  return (
1541
- projectFileName,
1402
+ project_file_name,
1542
1403
  projectChanged,
1543
1404
  {
1544
1405
  "error": (
@@ -1551,13 +1412,11 @@ def open_project_json(projectFileName: str) -> tuple:
1551
1412
 
1552
1413
  # check if old version v. 0 *.obs
1553
1414
  if cfg.PROJECT_VERSION not in pj:
1554
-
1555
1415
  # convert VIDEO, AUDIO -> MEDIA
1556
1416
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1557
1417
  projectChanged = True
1558
1418
 
1559
1419
  for obs in [x for x in pj[cfg.OBSERVATIONS]]:
1560
-
1561
1420
  # remove 'replace audio' key
1562
1421
  if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
1563
1422
  del pj[cfg.OBSERVATIONS][obs]["replace audio"]
@@ -1583,15 +1442,13 @@ def open_project_json(projectFileName: str) -> tuple:
1583
1442
  f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
1584
1443
  "Choose a new file name for saving it."
1585
1444
  )
1586
- projectFileName = ""
1445
+ project_file_name = ""
1587
1446
 
1588
1447
  # update modifiers to JSON format
1589
1448
 
1590
1449
  # check if project format version < 4 (modifiers were str)
1591
1450
  project_lowerthan4 = False
1592
- if cfg.PROJECT_VERSION in pj and util.versiontuple(
1593
- pj[cfg.PROJECT_VERSION]
1594
- ) < util.versiontuple("4.0"):
1451
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) < util.versiontuple("4.0"):
1595
1452
  for idx in pj[cfg.ETHOGRAM]:
1596
1453
  if pj[cfg.ETHOGRAM][idx]["modifiers"]:
1597
1454
  if isinstance(pj[cfg.ETHOGRAM][idx]["modifiers"], str):
@@ -1609,40 +1466,35 @@ def open_project_json(projectFileName: str) -> tuple:
1609
1466
  pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
1610
1467
 
1611
1468
  if not project_lowerthan4:
1612
- msg = "The project version was updated from {} to {}".format(
1613
- pj[cfg.PROJECT_VERSION], cfg.project_format_version
1614
- )
1469
+ msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
1615
1470
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1616
1471
  projectChanged = True
1617
1472
 
1618
1473
  # add category key if not found
1619
1474
  for idx in pj[cfg.ETHOGRAM]:
1620
- if "category" not in pj[cfg.ETHOGRAM][idx]:
1621
- pj[cfg.ETHOGRAM][idx]["category"] = ""
1475
+ if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
1476
+ pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
1622
1477
 
1623
1478
  # if one file is present in player #1 -> set "media_info" key with value of media_file_info
1624
1479
  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
- ):
1480
+ if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in [cfg.MEDIA] and cfg.MEDIA_INFO not in pj[cfg.OBSERVATIONS][obs]:
1629
1481
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1630
1482
  cfg.LENGTH: {},
1631
1483
  cfg.FPS: {},
1632
1484
  cfg.HAS_VIDEO: {},
1633
1485
  cfg.HAS_AUDIO: {},
1634
1486
  }
1635
- for player in [cfg.PLAYER1, cfg.PLAYER2]:
1487
+ for player in (cfg.PLAYER1, cfg.PLAYER2):
1636
1488
  # fix bug Anne Maijer 2017-07-17
1637
1489
  if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
1638
1490
  pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
1639
1491
 
1640
1492
  for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
1641
1493
  # FIX: ffmpeg path
1642
- ret, msg = util.check_ffmpeg_path()
1494
+ ret, ffmpeg_bin = util.check_ffmpeg_path()
1643
1495
  if not ret:
1644
1496
  return (
1645
- projectFileName,
1497
+ project_file_name,
1646
1498
  projectChanged,
1647
1499
  {"error": "FFmpeg path not found"},
1648
1500
  "",
@@ -1653,64 +1505,35 @@ def open_project_json(projectFileName: str) -> tuple:
1653
1505
  r = util.accurate_media_analysis(ffmpeg_bin, media_file_path)
1654
1506
 
1655
1507
  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"]
1508
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.LENGTH][media_file_path] = float(r["duration"])
1509
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
1510
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
1511
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
1668
1512
  projectChanged = True
1669
1513
  else: # file path not found
1670
1514
  if (
1671
1515
  cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
1672
1516
  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
1517
+ and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER1]) == 1
1518
+ and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER2]) == 0
1677
1519
  ):
1678
- media_md5_key = list(
1679
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO].keys()
1680
- )[0]
1520
+ media_md5_key = list(pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO].keys())[0]
1681
1521
  # duration
1682
1522
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1683
1523
  cfg.LENGTH: {
1684
- media_file_path: pj[cfg.OBSERVATIONS][obs][
1685
- cfg.MEDIA_FILE_INFO
1686
- ][media_md5_key]["video_length"]
1687
- / 1000
1524
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
1688
1525
  }
1689
1526
  }
1690
1527
  projectChanged = True
1691
1528
 
1692
1529
  # FPS
1693
- if (
1694
- "nframe"
1695
- in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][
1696
- media_md5_key
1697
- ]
1698
- ):
1530
+ if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
1699
1531
  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
- )
1532
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["nframe"]
1533
+ / (pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000)
1709
1534
  }
1710
1535
  else:
1711
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
1712
- media_file_path: 0
1713
- }
1536
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
1714
1537
 
1715
1538
  # update project to v.7 for time offset second player
1716
1539
  project_lowerthan7 = False
@@ -1723,9 +1546,7 @@ def open_project_json(projectFileName: str) -> tuple:
1723
1546
  for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
1724
1547
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
1725
1548
  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
- )
1549
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
1729
1550
 
1730
1551
  del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1731
1552
  project_lowerthan7 = True
@@ -1739,49 +1560,49 @@ def open_project_json(projectFileName: str) -> tuple:
1739
1560
  projectChanged = True
1740
1561
 
1741
1562
  if project_lowerthan7:
1742
-
1743
1563
  msg = f"The project was updated to the current project version ({cfg.project_format_version})."
1744
1564
 
1745
1565
  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)
1566
+ old_project_file_name = project_file_name.replace(".boris", f".v{pj['project_format_version']}.boris")
1567
+ copyfile(project_file_name, old_project_file_name)
1750
1568
  msg += f"\n\nThe old file project was saved as {old_project_file_name}"
1751
1569
  except Exception:
1752
- pass
1570
+ QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
1753
1571
 
1754
1572
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1755
1573
 
1756
- return projectFileName, projectChanged, pj, msg
1574
+ # sort events by time asc
1575
+ for obs_id in pj[cfg.OBSERVATIONS]:
1576
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
1577
+ # sort events list using the first 3 items (time, subject, behavior)
1578
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
1579
+
1580
+ return project_file_name, projectChanged, pj, msg
1757
1581
 
1758
1582
 
1759
- def event_type(code: str, ethogram: dict) -> str:
1583
+ def event_type(code: str, ethogram: dict) -> str | None:
1760
1584
  """
1761
- returns type of event for code
1585
+ returns type of event for behavior code
1762
1586
 
1763
1587
  Args:
1764
1588
  ethogram (dict); ethogram of project
1765
1589
  code (str): behavior code
1766
1590
 
1767
1591
  Returns:
1768
- str: "STATE EVENT", "POINT EVENT" or None if code not found in ethogram
1592
+ str: behavior type
1769
1593
  """
1770
1594
 
1771
1595
  for idx in ethogram:
1772
1596
  if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
1773
- return ethogram[idx][cfg.TYPE].upper()
1597
+ return ethogram[idx][cfg.TYPE]
1774
1598
  return None
1775
1599
 
1776
1600
 
1777
- def fix_unpaired_state_events(
1778
- ethogram: dict, observation: dict, fix_at_time: dec
1779
- ) -> list:
1601
+ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: dec) -> list:
1780
1602
  """
1781
1603
  fix unpaired state events in observation
1782
1604
 
1783
1605
  Args:
1784
- obsId (str): observation id
1785
1606
  ethogram (dict): ethogram dictionary
1786
1607
  observation (dict): observation dictionary
1787
1608
  fix_at_time (Decimal): time to fix the unpaired events
@@ -1790,31 +1611,23 @@ def fix_unpaired_state_events(
1790
1611
  list: list of events with state events fixed
1791
1612
  """
1792
1613
 
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}
1614
+ closing_events_to_add: list = []
1615
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1616
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1796
1617
 
1797
1618
  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
1619
+ behaviors: list = [
1620
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1803
1621
  ]
1804
1622
 
1805
1623
  for behavior in sorted(set(behaviors)):
1806
- if (behavior in ethogram_behaviors) and (
1807
- cfg.STATE in event_type(behavior, ethogram).upper()
1808
- ):
1809
-
1624
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1810
1625
  lst, memTime = [], {}
1811
1626
  for event in [
1812
1627
  event
1813
1628
  for event in observation[cfg.EVENTS]
1814
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior
1815
- and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1629
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1816
1630
  ]:
1817
-
1818
1631
  behav_modif = [
1819
1632
  event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1820
1633
  event[cfg.EVENT_MODIFIER_FIELD_IDX],
@@ -1828,19 +1641,78 @@ def fix_unpaired_state_events(
1828
1641
  memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1829
1642
 
1830
1643
  for event in lst:
1644
+ last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1831
1645
 
1832
- last_event_time = max(
1833
- [fix_at_time] + [x[0] for x in closing_events_to_add]
1646
+ closing_events_to_add.append(
1647
+ [
1648
+ last_event_time + dec("0.001"),
1649
+ subject,
1650
+ behavior,
1651
+ event[1], # modifiers
1652
+ "Event automatically added by the fix unpaired state events function",
1653
+ cfg.NA, # frame index
1654
+ ]
1834
1655
  )
1835
1656
 
1657
+ return closing_events_to_add
1658
+
1659
+
1660
+ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -> list:
1661
+ """
1662
+ fix unpaired state events in events list
1663
+
1664
+ Args:
1665
+ ethogram (dict): ethogram dictionary
1666
+ events (list): list of events
1667
+ fix_at_time (Decimal): time to fix the unpaired events
1668
+
1669
+ Returns:
1670
+ list: list of events with state events fixed
1671
+ """
1672
+
1673
+ logging.debug("fix_unpaired_state_events2 function")
1674
+
1675
+ closing_events_to_add: list = []
1676
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
1677
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1678
+
1679
+ for subject in sorted(set(subjects)):
1680
+ behaviors: list = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in events if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject]
1681
+
1682
+ for behavior in sorted(set(behaviors)):
1683
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1684
+ lst: list = []
1685
+ memTime: dict = {}
1686
+ for event in [
1687
+ event
1688
+ for event in events
1689
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1690
+ ]:
1691
+ behav_modif = [
1692
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1693
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
1694
+ ]
1695
+
1696
+ if behav_modif in lst:
1697
+ lst.remove(behav_modif)
1698
+ del memTime[str(behav_modif)]
1699
+ else:
1700
+ lst.append(behav_modif)
1701
+ memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1702
+
1703
+ for event in lst:
1704
+ last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1705
+
1836
1706
  closing_events_to_add.append(
1837
1707
  [
1838
- last_event_time + dec("0.001"),
1708
+ # last_event_time + dec("0.001"),
1709
+ last_event_time,
1839
1710
  subject,
1840
1711
  behavior,
1841
- event[1],
1842
- "",
1843
- ] # modifiers # comment
1712
+ event[1], # modifiers
1713
+ "Event automatically added by the fix unpaired state events function",
1714
+ cfg.NA, # frame index
1715
+ ]
1844
1716
  )
1845
1717
 
1846
1718
  return closing_events_to_add
@@ -1867,8 +1739,11 @@ def explore_project(self) -> None:
1867
1739
  manage double-click on tablewidget of explore project results
1868
1740
  """
1869
1741
  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))
1742
+
1743
+ self.tv_events.selectRow(event_idx - 1)
1744
+ index = self.tv_events.model().index(event_idx - 1, 0)
1745
+ self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
1746
+ # self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
1872
1747
 
1873
1748
  elements_list = ("Subject", "Behavior", "Modifier", "Comment")
1874
1749
  elements = []
@@ -1891,9 +1766,7 @@ def explore_project(self) -> None:
1891
1766
  nb_fields += explore_dlg.elements[element].text() != ""
1892
1767
 
1893
1768
  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
- ):
1769
+ for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
1897
1770
  nb_results = 0
1898
1771
  for text, idx in (
1899
1772
  (explore_dlg.elements["Subject"].text(), cfg.EVENT_SUBJECT_FIELD_IDX),
@@ -1904,14 +1777,8 @@ def explore_project(self) -> None:
1904
1777
  if text:
1905
1778
  if any(
1906
1779
  (
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
- ),
1780
+ (explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
1781
+ (not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
1915
1782
  )
1916
1783
  ):
1917
1784
  nb_results += 1
@@ -1928,26 +1795,232 @@ def explore_project(self) -> None:
1928
1795
  txt2 = ""
1929
1796
  for element in elements_list:
1930
1797
  if explore_dlg.elements[element].text():
1931
- txt2 += (
1932
- f"<b>{explore_dlg.elements[element].text()}</b> in {element}<br>"
1933
- )
1798
+ txt2 += f"<b>{explore_dlg.elements[element].text()}</b> in {element}<br>"
1934
1799
  if txt2:
1935
1800
  txt += " for<br>"
1936
1801
  self.results_dialog.lb.setText(txt + txt2)
1937
1802
  self.results_dialog.tw.setColumnCount(2)
1938
1803
  self.results_dialog.tw.setRowCount(len(results))
1939
- self.results_dialog.tw.setHorizontalHeaderLabels(
1940
- ["Observation id", "row index"]
1941
- )
1804
+ self.results_dialog.tw.setHorizontalHeaderLabels(["Observation id", "row index"])
1942
1805
 
1943
1806
  for row, result in enumerate(results):
1944
1807
  for i in range(0, 2):
1945
1808
  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
- )
1809
+ self.results_dialog.tw.item(row, i).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
1949
1810
 
1950
1811
  self.results_dialog.show()
1951
1812
 
1952
1813
  else:
1953
1814
  QMessageBox.information(self, cfg.programName, "No events found")
1815
+
1816
+
1817
+ def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
1818
+ """
1819
+ returns a pandas dataframe containing observations data
1820
+ """
1821
+ # print(pj.keys())
1822
+
1823
+ # print(pj["independent_variables"])
1824
+
1825
+ # indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
1826
+
1827
+ indep_variables = dict(
1828
+ [(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["type"]) for idx in pj[cfg.INDEPENDENT_VARIABLES]]
1829
+ )
1830
+
1831
+ # print()
1832
+ # print(f"{indep_variables=}")
1833
+
1834
+ # n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
1835
+
1836
+ # behavioral_categories
1837
+ behavioral_category = dict(
1838
+ [(pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE], pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]) for x in pj[cfg.ETHOGRAM]]
1839
+ )
1840
+
1841
+ # print(f"{pj["behaviors_conf"]=}")
1842
+
1843
+ # check all modifiers
1844
+ all_modifier_sets: list = []
1845
+ for behavior_id in pj[cfg.ETHOGRAM]:
1846
+ modifier_names: list = []
1847
+ set_count = 0
1848
+ if pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS] == "":
1849
+ continue
1850
+ for modifier in pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS].values():
1851
+ if modifier["name"]:
1852
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], modifier["name"]))
1853
+ else:
1854
+ set_count += 1
1855
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], f"set #{set_count}"))
1856
+
1857
+ # print(modifier_names)
1858
+ if modifier_names:
1859
+ all_modifier_sets.extend(modifier_names)
1860
+
1861
+ # print()
1862
+ # print(f"{all_modifier_sets=}")
1863
+
1864
+ # create df
1865
+
1866
+ data = {
1867
+ "Observation id": [],
1868
+ "Observation date": [],
1869
+ "Description": [],
1870
+ "Observation type": [],
1871
+ "Observation interval start": [],
1872
+ "Observation interval stop": [],
1873
+ # "Source": [],
1874
+ # "Time offset (s)": [],
1875
+ # "Coding duration": [],
1876
+ # "Media duration (s)": [],
1877
+ # "FPS (frame/s)": [],
1878
+ }
1879
+
1880
+ for indep_var in indep_variables:
1881
+ data[f"independent variable '{indep_var}'"] = []
1882
+
1883
+ data = data | {
1884
+ "Subject": [],
1885
+ "Observation duration by subject by observation": [],
1886
+ "Behavior": [],
1887
+ "Behavioral category": [],
1888
+ }
1889
+
1890
+ for modifier_set in all_modifier_sets:
1891
+ data[modifier_set] = []
1892
+
1893
+ data = data | {
1894
+ "Behavior type": [],
1895
+ "Start (s)": [],
1896
+ "Stop (s)": [],
1897
+ "Duration (s)": [],
1898
+ # "Media file name": [],
1899
+ # "Image index start": [],
1900
+ # "Image index stop": [],
1901
+ # "Image file path start": [],
1902
+ # "Image file path stop": [],
1903
+ "Comment start": [],
1904
+ "Comment stop": [],
1905
+ }
1906
+
1907
+ #
1908
+
1909
+ type_ = {
1910
+ "Observation id": "string",
1911
+ "Observation date": "string",
1912
+ "Description": "string",
1913
+ "Observation type": "string",
1914
+ "Observation interval start": "float64",
1915
+ "Observation interval stop": "float64",
1916
+ # "Source": "string",
1917
+ # "Time offset (s)": "string",
1918
+ # "Coding duration": "float64",
1919
+ # "Media duration (s)": "string",
1920
+ # "FPS (frame/s)": "float64",
1921
+ }
1922
+
1923
+ # TODO: set correct type in base of the var type
1924
+ for indep_var in indep_variables:
1925
+ type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == cfg.NUMERIC else "string"
1926
+
1927
+ type_ = type_ | {
1928
+ "Subject": "string",
1929
+ "Observation duration by subject by observation": "float64",
1930
+ "Behavior": "string",
1931
+ "Behavioral category": "string",
1932
+ }
1933
+
1934
+ for modifer_set in all_modifier_sets:
1935
+ type_[modifer_set] = "string"
1936
+
1937
+ type_ = type_ | {
1938
+ "Behavior type": "string",
1939
+ "Start (s)": "float64",
1940
+ "Stop (s)": "float64",
1941
+ "Duration (s)": "float64",
1942
+ # "Media file name": "string",
1943
+ # "Image index start": "float64",
1944
+ # "Image index stop": "float64",
1945
+ # "Image file path start": "string",
1946
+ # "Image file path stop": "string",
1947
+ "Comment start": "string",
1948
+ "Comment stop": "string",
1949
+ }
1950
+
1951
+ state_behaviors = util.state_behavior_codes(pj[cfg.ETHOGRAM])
1952
+
1953
+ for obs_id in pj[cfg.OBSERVATIONS]:
1954
+ if observations_list and obs_id not in observations_list:
1955
+ continue
1956
+ # print(obs_id)
1957
+ stop_event_idx = set()
1958
+ for idx_event, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
1959
+ if idx_event in stop_event_idx:
1960
+ continue
1961
+ data["Observation id"].append(obs_id)
1962
+ data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
1963
+ data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
1964
+ data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
1965
+
1966
+ data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
1967
+ data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
1968
+
1969
+ # data["Source"].append("")
1970
+ # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
1971
+ # data["Coding duration"].append("")
1972
+ # data["Media duration (s)"].append("")
1973
+ # data["FPS (frame/s)"].append("")
1974
+
1975
+ for indep_var in indep_variables:
1976
+ data[f"independent variable '{indep_var}'"].append(
1977
+ pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES].get(indep_var, None)
1978
+ )
1979
+
1980
+ data["Subject"].append(event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] != "" else cfg.NO_FOCAL_SUBJECT)
1981
+ data["Observation duration by subject by observation"].append(-1)
1982
+ data["Behavior"].append(event[2])
1983
+ data["Behavioral category"].append(behavioral_category[event[2]])
1984
+
1985
+ count_set = 0
1986
+ for modifier_set in all_modifier_sets:
1987
+ if event[2] == modifier_set[0]:
1988
+ try:
1989
+ data[modifier_set].append(event[3].split("|")[count_set])
1990
+ except Exception:
1991
+ return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
1992
+ count_set += 1
1993
+ else:
1994
+ data[modifier_set].append(np.nan)
1995
+
1996
+ data["Behavior type"].append(cfg.STATE_EVENT if event[2] in state_behaviors else cfg.POINT_EVENT)
1997
+ data["Start (s)"].append(float(event[0]))
1998
+ if event[2] in state_behaviors:
1999
+ # search stop
2000
+ # print(f"==> {idx_event=} {event[1:4]=}")
2001
+ for idx_event2, event2 in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx_event + 1 :], start=idx_event + 1):
2002
+ # print(f"{idx_event2=} {event2[1:4]=}")
2003
+ if event2[1:4] == event[1:4]:
2004
+ # print("found")
2005
+ stop_event_idx.add(idx_event2)
2006
+ data["Stop (s)"].append(float(event2[0]))
2007
+ data["Duration (s)"].append(float(event2[0] - event[0]))
2008
+ data["Comment start"].append(event[4])
2009
+ data["Comment stop"].append(event2[4])
2010
+ break
2011
+ else:
2012
+ return f"Some events are not paired in {obs_id}", pd.DataFrame()
2013
+
2014
+ else: # point
2015
+ data["Stop (s)"].append(float(event[0]))
2016
+ data["Duration (s)"].append(np.nan)
2017
+ data["Comment start"].append(event[4])
2018
+ data["Comment stop"].append(event[4])
2019
+
2020
+ # Set the display option to show all rows and columns
2021
+ pd.set_option("display.max_rows", None)
2022
+ pd.set_option("display.max_columns", None)
2023
+
2024
+ pd.DataFrame(data).info()
2025
+
2026
+ return "", pd.DataFrame(data)