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