boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  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 +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  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 +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -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 +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  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 +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.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:
@@ -73,18 +77,18 @@ def check_observation_exhaustivity(
73
77
  events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = I.empty()
74
78
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
75
79
 
80
+ # state event
76
81
  if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_events_list:
77
82
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]].append(
78
83
  event[cfg.EVENT_TIME_FIELD_IDX]
79
84
  )
80
85
  if len(mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]]) == 2:
81
- events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
82
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
83
- ] |= I.closedopen(
86
+ events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.closedopen(
84
87
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][0],
85
88
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][1],
86
89
  )
87
90
  mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
91
+ # point event
88
92
  else:
89
93
  events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.singleton(
90
94
  event[cfg.EVENT_TIME_FIELD_IDX]
@@ -92,13 +96,13 @@ def check_observation_exhaustivity(
92
96
 
93
97
  if events:
94
98
  # coding duration
95
- obs_theo_dur = max(events)[cfg.EVENT_TIME_FIELD_IDX] - min(events)[cfg.EVENT_TIME_FIELD_IDX]
99
+ event_timestamps = [event[cfg.EVENT_TIME_FIELD_IDX] for event in events]
100
+ obs_theo_dur = max(event_timestamps) - min(event_timestamps)
96
101
  else:
97
102
  obs_theo_dur = dec("0")
98
103
 
99
104
  total_duration = 0
100
105
  for subject in events_interval:
101
-
102
106
  tot_behav_for_subject = I.empty()
103
107
  for behav in events_interval[subject]:
104
108
  tot_behav_for_subject |= events_interval[subject][behav]
@@ -241,6 +245,18 @@ def check_coded_behaviors_in_obs_list(pj: dict, observations_list: list) -> bool
241
245
  return False
242
246
 
243
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
+
244
260
  def check_coded_behaviors(pj: dict) -> set:
245
261
  """
246
262
  check if behaviors coded in events are defined in ethogram for all observations
@@ -249,7 +265,7 @@ def check_coded_behaviors(pj: dict) -> set:
249
265
  pj (dict): project dictionary
250
266
 
251
267
  Returns:
252
- set: behaviors present in observations that are not define in ethogram
268
+ set: behaviors present in observations that are not defined in ethogram
253
269
  """
254
270
 
255
271
  # set of behaviors defined in ethogram
@@ -263,9 +279,7 @@ def check_coded_behaviors(pj: dict) -> set:
263
279
  return set(sorted(behaviors_not_defined))
264
280
 
265
281
 
266
- def check_state_events_obs(
267
- obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS
268
- ) -> Tuple[bool, str]:
282
+ def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS) -> Tuple[bool, str]:
269
283
  """
270
284
  check state events for the observation obsId
271
285
  check if behaviors in observation are defined in ethogram
@@ -281,7 +295,7 @@ def check_state_events_obs(
281
295
  tuple (bool, str): if OK True else False , message
282
296
  """
283
297
 
284
- out = ""
298
+ out: str = ""
285
299
 
286
300
  # check if behaviors are defined as "state event"
287
301
  event_types = {ethogram[idx]["type"] for idx in ethogram}
@@ -289,16 +303,13 @@ def check_state_events_obs(
289
303
  if not event_types or event_types == {"Point event"}:
290
304
  return (True, "No behavior is defined as `State event`")
291
305
 
292
- flagStateEvent = False
293
306
  subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
294
307
  ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
308
+ state_behaviors = set(util.state_behavior_codes(ethogram))
295
309
 
296
310
  for subject in sorted(set(subjects)):
297
-
298
311
  behaviors = [
299
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
300
- for event in observation[cfg.EVENTS]
301
- 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
302
313
  ]
303
314
 
304
315
  for behavior in sorted(set(behaviors)):
@@ -306,32 +317,35 @@ def check_state_events_obs(
306
317
  # return (False, "The behaviour <b>{}</b> is not defined in the ethogram.<br>".format(behavior))
307
318
  continue
308
319
  else:
309
- if cfg.STATE in event_type(behavior, ethogram).upper():
310
- flagStateEvent = True
311
- lst, memTime = [], {}
312
- for event in [
313
- event
314
- for event in observation[cfg.EVENTS]
315
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior
316
- and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
317
- ]:
318
-
319
- behav_modif = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX], event[cfg.EVENT_MODIFIER_FIELD_IDX]]
320
-
321
- if behav_modif in lst:
322
- lst.remove(behav_modif)
323
- del memTime[str(behav_modif)]
324
- else:
325
- lst.append(behav_modif)
326
- memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
327
-
328
- for event in lst:
329
- out += (
330
- f"The behavior <b>{behavior}</b> "
331
- f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
332
- f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
333
- f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
334
- )
320
+ if behavior not in state_behaviors:
321
+ continue
322
+
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
+ )
335
349
 
336
350
  return (False, out) if out else (True, "No problem detected")
337
351
 
@@ -342,6 +356,8 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
342
356
  use check_state_events_obs function
343
357
  """
344
358
 
359
+ logging.info("Check state events function")
360
+
345
361
  out = ""
346
362
  not_paired_obs_list = []
347
363
  for obs_id in observations_list:
@@ -367,21 +383,28 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
367
383
  if not new_observations_list:
368
384
  QMessageBox.warning(None, cfg.programName, "The observation list is empty")
369
385
 
386
+ logging.info("Check state events done")
387
+
370
388
  return False, new_observations_list # no state events are unpaired
371
389
 
372
390
 
373
391
  def check_project_integrity(
374
- pj: dict, time_format: str, project_file_name: str, media_file_available: bool = True
392
+ pj: dict,
393
+ time_format: str,
394
+ project_file_name: str,
395
+ media_file_available: bool = True,
375
396
  ) -> str:
376
397
  """
377
- check project integrity
378
- check if behaviors in observations are in ethogram
379
- check unpaired state events
380
- check if behavior belong to behavioral category that do not more exist
381
- check for leading and trailing spaces and special chars in modifiers
382
- check if media file are available
383
- check if media length available
384
- 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
385
408
 
386
409
  Args:
387
410
  pj (dict): BORIS project
@@ -391,8 +414,12 @@ def check_project_integrity(
391
414
 
392
415
  Returns:
393
416
  str: message
417
+
418
+
419
+ TODO: implement check on order of events (for live and media)
420
+
394
421
  """
395
- out = ""
422
+ out: str = ""
396
423
 
397
424
  # check if coded behaviors are defined in ethogram
398
425
  r = check_coded_behaviors(pj)
@@ -428,7 +455,7 @@ def check_project_integrity(
428
455
  out += (
429
456
  "The following <b>modifier</b> defined in ethogram "
430
457
  "has leading/trailing spaces or special chars: "
431
- 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>"
432
459
  )
433
460
 
434
461
  # check if all media are available
@@ -439,13 +466,16 @@ def check_project_integrity(
439
466
  out += "<br><br>" if out else ""
440
467
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
441
468
 
442
- # check if media length available
469
+ out_events: str = ""
443
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>"
444
476
 
445
- # TODO: add images observations
446
- if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE]:
447
- continue
448
-
477
+ """
478
+ # check if media length available
449
479
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
450
480
  for nplayer in cfg.ALL_PLAYERS:
451
481
  if nplayer in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
@@ -455,6 +485,10 @@ def check_project_integrity(
455
485
  except KeyError:
456
486
  out += "<br><br>" if out else ""
457
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
458
492
 
459
493
  # check for leading/trailing spaces/special chars in observation id
460
494
  for obs_id in pj[cfg.OBSERVATIONS]:
@@ -481,28 +515,98 @@ def check_project_integrity(
481
515
  if not_defined:
482
516
  out += "<br><br>" if out else ""
483
517
  for var_label in not_defined:
484
- out += f"The independent variable <b>{util.replace_leading_trailing_chars(var_label, ' ', '&#9608;')}</b> present in {len(not_defined[var_label])} observation(s) is not defined.<br>"
518
+ out += (
519
+ f"The independent variable <b>{util.replace_leading_trailing_chars(var_label, ' ', '&#9608;')}</b> "
520
+ f"present in {len(not_defined[var_label])} observation(s) is not defined.<br>"
521
+ )
485
522
 
486
523
  # check values of independent variables
487
524
  defined_set_var_label: dict = dict(
488
525
  [
489
- (pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["possible values"])
526
+ (
527
+ pj[cfg.INDEPENDENT_VARIABLES][idx]["label"],
528
+ pj[cfg.INDEPENDENT_VARIABLES][idx]["possible values"],
529
+ )
490
530
  for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})
491
531
  if pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "value from set"
492
532
  ]
493
533
  )
494
534
 
495
- out += "<br><br>" if out else ""
535
+ tmp_out: str = ""
496
536
  for obs_id in pj[cfg.OBSERVATIONS]:
497
537
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
498
538
  continue
499
539
  for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
500
540
  if var_label in defined_set_var_label:
501
- if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[
502
- var_label
503
- ].split(","):
541
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
542
+ tmp_out += (
543
+ f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
544
+ f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
545
+ )
546
+ if tmp_out:
547
+ out += "<br><br>" if out else ""
548
+ out += tmp_out
504
549
 
505
- out += f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
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>"
506
610
 
507
611
  return out
508
612
 
@@ -537,14 +641,17 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
537
641
  return "", ""
538
642
  else:
539
643
  return (
540
- f"""<font color="{cfg.subtitlesColors[
541
- parameters[cfg.SELECTED_SUBJECTS].index(row['subject']) % len(cfg.subtitlesColors)
542
- ]}">""",
644
+ f"""<font color="{
645
+ cfg.subtitlesColors[parameters[cfg.SELECTED_SUBJECTS].index(row["subject"]) % len(cfg.subtitlesColors)]
646
+ }">""",
543
647
  "</font>",
544
648
  )
545
649
 
546
650
  ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
547
- pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
651
+ pj,
652
+ parameters[cfg.SELECTED_SUBJECTS],
653
+ selected_observations,
654
+ parameters[cfg.SELECTED_BEHAVIORS],
548
655
  )
549
656
  if not ok:
550
657
  return False, msg
@@ -588,7 +695,11 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
588
695
  ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
589
696
  ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
590
697
  ),
591
- [obs_id, float(parameters[cfg.START_TIME]), float(parameters[cfg.END_TIME])]
698
+ [
699
+ obs_id,
700
+ float(parameters[cfg.START_TIME]),
701
+ float(parameters[cfg.END_TIME]),
702
+ ]
592
703
  + parameters[cfg.SELECTED_SUBJECTS]
593
704
  + parameters[cfg.SELECTED_BEHAVIORS],
594
705
  )
@@ -599,14 +710,12 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
599
710
  modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
600
711
  else:
601
712
  modifiers_str = ""
602
- out += (
603
- "{idx}\n" "{start} --> {stop}\n" "{col1}{subject}: {behavior}" "{modifiers}" "{col2}\n\n"
604
- ).format(
713
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
605
714
  idx=idx + 1,
606
715
  start=util.seconds2time(row["start"]).replace(".", ","),
607
- stop=util.seconds2time(
608
- row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION
609
- ).replace(".", ","),
716
+ stop=util.seconds2time(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION).replace(
717
+ ".", ","
718
+ ),
610
719
  col1=col1,
611
720
  col2=col2,
612
721
  subject=row["subject"],
@@ -614,13 +723,19 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
614
723
  modifiers=modifiers_str,
615
724
  )
616
725
 
617
- file_name = pl.Path(export_dir) / pl.Path(util.safeFileName(obs_id)).with_suffix(".srt")
726
+ file_name = Path(export_dir) / Path(util.safeFileName(obs_id)).with_suffix(".srt")
618
727
 
619
728
  if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
620
729
  mem_command = dialog.MessageDialog(
621
730
  cfg.programName,
622
731
  f"The file {file_name} already exists.",
623
- [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
732
+ [
733
+ cfg.OVERWRITE,
734
+ cfg.OVERWRITE_ALL,
735
+ cfg.SKIP,
736
+ cfg.SKIP_ALL,
737
+ cfg.CANCEL,
738
+ ],
624
739
  )
625
740
  if mem_command == cfg.CANCEL:
626
741
  return False, ""
@@ -635,7 +750,6 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
635
750
  msg += f"observation: {obs_id}\ngave the following error:\n{str(sys.exc_info()[1])}\n"
636
751
 
637
752
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
638
-
639
753
  for nplayer in cfg.ALL_PLAYERS:
640
754
  if not pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
641
755
  continue
@@ -644,7 +758,10 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
644
758
  try:
645
759
  end = init + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
646
760
  except KeyError:
647
- return False, f"The length for media file {media_file} is not available"
761
+ return (
762
+ False,
763
+ f"The length for media file {media_file} is not available",
764
+ )
648
765
  out = ""
649
766
 
650
767
  if parameters["time"] in (cfg.TIME_EVENTS, cfg.TIME_FULL_OBS):
@@ -670,7 +787,6 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
670
787
  )
671
788
 
672
789
  else: # arbitrary 'time interval'
673
-
674
790
  cursor.execute(
675
791
  (
676
792
  "SELECT subject, behavior, type, start, stop, modifiers FROM aggregated_events "
@@ -684,7 +800,13 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
684
800
  ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
685
801
  ",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
686
802
  ),
687
- [obs_id, init, end, float(parameters[cfg.START_TIME]), float(parameters[cfg.END_TIME])]
803
+ [
804
+ obs_id,
805
+ init,
806
+ end,
807
+ float(parameters[cfg.START_TIME]),
808
+ float(parameters[cfg.END_TIME]),
809
+ ]
688
810
  + parameters[cfg.SELECTED_SUBJECTS]
689
811
  + parameters[cfg.SELECTED_BEHAVIORS],
690
812
  )
@@ -696,14 +818,11 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
696
818
  else:
697
819
  modifiers_str = ""
698
820
 
699
- out += (
700
- "{idx}\n" "{start} --> {stop}\n" "{col1}{subject}: {behavior}" "{modifiers}" "{col2}\n\n"
701
- ).format(
821
+ out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
702
822
  idx=idx + 1,
703
823
  start=util.seconds2time(row["start"] - init).replace(".", ","),
704
824
  stop=util.seconds2time(
705
- (row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION)
706
- - init
825
+ (row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION) - init
707
826
  ).replace(".", ","),
708
827
  col1=col1,
709
828
  col2=col2,
@@ -711,13 +830,19 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
711
830
  behavior=row["behavior"],
712
831
  modifiers=modifiers_str,
713
832
  )
714
- file_name = pl.Path(export_dir) / pl.Path(pl.Path(media_file).stem).with_suffix(".srt")
833
+ file_name = Path(export_dir) / Path(Path(media_file).stem).with_suffix(".srt")
715
834
 
716
835
  if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
717
836
  mem_command = dialog.MessageDialog(
718
837
  cfg.programName,
719
838
  f"The file {file_name} already exists.",
720
- [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
839
+ [
840
+ cfg.OVERWRITE,
841
+ cfg.OVERWRITE_ALL,
842
+ cfg.SKIP,
843
+ cfg.SKIP_ALL,
844
+ cfg.CANCEL,
845
+ ],
721
846
  )
722
847
  if mem_command == cfg.CANCEL:
723
848
  return False, ""
@@ -750,7 +875,13 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
750
875
  """
751
876
 
752
877
  data = tablib.Dataset()
753
- data.headers = ["Observation id", "Date", "Description", "Subjects", "Media files/Live observation"]
878
+ data.headers = [
879
+ "Observation id",
880
+ "Date",
881
+ "Description",
882
+ "Subjects",
883
+ "Media files/Live observation",
884
+ ]
754
885
 
755
886
  indep_var_header = []
756
887
  if cfg.INDEPENDENT_VARIABLES in pj:
@@ -759,10 +890,7 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
759
890
  data.headers.extend(indep_var_header)
760
891
 
761
892
  for obs_id in selected_observations:
762
-
763
- subjects_list = sorted(
764
- list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]]))
765
- )
893
+ subjects_list = sorted(list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]])))
766
894
  if "" in subjects_list:
767
895
  subjects_list = [cfg.NO_FOCAL_SUBJECT] + subjects_list
768
896
  subjects_list.remove("")
@@ -797,13 +925,13 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
797
925
  + indep_var
798
926
  )
799
927
 
800
- if output_format in ["tsv", "csv", "html"]:
928
+ if output_format in (cfg.TSV_EXT, cfg.CSV_EXT, cfg.HTML_EXT):
801
929
  try:
802
930
  with open(file_name, "wb") as f:
803
931
  f.write(str.encode(data.export(output_format)))
804
932
  except Exception:
805
933
  return False
806
- if output_format in ["ods", "xlsx", "xls"]:
934
+ if output_format in [cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT]:
807
935
  try:
808
936
  with open(file_name, "wb") as f:
809
937
  f.write(data.export(output_format))
@@ -830,13 +958,9 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
830
958
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
831
959
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
832
960
  try:
833
- pl.Path(img_dir).relative_to(pl.Path(project_file_name).parent)
961
+ Path(img_dir).relative_to(Path(project_file_name).parent)
834
962
  except ValueError:
835
- if (
836
- pl.Path(img_dir).is_absolute()
837
- or not (pl.Path(project_file_name).parent / pl.Path(img_dir)).is_dir()
838
- ):
839
-
963
+ if Path(img_dir).is_absolute() or not (Path(project_file_name).parent / Path(img_dir)).is_dir():
840
964
  QMessageBox.critical(
841
965
  None,
842
966
  cfg.programName,
@@ -849,35 +973,29 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
849
973
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
850
974
  for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
851
975
  try:
852
- pl.Path(media_file).relative_to(pl.Path(project_file_name).parent)
976
+ Path(media_file).relative_to(Path(project_file_name).parent)
853
977
  except ValueError:
854
-
855
- if (
856
- pl.Path(media_file).is_absolute()
857
- or not (pl.Path(project_file_name).parent / pl.Path(media_file)).is_file()
858
- ):
859
-
978
+ if Path(media_file).is_absolute() or not (Path(project_file_name).parent / Path(media_file)).is_file():
860
979
  QMessageBox.critical(
861
980
  None,
862
981
  cfg.programName,
863
- f"Observation <b>{obs_id}</b>:<br>the path of <b>{media_file}</b> is not relative to <b>{project_file_name}</b>",
982
+ (
983
+ f"Observation <b>{obs_id}</b>:"
984
+ f"<br>the path of <b>{media_file}</b> is not relative to <b>{project_file_name}</b>"
985
+ ),
864
986
  )
865
987
  return False
866
988
 
867
989
  # set media path and image dir relative to project dir
868
990
  flag_changed = False
869
991
  for obs_id in pj[cfg.OBSERVATIONS]:
870
-
871
992
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
872
993
  new_dir_list = []
873
994
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
874
995
  try:
875
- new_dir_list.append(str(pl.Path(img_dir).relative_to(pl.Path(project_file_name).parent)))
996
+ new_dir_list.append(str(Path(img_dir).relative_to(Path(project_file_name).parent)))
876
997
  except ValueError:
877
- if (
878
- not pl.Path(img_dir).is_absolute()
879
- and (pl.Path(project_file_name).parent / pl.Path(img_dir)).is_dir()
880
- ):
998
+ if not Path(img_dir).is_absolute() and (Path(project_file_name).parent / Path(img_dir)).is_dir():
881
999
  new_dir_list.append(img_dir)
882
1000
 
883
1001
  if pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] != new_dir_list:
@@ -889,26 +1007,28 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
889
1007
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
890
1008
  for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
891
1009
  try:
892
- p = str(pl.Path(media_file).relative_to(pl.Path(project_file_name).parent))
1010
+ p = str(Path(media_file).relative_to(Path(project_file_name).parent))
893
1011
  except ValueError:
894
- if (
895
- not pl.Path(media_file).is_absolute()
896
- and (pl.Path(project_file_name).parent / pl.Path(media_file)).is_file()
897
- ):
1012
+ if not Path(media_file).is_absolute() and (Path(project_file_name).parent / Path(media_file)).is_file():
898
1013
  p = media_file
899
1014
  if p != media_file:
900
1015
  flag_changed = True
901
1016
  pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
902
1017
  if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
903
- for info in [cfg.LENGTH, cfg.HAS_AUDIO, cfg.HAS_VIDEO, cfg.FPS]:
1018
+ for info in [
1019
+ cfg.LENGTH,
1020
+ cfg.HAS_AUDIO,
1021
+ cfg.HAS_VIDEO,
1022
+ cfg.FPS,
1023
+ ]:
904
1024
  if (
905
1025
  info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
906
1026
  and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
907
1027
  ):
908
1028
  # add new file path
909
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][
910
- obs_id
911
- ][cfg.MEDIA_INFO][info][media_file]
1029
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1030
+ cfg.MEDIA_INFO
1031
+ ][info][media_file]
912
1032
  # remove old path
913
1033
  del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
914
1034
  return flag_changed
@@ -930,35 +1050,32 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
930
1050
  for _, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
931
1051
  if cfg.FILE_PATH in v:
932
1052
  try:
933
- pl.Path(v[cfg.FILE_PATH]).relative_to(pl.Path(project_file_name).parent)
1053
+ Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent)
934
1054
  except ValueError:
935
1055
  # check if file is in project dir
936
- if (
937
- pl.Path(v[cfg.FILE_PATH]).is_absolute()
938
- or not (pl.Path(project_file_name).parent / pl.Path(v[cfg.FILE_PATH])).is_file()
939
- ):
1056
+ if Path(v[cfg.FILE_PATH]).is_absolute() or not (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
940
1057
  QMessageBox.critical(
941
1058
  None,
942
1059
  cfg.programName,
943
- f"Observation <b>{obs_id}</b>:<br>the path of <b>{v[cfg.FILE_PATH]}</b> is not relative to <b>{project_file_name}</b>.",
1060
+ (
1061
+ f"Observation <b>{obs_id}</b>:"
1062
+ f"<br>the path of <b>{v[cfg.FILE_PATH]}</b> "
1063
+ f"is not relative to <b>{project_file_name}</b>."
1064
+ ),
944
1065
  )
945
1066
  return False
946
1067
 
947
1068
  flag_changed = False
948
1069
  for obs_id in pj[cfg.OBSERVATIONS]:
949
-
950
1070
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
951
1071
  continue
952
1072
  for idx, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
953
1073
  if cfg.FILE_PATH in v:
954
1074
  try:
955
- p = str(pl.Path(v[cfg.FILE_PATH]).relative_to(pl.Path(project_file_name).parent))
1075
+ p = str(Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent))
956
1076
  except ValueError:
957
1077
  # check if file is in project dir
958
- if (
959
- not pl.Path(v[cfg.FILE_PATH]).is_absolute()
960
- and (pl.Path(project_file_name).parent / pl.Path(v[cfg.FILE_PATH])).is_file()
961
- ):
1078
+ if not Path(v[cfg.FILE_PATH]).is_absolute() and (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
962
1079
  p = v[cfg.FILE_PATH]
963
1080
 
964
1081
  if p != v[cfg.FILE_PATH]:
@@ -980,13 +1097,12 @@ def remove_data_files_path(pj: dict) -> None:
980
1097
  """
981
1098
 
982
1099
  for obs_id in pj[cfg.OBSERVATIONS]:
983
-
984
1100
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
985
1101
  continue
986
1102
  if cfg.PLOT_DATA in pj[cfg.OBSERVATIONS][obs_id]:
987
1103
  for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA]:
988
1104
  if "file_path" in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]:
989
- p = str(pl.Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
1105
+ p = str(Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
990
1106
  if p != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]:
991
1107
  pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"] = p
992
1108
 
@@ -1006,17 +1122,16 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1006
1122
  file_not_found = []
1007
1123
  # check if media and images dir
1008
1124
  for obs_id in pj[cfg.OBSERVATIONS]:
1009
-
1010
1125
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1011
1126
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1012
- if full_path(pl.Path(img_dir).name, project_file_name) == "":
1127
+ if full_path(Path(img_dir).name, project_file_name) == "":
1013
1128
  file_not_found.append(img_dir)
1014
1129
 
1015
1130
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1016
1131
  for n_player in cfg.ALL_PLAYERS:
1017
1132
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1018
1133
  for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1019
- if full_path(pl.Path(media_file).name, project_file_name) == "":
1134
+ if full_path(Path(media_file).name, project_file_name) == "":
1020
1135
  file_not_found.append(media_file)
1021
1136
 
1022
1137
  file_not_found = set(file_not_found)
@@ -1037,33 +1152,37 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1037
1152
 
1038
1153
  flag_changed = False
1039
1154
  for obs_id in pj[cfg.OBSERVATIONS]:
1040
-
1041
1155
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1042
1156
  new_img_dir_list = []
1043
1157
  for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
1044
- if img_dir != pl.Path(img_dir).name:
1158
+ if img_dir != Path(img_dir).name:
1045
1159
  flag_changed = True
1046
- new_img_dir_list.append(str(pl.Path(img_dir).name))
1160
+ new_img_dir_list.append(str(Path(img_dir).name))
1047
1161
  pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_img_dir_list
1048
1162
 
1049
1163
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1050
1164
  for n_player in cfg.ALL_PLAYERS:
1051
1165
  if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1052
1166
  for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
1053
- p = pl.Path(media_file).name
1167
+ p = Path(media_file).name
1054
1168
  if p != media_file:
1055
1169
  flag_changed = True
1056
1170
  pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
1057
1171
  if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
1058
- for info in [cfg.LENGTH, cfg.HAS_AUDIO, cfg.HAS_VIDEO, cfg.FPS]:
1172
+ for info in [
1173
+ cfg.LENGTH,
1174
+ cfg.HAS_AUDIO,
1175
+ cfg.HAS_VIDEO,
1176
+ cfg.FPS,
1177
+ ]:
1059
1178
  if (
1060
1179
  info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
1061
1180
  and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
1062
1181
  ):
1063
1182
  # add new file path
1064
- pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][
1065
- obs_id
1066
- ][cfg.MEDIA_INFO][info][media_file]
1183
+ pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
1184
+ cfg.MEDIA_INFO
1185
+ ][info][media_file]
1067
1186
  # remove old path
1068
1187
  del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
1069
1188
 
@@ -1073,7 +1192,7 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
1073
1192
  def full_path(path: str, project_file_name: str) -> str:
1074
1193
  """
1075
1194
  returns the media/data full path or the images directory full path
1076
- 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
1077
1196
 
1078
1197
  Args:
1079
1198
  path (str): file path or images directory path
@@ -1083,12 +1202,12 @@ def full_path(path: str, project_file_name: str) -> str:
1083
1202
  str: full path
1084
1203
  """
1085
1204
 
1086
- source_path = pl.Path(path)
1205
+ source_path = Path(path)
1087
1206
  if source_path.exists():
1088
1207
  return str(source_path)
1089
1208
  else:
1090
1209
  # check relative path (to project path)
1091
- project_path = pl.Path(project_file_name)
1210
+ project_path = Path(project_file_name)
1092
1211
  if (project_path.parent / source_path).exists():
1093
1212
  return str(project_path.parent / source_path)
1094
1213
  else:
@@ -1107,21 +1226,27 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1107
1226
  """
1108
1227
  if not observation[cfg.EVENTS]:
1109
1228
  return (dec("0.0"), dec("0.0"))
1229
+
1110
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
+
1111
1233
  return (
1112
- min(observation[cfg.EVENTS])[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]],
1113
- max(observation[cfg.EVENTS])[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]],
1234
+ min(event_timestamp),
1235
+ max(event_timestamp),
1114
1236
  )
1115
1237
  if observation[cfg.TYPE] == cfg.IMAGES:
1116
1238
  events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
1117
-
1118
- return (dec(min(events)), dec(max(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"))
1119
1245
 
1120
1246
 
1121
- def events_start_stop(ethogram: dict, events: list) -> list:
1247
+ def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
1122
1248
  """
1123
1249
  returns events with status (START/STOP or POINT)
1124
- Take consideration of subject
1125
1250
 
1126
1251
  Args:
1127
1252
  events (list): list of events
@@ -1132,18 +1257,9 @@ def events_start_stop(ethogram: dict, events: list) -> list:
1132
1257
 
1133
1258
  state_events_list = util.state_behavior_codes(ethogram)
1134
1259
 
1135
- events_flagged = []
1260
+ events_flagged: list = []
1136
1261
  for idx, event in enumerate(events):
1137
- """
1138
- time, subject, code, modifier = (
1139
- event[cfg.EVENT_TIME_FIELD_IDX],
1140
- event[cfg.EVENT_SUBJECT_FIELD_IDX],
1141
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1142
- event[cfg.EVENT_MODIFIER_FIELD_IDX],
1143
- )
1144
- """
1145
-
1146
- time, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
1262
+ _, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
1147
1263
 
1148
1264
  # check if code is state
1149
1265
  if code in state_events_list:
@@ -1153,9 +1269,10 @@ def events_start_stop(ethogram: dict, events: list) -> list:
1153
1269
  [
1154
1270
  x[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1155
1271
  for idx1, x in enumerate(events)
1156
- if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == code and idx1 < idx
1157
- # and x[cfg.EVENT_TIME_FIELD_IDX] < time
1158
- and x[cfg.EVENT_SUBJECT_FIELD_IDX] == subject and x[cfg.EVENT_MODIFIER_FIELD_IDX] == modifier
1272
+ if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == code
1273
+ and idx1 < idx
1274
+ and x[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1275
+ and x[cfg.EVENT_MODIFIER_FIELD_IDX] == modifier
1159
1276
  ]
1160
1277
  )
1161
1278
  % 2
@@ -1166,7 +1283,17 @@ def events_start_stop(ethogram: dict, events: list) -> list:
1166
1283
  else:
1167
1284
  flag = cfg.POINT
1168
1285
 
1169
- events_flagged.append(tuple(event) + (flag,))
1286
+ # no frame_index
1287
+ if obs_type == cfg.MEDIA and len(event) == 5:
1288
+ events_flagged.append(
1289
+ tuple(event)
1290
+ + (
1291
+ cfg.NA,
1292
+ flag,
1293
+ )
1294
+ )
1295
+ else:
1296
+ events_flagged.append(tuple(event) + (flag,))
1170
1297
 
1171
1298
  return events_flagged
1172
1299
 
@@ -1189,7 +1316,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1189
1316
  return list(set(observed_subjects))
1190
1317
 
1191
1318
 
1192
- def open_project_json(projectFileName: str) -> tuple:
1319
+ def open_project_json(project_file_name: str) -> tuple:
1193
1320
  """
1194
1321
  open BORIS project file in json format or GZ compressed json format
1195
1322
 
@@ -1203,31 +1330,56 @@ def open_project_json(projectFileName: str) -> tuple:
1203
1330
  str: message
1204
1331
  """
1205
1332
 
1206
- logging.debug(f"open project: {projectFileName}")
1333
+ logging.debug(f"open_project_json function: {project_file_name}")
1207
1334
 
1208
- projectChanged = False
1209
- msg = ""
1335
+ projectChanged: bool = False
1336
+ msg: str = ""
1210
1337
 
1211
- if not os.path.isfile(projectFileName):
1212
- return projectFileName, projectChanged, {"error": f"File {projectFileName} not found"}, msg
1338
+ if not Path(project_file_name).is_file():
1339
+ return (
1340
+ project_file_name,
1341
+ projectChanged,
1342
+ {"error": f"File {project_file_name} not found"},
1343
+ msg,
1344
+ )
1213
1345
 
1214
1346
  try:
1215
- if projectFileName.endswith(".boris.gz"):
1216
- 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")
1217
1349
  else:
1218
- file_in = open(projectFileName, "r")
1350
+ file_in = open(project_file_name, "r")
1219
1351
  file_content = file_in.read()
1220
1352
  except PermissionError:
1221
- return projectFileName, projectChanged, {f"error": f"File {projectFileName}: Permission denied"}, msg
1353
+ return (
1354
+ project_file_name,
1355
+ projectChanged,
1356
+ {"error": f"File {project_file_name}: Permission denied"},
1357
+ msg,
1358
+ )
1222
1359
  except Exception:
1223
- return projectFileName, projectChanged, {f"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"}, msg
1360
+ return (
1361
+ project_file_name,
1362
+ projectChanged,
1363
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1364
+ msg,
1365
+ )
1224
1366
 
1225
1367
  try:
1226
1368
  pj = json.loads(file_content)
1227
1369
  except json.decoder.JSONDecodeError:
1228
- return projectFileName, projectChanged, {"error": "This project file seems corrupted"}, msg
1370
+ return (
1371
+ project_file_name,
1372
+ projectChanged,
1373
+ {"error": "This project file seems corrupted"},
1374
+ msg,
1375
+ )
1229
1376
  except Exception:
1230
- return projectFileName, projectChanged, {f"error": f"Error on file {projectFileName}: {sys.exc_info()[1]}"}, msg
1377
+ return (
1378
+ project_file_name,
1379
+ projectChanged,
1380
+ {"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
1381
+ msg,
1382
+ )
1231
1383
 
1232
1384
  # transform time to decimal
1233
1385
  pj = util.convert_time_to_decimal(pj)
@@ -1245,11 +1397,9 @@ def open_project_json(projectFileName: str) -> tuple:
1245
1397
  projectChanged = True
1246
1398
 
1247
1399
  # check if project file version is newer than current BORIS project file version
1248
- if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(
1249
- version.__version__
1250
- ):
1400
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
1251
1401
  return (
1252
- projectFileName,
1402
+ project_file_name,
1253
1403
  projectChanged,
1254
1404
  {
1255
1405
  "error": (
@@ -1262,13 +1412,11 @@ def open_project_json(projectFileName: str) -> tuple:
1262
1412
 
1263
1413
  # check if old version v. 0 *.obs
1264
1414
  if cfg.PROJECT_VERSION not in pj:
1265
-
1266
1415
  # convert VIDEO, AUDIO -> MEDIA
1267
1416
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1268
1417
  projectChanged = True
1269
1418
 
1270
1419
  for obs in [x for x in pj[cfg.OBSERVATIONS]]:
1271
-
1272
1420
  # remove 'replace audio' key
1273
1421
  if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
1274
1422
  del pj[cfg.OBSERVATIONS][obs]["replace audio"]
@@ -1277,6 +1425,7 @@ def open_project_json(projectFileName: str) -> tuple:
1277
1425
  pj[cfg.OBSERVATIONS][obs][cfg.TYPE] = cfg.MEDIA
1278
1426
 
1279
1427
  # convert old media list in new one
1428
+ d1: dict = {}
1280
1429
  if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]):
1281
1430
  d1 = {cfg.PLAYER1: [pj[cfg.OBSERVATIONS][obs][cfg.FILE][0]]}
1282
1431
 
@@ -1294,7 +1443,7 @@ def open_project_json(projectFileName: str) -> tuple:
1294
1443
  f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
1295
1444
  "Choose a new file name for saving it."
1296
1445
  )
1297
- projectFileName = ""
1446
+ project_file_name = ""
1298
1447
 
1299
1448
  # update modifiers to JSON format
1300
1449
 
@@ -1318,16 +1467,26 @@ def open_project_json(projectFileName: str) -> tuple:
1318
1467
  pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
1319
1468
 
1320
1469
  if not project_lowerthan4:
1321
- msg = "The project version was updated from {} to {}".format(
1322
- pj[cfg.PROJECT_VERSION], cfg.project_format_version
1323
- )
1470
+ msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
1324
1471
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1325
1472
  projectChanged = True
1326
1473
 
1474
+ # check if behavioral categories are stored as a list
1475
+ if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
1476
+ if isinstance(pj[cfg.BEHAVIORAL_CATEGORIES_CONF], list):
1477
+ # convert to dict
1478
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {str(idx): {"name": bc} for idx, bc in enumerate(pj[cfg.BEHAVIORAL_CATEGORIES_CONF])}
1479
+ logging.info("Behavioral categories was converted from a list to a dictionary")
1480
+ projectChanged = True
1481
+ else:
1482
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict()
1483
+ projectChanged = True
1484
+
1485
+
1327
1486
  # add category key if not found
1328
1487
  for idx in pj[cfg.ETHOGRAM]:
1329
- if "category" not in pj[cfg.ETHOGRAM][idx]:
1330
- pj[cfg.ETHOGRAM][idx]["category"] = ""
1488
+ if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
1489
+ pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
1331
1490
 
1332
1491
  # if one file is present in player #1 -> set "media_info" key with value of media_file_info
1333
1492
  for obs in pj[cfg.OBSERVATIONS]:
@@ -1338,16 +1497,21 @@ def open_project_json(projectFileName: str) -> tuple:
1338
1497
  cfg.HAS_VIDEO: {},
1339
1498
  cfg.HAS_AUDIO: {},
1340
1499
  }
1341
- for player in [cfg.PLAYER1, cfg.PLAYER2]:
1500
+ for player in (cfg.PLAYER1, cfg.PLAYER2):
1342
1501
  # fix bug Anne Maijer 2017-07-17
1343
1502
  if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
1344
1503
  pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
1345
1504
 
1346
1505
  for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
1347
1506
  # FIX: ffmpeg path
1348
- ret, msg = util.check_ffmpeg_path()
1507
+ ret, ffmpeg_bin = util.check_ffmpeg_path()
1349
1508
  if not ret:
1350
- return projectFileName, projectChanged, {"error": "FFmpeg path not found"}, ""
1509
+ return (
1510
+ project_file_name,
1511
+ projectChanged,
1512
+ {"error": "FFmpeg path not found"},
1513
+ "",
1514
+ )
1351
1515
  else:
1352
1516
  ffmpeg_bin = msg
1353
1517
 
@@ -1358,7 +1522,7 @@ def open_project_json(projectFileName: str) -> tuple:
1358
1522
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
1359
1523
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
1360
1524
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
1361
- project_updated, projectChanged = True, True
1525
+ projectChanged = True
1362
1526
  else: # file path not found
1363
1527
  if (
1364
1528
  cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
@@ -1370,10 +1534,7 @@ def open_project_json(projectFileName: str) -> tuple:
1370
1534
  # duration
1371
1535
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1372
1536
  cfg.LENGTH: {
1373
- media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
1374
- "video_length"
1375
- ]
1376
- / 1000
1537
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
1377
1538
  }
1378
1539
  }
1379
1540
  projectChanged = True
@@ -1381,13 +1542,8 @@ def open_project_json(projectFileName: str) -> tuple:
1381
1542
  # FPS
1382
1543
  if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
1383
1544
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
1384
- media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
1385
- "nframe"
1386
- ]
1387
- / (
1388
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"]
1389
- / 1000
1390
- )
1545
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["nframe"]
1546
+ / (pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000)
1391
1547
  }
1392
1548
  else:
1393
1549
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
@@ -1403,9 +1559,7 @@ def open_project_json(projectFileName: str) -> tuple:
1403
1559
  for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
1404
1560
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
1405
1561
  if pj[cfg.OBSERVATIONS][obs]["time offset second player"]:
1406
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(
1407
- pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1408
- )
1562
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
1409
1563
 
1410
1564
  del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1411
1565
  project_lowerthan7 = True
@@ -1419,36 +1573,41 @@ def open_project_json(projectFileName: str) -> tuple:
1419
1573
  projectChanged = True
1420
1574
 
1421
1575
  if project_lowerthan7:
1422
-
1423
1576
  msg = f"The project was updated to the current project version ({cfg.project_format_version})."
1424
1577
 
1425
1578
  try:
1426
- old_project_file_name = projectFileName.replace(".boris", f".v{pj['project_format_version']}.boris")
1427
- copyfile(projectFileName, old_project_file_name)
1579
+ old_project_file_name = project_file_name.replace(".boris", f".v{pj['project_format_version']}.boris")
1580
+ copyfile(project_file_name, old_project_file_name)
1428
1581
  msg += f"\n\nThe old file project was saved as {old_project_file_name}"
1429
1582
  except Exception:
1430
- pass
1583
+ QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
1431
1584
 
1432
1585
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1433
1586
 
1434
- return projectFileName, projectChanged, pj, msg
1587
+ # sort events by time asc
1588
+ for obs_id in pj[cfg.OBSERVATIONS]:
1589
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
1590
+ # sort events list using the first 3 items (time, subject, behavior)
1591
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
1592
+
1593
+ return project_file_name, projectChanged, pj, msg
1435
1594
 
1436
1595
 
1437
- def event_type(code: str, ethogram: dict) -> str:
1596
+ def event_type(code: str, ethogram: dict) -> str | None:
1438
1597
  """
1439
- returns type of event for code
1598
+ returns type of event for behavior code
1440
1599
 
1441
1600
  Args:
1442
1601
  ethogram (dict); ethogram of project
1443
1602
  code (str): behavior code
1444
1603
 
1445
1604
  Returns:
1446
- str: "STATE EVENT", "POINT EVENT" or None if code not found in ethogram
1605
+ str: behavior type
1447
1606
  """
1448
1607
 
1449
1608
  for idx in ethogram:
1450
1609
  if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
1451
- return ethogram[idx][cfg.TYPE].upper()
1610
+ return ethogram[idx][cfg.TYPE]
1452
1611
  return None
1453
1612
 
1454
1613
 
@@ -1457,7 +1616,6 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1457
1616
  fix unpaired state events in observation
1458
1617
 
1459
1618
  Args:
1460
- obsId (str): observation id
1461
1619
  ethogram (dict): ethogram dictionary
1462
1620
  observation (dict): observation dictionary
1463
1621
  fix_at_time (Decimal): time to fix the unpaired events
@@ -1466,31 +1624,27 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1466
1624
  list: list of events with state events fixed
1467
1625
  """
1468
1626
 
1469
- out = ""
1470
- closing_events_to_add = []
1471
- flagStateEvent = False
1472
- subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1473
- ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1627
+ closing_events_to_add: list = []
1628
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1629
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1474
1630
 
1475
1631
  for subject in sorted(set(subjects)):
1476
-
1477
- behaviors = [
1478
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1479
- for event in observation[cfg.EVENTS]
1480
- if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1632
+ behaviors: list = [
1633
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1481
1634
  ]
1482
1635
 
1483
1636
  for behavior in sorted(set(behaviors)):
1484
- if (behavior in ethogram_behaviors) and (cfg.STATE in event_type(behavior, ethogram).upper()):
1485
-
1637
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1486
1638
  lst, memTime = [], {}
1487
1639
  for event in [
1488
1640
  event
1489
1641
  for event in observation[cfg.EVENTS]
1490
1642
  if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1491
1643
  ]:
1492
-
1493
- behav_modif = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX], event[cfg.EVENT_MODIFIER_FIELD_IDX]]
1644
+ behav_modif = [
1645
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1646
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
1647
+ ]
1494
1648
 
1495
1649
  if behav_modif in lst:
1496
1650
  lst.remove(behav_modif)
@@ -1500,11 +1654,78 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1500
1654
  memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1501
1655
 
1502
1656
  for event in lst:
1657
+ last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1658
+
1659
+ closing_events_to_add.append(
1660
+ [
1661
+ last_event_time + dec("0.001"),
1662
+ subject,
1663
+ behavior,
1664
+ event[1], # modifiers
1665
+ "Event automatically added by the fix unpaired state events function",
1666
+ cfg.NA, # frame index
1667
+ ]
1668
+ )
1669
+
1670
+ return closing_events_to_add
1671
+
1672
+
1673
+ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -> list:
1674
+ """
1675
+ fix unpaired state events in events list
1503
1676
 
1677
+ Args:
1678
+ ethogram (dict): ethogram dictionary
1679
+ events (list): list of events
1680
+ fix_at_time (Decimal): time to fix the unpaired events
1681
+
1682
+ Returns:
1683
+ list: list of events with state events fixed
1684
+ """
1685
+
1686
+ logging.debug("fix_unpaired_state_events2 function")
1687
+
1688
+ closing_events_to_add: list = []
1689
+ subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
1690
+ ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
1691
+
1692
+ for subject in sorted(set(subjects)):
1693
+ behaviors: list = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in events if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject]
1694
+
1695
+ for behavior in sorted(set(behaviors)):
1696
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1697
+ lst: list = []
1698
+ memTime: dict = {}
1699
+ for event in [
1700
+ event
1701
+ for event in events
1702
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1703
+ ]:
1704
+ behav_modif = [
1705
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
1706
+ event[cfg.EVENT_MODIFIER_FIELD_IDX],
1707
+ ]
1708
+
1709
+ if behav_modif in lst:
1710
+ lst.remove(behav_modif)
1711
+ del memTime[str(behav_modif)]
1712
+ else:
1713
+ lst.append(behav_modif)
1714
+ memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1715
+
1716
+ for event in lst:
1504
1717
  last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1505
1718
 
1506
1719
  closing_events_to_add.append(
1507
- [last_event_time + dec("0.001"), subject, behavior, event[1], ""] # modifiers # comment
1720
+ [
1721
+ # last_event_time + dec("0.001"),
1722
+ last_event_time,
1723
+ subject,
1724
+ behavior,
1725
+ event[1], # modifiers
1726
+ "Event automatically added by the fix unpaired state events function",
1727
+ cfg.NA, # frame index
1728
+ ]
1508
1729
  )
1509
1730
 
1510
1731
  return closing_events_to_add
@@ -1531,8 +1752,11 @@ def explore_project(self) -> None:
1531
1752
  manage double-click on tablewidget of explore project results
1532
1753
  """
1533
1754
  observation_operations.load_observation(self, obs_id, cfg.VIEW)
1534
- self.twEvents.selectRow(event_idx - 1)
1535
- self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
1755
+
1756
+ self.tv_events.selectRow(event_idx - 1)
1757
+ index = self.tv_events.model().index(event_idx - 1, 0)
1758
+ self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
1759
+ # self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
1536
1760
 
1537
1761
  elements_list = ("Subject", "Behavior", "Modifier", "Comment")
1538
1762
  elements = []
@@ -1541,7 +1765,9 @@ def explore_project(self) -> None:
1541
1765
  elements.append(("cb", "Case sensitive", False))
1542
1766
 
1543
1767
  explore_dlg = dialog.Input_dialog(
1544
- label_caption="Search in all observations", elements_list=elements, title="Explore project"
1768
+ label_caption="Search in all observations",
1769
+ elements_list=elements,
1770
+ title="Explore project",
1545
1771
  )
1546
1772
  explore_dlg.pbOK.setText("Find")
1547
1773
  if not explore_dlg.exec_():
@@ -1565,10 +1791,7 @@ def explore_project(self) -> None:
1565
1791
  if any(
1566
1792
  (
1567
1793
  (explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
1568
- (
1569
- not explore_dlg.elements["Case sensitive"].isChecked()
1570
- and text.upper() in event[idx].upper()
1571
- ),
1794
+ (not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
1572
1795
  )
1573
1796
  ):
1574
1797
  nb_results += 1
@@ -1602,3 +1825,215 @@ def explore_project(self) -> None:
1602
1825
 
1603
1826
  else:
1604
1827
  QMessageBox.information(self, cfg.programName, "No events found")
1828
+
1829
+
1830
+ def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
1831
+ """
1832
+ returns a pandas dataframe containing observations data
1833
+ """
1834
+ # print(pj.keys())
1835
+
1836
+ # print(pj["independent_variables"])
1837
+
1838
+ # indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
1839
+
1840
+ indep_variables = dict(
1841
+ [(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["type"]) for idx in pj[cfg.INDEPENDENT_VARIABLES]]
1842
+ )
1843
+
1844
+ # print()
1845
+ # print(f"{indep_variables=}")
1846
+
1847
+ # n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
1848
+
1849
+ # behavioral_categories
1850
+ behavioral_category = dict(
1851
+ [(pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE], pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]) for x in pj[cfg.ETHOGRAM]]
1852
+ )
1853
+
1854
+ # print(f"{pj["behaviors_conf"]=}")
1855
+
1856
+ # check all modifiers
1857
+ all_modifier_sets: list = []
1858
+ for behavior_id in pj[cfg.ETHOGRAM]:
1859
+ modifier_names: list = []
1860
+ set_count = 0
1861
+ if pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS] == "":
1862
+ continue
1863
+ for modifier in pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS].values():
1864
+ if modifier["name"]:
1865
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], modifier["name"]))
1866
+ else:
1867
+ set_count += 1
1868
+ modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], f"set #{set_count}"))
1869
+
1870
+ # print(modifier_names)
1871
+ if modifier_names:
1872
+ all_modifier_sets.extend(modifier_names)
1873
+
1874
+ # print()
1875
+ # print(f"{all_modifier_sets=}")
1876
+
1877
+ # create df
1878
+
1879
+ data = {
1880
+ "Observation id": [],
1881
+ "Observation date": [],
1882
+ "Description": [],
1883
+ "Observation type": [],
1884
+ "Observation interval start": [],
1885
+ "Observation interval stop": [],
1886
+ # "Source": [],
1887
+ # "Time offset (s)": [],
1888
+ # "Coding duration": [],
1889
+ # "Media duration (s)": [],
1890
+ # "FPS (frame/s)": [],
1891
+ }
1892
+
1893
+ for indep_var in indep_variables:
1894
+ data[f"independent variable '{indep_var}'"] = []
1895
+
1896
+ data = data | {
1897
+ "Subject": [],
1898
+ "Observation duration by subject by observation": [],
1899
+ "Behavior": [],
1900
+ "Behavioral category": [],
1901
+ }
1902
+
1903
+ for modifier_set in all_modifier_sets:
1904
+ data[modifier_set] = []
1905
+
1906
+ data = data | {
1907
+ "Behavior type": [],
1908
+ "Start (s)": [],
1909
+ "Stop (s)": [],
1910
+ "Duration (s)": [],
1911
+ # "Media file name": [],
1912
+ # "Image index start": [],
1913
+ # "Image index stop": [],
1914
+ # "Image file path start": [],
1915
+ # "Image file path stop": [],
1916
+ "Comment start": [],
1917
+ "Comment stop": [],
1918
+ }
1919
+
1920
+ #
1921
+
1922
+ type_ = {
1923
+ "Observation id": "string",
1924
+ "Observation date": "string",
1925
+ "Description": "string",
1926
+ "Observation type": "string",
1927
+ "Observation interval start": "float64",
1928
+ "Observation interval stop": "float64",
1929
+ # "Source": "string",
1930
+ # "Time offset (s)": "string",
1931
+ # "Coding duration": "float64",
1932
+ # "Media duration (s)": "string",
1933
+ # "FPS (frame/s)": "float64",
1934
+ }
1935
+
1936
+ # TODO: set correct type in base of the var type
1937
+ for indep_var in indep_variables:
1938
+ type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == cfg.NUMERIC else "string"
1939
+
1940
+ type_ = type_ | {
1941
+ "Subject": "string",
1942
+ "Observation duration by subject by observation": "float64",
1943
+ "Behavior": "string",
1944
+ "Behavioral category": "string",
1945
+ }
1946
+
1947
+ for modifer_set in all_modifier_sets:
1948
+ type_[modifer_set] = "string"
1949
+
1950
+ type_ = type_ | {
1951
+ "Behavior type": "string",
1952
+ "Start (s)": "float64",
1953
+ "Stop (s)": "float64",
1954
+ "Duration (s)": "float64",
1955
+ # "Media file name": "string",
1956
+ # "Image index start": "float64",
1957
+ # "Image index stop": "float64",
1958
+ # "Image file path start": "string",
1959
+ # "Image file path stop": "string",
1960
+ "Comment start": "string",
1961
+ "Comment stop": "string",
1962
+ }
1963
+
1964
+ state_behaviors = util.state_behavior_codes(pj[cfg.ETHOGRAM])
1965
+
1966
+ for obs_id in pj[cfg.OBSERVATIONS]:
1967
+ if observations_list and obs_id not in observations_list:
1968
+ continue
1969
+ # print(obs_id)
1970
+ stop_event_idx = set()
1971
+ for idx_event, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
1972
+ if idx_event in stop_event_idx:
1973
+ continue
1974
+ data["Observation id"].append(obs_id)
1975
+ data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
1976
+ data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
1977
+ data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
1978
+
1979
+ data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
1980
+ data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
1981
+
1982
+ # data["Source"].append("")
1983
+ # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
1984
+ # data["Coding duration"].append("")
1985
+ # data["Media duration (s)"].append("")
1986
+ # data["FPS (frame/s)"].append("")
1987
+
1988
+ for indep_var in indep_variables:
1989
+ data[f"independent variable '{indep_var}'"].append(
1990
+ pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES].get(indep_var, None)
1991
+ )
1992
+
1993
+ data["Subject"].append(event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] != "" else cfg.NO_FOCAL_SUBJECT)
1994
+ data["Observation duration by subject by observation"].append(-1)
1995
+ data["Behavior"].append(event[2])
1996
+ data["Behavioral category"].append(behavioral_category[event[2]])
1997
+
1998
+ count_set = 0
1999
+ for modifier_set in all_modifier_sets:
2000
+ if event[2] == modifier_set[0]:
2001
+ try:
2002
+ data[modifier_set].append(event[3].split("|")[count_set])
2003
+ except Exception:
2004
+ return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
2005
+ count_set += 1
2006
+ else:
2007
+ data[modifier_set].append(np.nan)
2008
+
2009
+ data["Behavior type"].append(cfg.STATE_EVENT if event[2] in state_behaviors else cfg.POINT_EVENT)
2010
+ data["Start (s)"].append(float(event[0]))
2011
+ if event[2] in state_behaviors:
2012
+ # search stop
2013
+ # print(f"==> {idx_event=} {event[1:4]=}")
2014
+ for idx_event2, event2 in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx_event + 1 :], start=idx_event + 1):
2015
+ # print(f"{idx_event2=} {event2[1:4]=}")
2016
+ if event2[1:4] == event[1:4]:
2017
+ # print("found")
2018
+ stop_event_idx.add(idx_event2)
2019
+ data["Stop (s)"].append(float(event2[0]))
2020
+ data["Duration (s)"].append(float(event2[0] - event[0]))
2021
+ data["Comment start"].append(event[4])
2022
+ data["Comment stop"].append(event2[4])
2023
+ break
2024
+ else:
2025
+ return f"Some events are not paired in {obs_id}", pd.DataFrame()
2026
+
2027
+ else: # point
2028
+ data["Stop (s)"].append(float(event[0]))
2029
+ data["Duration (s)"].append(np.nan)
2030
+ data["Comment start"].append(event[4])
2031
+ data["Comment stop"].append(event[4])
2032
+
2033
+ # Set the display option to show all rows and columns
2034
+ pd.set_option("display.max_rows", None)
2035
+ pd.set_option("display.max_columns", None)
2036
+
2037
+ pd.DataFrame(data).info()
2038
+
2039
+ return "", pd.DataFrame(data)