boris-behav-obs 8.12__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -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 +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  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 +408 -293
  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 +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  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 +325 -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 +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 +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  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 +25 -33
  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 +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  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 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  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 +627 -236
  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 +95 -29
  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.12.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/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.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
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
504
561
 
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>"
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,15 +1226,22 @@ 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
1247
  def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
@@ -1133,7 +1259,6 @@ def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple
1133
1259
 
1134
1260
  events_flagged: list = []
1135
1261
  for idx, event in enumerate(events):
1136
-
1137
1262
  _, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
1138
1263
 
1139
1264
  # check if code is state
@@ -1191,7 +1316,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
1191
1316
  return list(set(observed_subjects))
1192
1317
 
1193
1318
 
1194
- def open_project_json(projectFileName: str) -> tuple:
1319
+ def open_project_json(project_file_name: str) -> tuple:
1195
1320
  """
1196
1321
  open BORIS project file in json format or GZ compressed json format
1197
1322
 
@@ -1205,31 +1330,56 @@ def open_project_json(projectFileName: str) -> tuple:
1205
1330
  str: message
1206
1331
  """
1207
1332
 
1208
- logging.debug(f"open project: {projectFileName}")
1333
+ logging.debug(f"open_project_json function: {project_file_name}")
1209
1334
 
1210
- projectChanged = False
1211
- msg = ""
1335
+ projectChanged: bool = False
1336
+ msg: str = ""
1212
1337
 
1213
- if not os.path.isfile(projectFileName):
1214
- 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
+ )
1215
1345
 
1216
1346
  try:
1217
- if projectFileName.endswith(".boris.gz"):
1218
- 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")
1219
1349
  else:
1220
- file_in = open(projectFileName, "r")
1350
+ file_in = open(project_file_name, "r")
1221
1351
  file_content = file_in.read()
1222
1352
  except PermissionError:
1223
- 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
+ )
1224
1359
  except Exception:
1225
- 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
+ )
1226
1366
 
1227
1367
  try:
1228
1368
  pj = json.loads(file_content)
1229
1369
  except json.decoder.JSONDecodeError:
1230
- 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
+ )
1231
1376
  except Exception:
1232
- 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
+ )
1233
1383
 
1234
1384
  # transform time to decimal
1235
1385
  pj = util.convert_time_to_decimal(pj)
@@ -1247,11 +1397,9 @@ def open_project_json(projectFileName: str) -> tuple:
1247
1397
  projectChanged = True
1248
1398
 
1249
1399
  # check if project file version is newer than current BORIS project file version
1250
- if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(
1251
- version.__version__
1252
- ):
1400
+ if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
1253
1401
  return (
1254
- projectFileName,
1402
+ project_file_name,
1255
1403
  projectChanged,
1256
1404
  {
1257
1405
  "error": (
@@ -1264,13 +1412,11 @@ def open_project_json(projectFileName: str) -> tuple:
1264
1412
 
1265
1413
  # check if old version v. 0 *.obs
1266
1414
  if cfg.PROJECT_VERSION not in pj:
1267
-
1268
1415
  # convert VIDEO, AUDIO -> MEDIA
1269
1416
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1270
1417
  projectChanged = True
1271
1418
 
1272
1419
  for obs in [x for x in pj[cfg.OBSERVATIONS]]:
1273
-
1274
1420
  # remove 'replace audio' key
1275
1421
  if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
1276
1422
  del pj[cfg.OBSERVATIONS][obs]["replace audio"]
@@ -1279,6 +1425,7 @@ def open_project_json(projectFileName: str) -> tuple:
1279
1425
  pj[cfg.OBSERVATIONS][obs][cfg.TYPE] = cfg.MEDIA
1280
1426
 
1281
1427
  # convert old media list in new one
1428
+ d1: dict = {}
1282
1429
  if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]):
1283
1430
  d1 = {cfg.PLAYER1: [pj[cfg.OBSERVATIONS][obs][cfg.FILE][0]]}
1284
1431
 
@@ -1296,7 +1443,7 @@ def open_project_json(projectFileName: str) -> tuple:
1296
1443
  f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
1297
1444
  "Choose a new file name for saving it."
1298
1445
  )
1299
- projectFileName = ""
1446
+ project_file_name = ""
1300
1447
 
1301
1448
  # update modifiers to JSON format
1302
1449
 
@@ -1320,16 +1467,26 @@ def open_project_json(projectFileName: str) -> tuple:
1320
1467
  pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
1321
1468
 
1322
1469
  if not project_lowerthan4:
1323
- msg = "The project version was updated from {} to {}".format(
1324
- pj[cfg.PROJECT_VERSION], cfg.project_format_version
1325
- )
1470
+ msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
1326
1471
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1327
1472
  projectChanged = True
1328
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
+
1329
1486
  # add category key if not found
1330
1487
  for idx in pj[cfg.ETHOGRAM]:
1331
- if "category" not in pj[cfg.ETHOGRAM][idx]:
1332
- pj[cfg.ETHOGRAM][idx]["category"] = ""
1488
+ if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
1489
+ pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
1333
1490
 
1334
1491
  # if one file is present in player #1 -> set "media_info" key with value of media_file_info
1335
1492
  for obs in pj[cfg.OBSERVATIONS]:
@@ -1340,16 +1497,21 @@ def open_project_json(projectFileName: str) -> tuple:
1340
1497
  cfg.HAS_VIDEO: {},
1341
1498
  cfg.HAS_AUDIO: {},
1342
1499
  }
1343
- for player in [cfg.PLAYER1, cfg.PLAYER2]:
1500
+ for player in (cfg.PLAYER1, cfg.PLAYER2):
1344
1501
  # fix bug Anne Maijer 2017-07-17
1345
1502
  if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
1346
1503
  pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
1347
1504
 
1348
1505
  for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
1349
1506
  # FIX: ffmpeg path
1350
- ret, msg = util.check_ffmpeg_path()
1507
+ ret, ffmpeg_bin = util.check_ffmpeg_path()
1351
1508
  if not ret:
1352
- return projectFileName, projectChanged, {"error": "FFmpeg path not found"}, ""
1509
+ return (
1510
+ project_file_name,
1511
+ projectChanged,
1512
+ {"error": "FFmpeg path not found"},
1513
+ "",
1514
+ )
1353
1515
  else:
1354
1516
  ffmpeg_bin = msg
1355
1517
 
@@ -1360,7 +1522,7 @@ def open_project_json(projectFileName: str) -> tuple:
1360
1522
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
1361
1523
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
1362
1524
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
1363
- project_updated, projectChanged = True, True
1525
+ projectChanged = True
1364
1526
  else: # file path not found
1365
1527
  if (
1366
1528
  cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
@@ -1372,10 +1534,7 @@ def open_project_json(projectFileName: str) -> tuple:
1372
1534
  # duration
1373
1535
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
1374
1536
  cfg.LENGTH: {
1375
- media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
1376
- "video_length"
1377
- ]
1378
- / 1000
1537
+ media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
1379
1538
  }
1380
1539
  }
1381
1540
  projectChanged = True
@@ -1383,13 +1542,8 @@ def open_project_json(projectFileName: str) -> tuple:
1383
1542
  # FPS
1384
1543
  if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
1385
1544
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
1386
- media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
1387
- "nframe"
1388
- ]
1389
- / (
1390
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"]
1391
- / 1000
1392
- )
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)
1393
1547
  }
1394
1548
  else:
1395
1549
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
@@ -1405,9 +1559,7 @@ def open_project_json(projectFileName: str) -> tuple:
1405
1559
  for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
1406
1560
  pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
1407
1561
  if pj[cfg.OBSERVATIONS][obs]["time offset second player"]:
1408
- pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(
1409
- pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1410
- )
1562
+ pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
1411
1563
 
1412
1564
  del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
1413
1565
  project_lowerthan7 = True
@@ -1421,36 +1573,41 @@ def open_project_json(projectFileName: str) -> tuple:
1421
1573
  projectChanged = True
1422
1574
 
1423
1575
  if project_lowerthan7:
1424
-
1425
1576
  msg = f"The project was updated to the current project version ({cfg.project_format_version})."
1426
1577
 
1427
1578
  try:
1428
- old_project_file_name = projectFileName.replace(".boris", f".v{pj['project_format_version']}.boris")
1429
- 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)
1430
1581
  msg += f"\n\nThe old file project was saved as {old_project_file_name}"
1431
1582
  except Exception:
1432
- pass
1583
+ QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
1433
1584
 
1434
1585
  pj[cfg.PROJECT_VERSION] = cfg.project_format_version
1435
1586
 
1436
- 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
1437
1594
 
1438
1595
 
1439
- def event_type(code: str, ethogram: dict) -> str:
1596
+ def event_type(code: str, ethogram: dict) -> str | None:
1440
1597
  """
1441
- returns type of event for code
1598
+ returns type of event for behavior code
1442
1599
 
1443
1600
  Args:
1444
1601
  ethogram (dict); ethogram of project
1445
1602
  code (str): behavior code
1446
1603
 
1447
1604
  Returns:
1448
- str: "STATE EVENT", "POINT EVENT" or None if code not found in ethogram
1605
+ str: behavior type
1449
1606
  """
1450
1607
 
1451
1608
  for idx in ethogram:
1452
1609
  if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
1453
- return ethogram[idx][cfg.TYPE].upper()
1610
+ return ethogram[idx][cfg.TYPE]
1454
1611
  return None
1455
1612
 
1456
1613
 
@@ -1459,7 +1616,6 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1459
1616
  fix unpaired state events in observation
1460
1617
 
1461
1618
  Args:
1462
- obsId (str): observation id
1463
1619
  ethogram (dict): ethogram dictionary
1464
1620
  observation (dict): observation dictionary
1465
1621
  fix_at_time (Decimal): time to fix the unpaired events
@@ -1468,31 +1624,27 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1468
1624
  list: list of events with state events fixed
1469
1625
  """
1470
1626
 
1471
- out = ""
1472
- closing_events_to_add = []
1473
- flagStateEvent = False
1474
- subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
1475
- 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}
1476
1630
 
1477
1631
  for subject in sorted(set(subjects)):
1478
-
1479
- behaviors = [
1480
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1481
- for event in observation[cfg.EVENTS]
1482
- 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
1483
1634
  ]
1484
1635
 
1485
1636
  for behavior in sorted(set(behaviors)):
1486
- if (behavior in ethogram_behaviors) and (cfg.STATE in event_type(behavior, ethogram).upper()):
1487
-
1637
+ if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
1488
1638
  lst, memTime = [], {}
1489
1639
  for event in [
1490
1640
  event
1491
1641
  for event in observation[cfg.EVENTS]
1492
1642
  if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
1493
1643
  ]:
1494
-
1495
- 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
+ ]
1496
1648
 
1497
1649
  if behav_modif in lst:
1498
1650
  lst.remove(behav_modif)
@@ -1502,11 +1654,78 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
1502
1654
  memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
1503
1655
 
1504
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
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
+ ]
1505
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:
1506
1717
  last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
1507
1718
 
1508
1719
  closing_events_to_add.append(
1509
- [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
+ ]
1510
1729
  )
1511
1730
 
1512
1731
  return closing_events_to_add
@@ -1533,8 +1752,11 @@ def explore_project(self) -> None:
1533
1752
  manage double-click on tablewidget of explore project results
1534
1753
  """
1535
1754
  observation_operations.load_observation(self, obs_id, cfg.VIEW)
1536
- self.twEvents.selectRow(event_idx - 1)
1537
- 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))
1538
1760
 
1539
1761
  elements_list = ("Subject", "Behavior", "Modifier", "Comment")
1540
1762
  elements = []
@@ -1543,7 +1765,9 @@ def explore_project(self) -> None:
1543
1765
  elements.append(("cb", "Case sensitive", False))
1544
1766
 
1545
1767
  explore_dlg = dialog.Input_dialog(
1546
- 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",
1547
1771
  )
1548
1772
  explore_dlg.pbOK.setText("Find")
1549
1773
  if not explore_dlg.exec_():
@@ -1567,10 +1791,7 @@ def explore_project(self) -> None:
1567
1791
  if any(
1568
1792
  (
1569
1793
  (explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
1570
- (
1571
- not explore_dlg.elements["Case sensitive"].isChecked()
1572
- and text.upper() in event[idx].upper()
1573
- ),
1794
+ (not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
1574
1795
  )
1575
1796
  ):
1576
1797
  nb_results += 1
@@ -1604,3 +1825,215 @@ def explore_project(self) -> None:
1604
1825
 
1605
1826
  else:
1606
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)