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.
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,1203 @@
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
+ from decimal import Decimal as dec
23
+ import tablib
24
+ import logging
25
+ import os
26
+ import sys
27
+ import datetime as dt
28
+ import math
29
+ import pathlib
30
+ from io import StringIO
31
+ import pandas as pd
32
+ from typing import Tuple
33
+
34
+ try:
35
+ import pyreadr
36
+
37
+ flag_pyreadr_loaded = True
38
+ except ModuleNotFoundError:
39
+ flag_pyreadr_loaded = False
40
+
41
+ from . import dialog
42
+ from . import config as cfg
43
+ from . import utilities as util
44
+ from . import project_functions
45
+ from . import observation_operations
46
+ from . import db_functions
47
+ from . import event_operations
48
+
49
+
50
+ def export_events_jwatcher(
51
+ parameters: dict, obsId: str, observation: list, ethogram: dict, file_name: str, output_format: str
52
+ ) -> Tuple[bool, str]:
53
+ """
54
+ export events jwatcher .dat format
55
+
56
+ Args:
57
+ parameters (dict): subjects, behaviors
58
+ obsId (str): observation id
59
+ observation (dict): observation
60
+ ethogram (dict): ethogram of project
61
+ file_name (str): file name for exporting events
62
+ output_format (str): Not used for compatibility with export_events function
63
+
64
+ Returns:
65
+ bool: result: True if OK else False
66
+ str: error message
67
+ """
68
+ for subject in parameters[cfg.SELECTED_SUBJECTS]:
69
+ # select events for current subject
70
+ events = []
71
+ for event in observation[cfg.EVENTS]:
72
+ if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject or (
73
+ subject == cfg.NO_FOCAL_SUBJECT and event[cfg.EVENT_SUBJECT_FIELD_IDX] == ""
74
+ ):
75
+ events.append(event)
76
+
77
+ if not events:
78
+ continue
79
+
80
+ total_length = 0 # in seconds
81
+ if observation[cfg.EVENTS]:
82
+ total_length = observation[cfg.EVENTS][-1][0] - observation[cfg.EVENTS][0][0] # last event time - first event time
83
+
84
+ file_name_subject = str(pathlib.Path(file_name).parent / pathlib.Path(file_name).stem) + "_" + subject + ".dat"
85
+
86
+ rows = ["FirstLineOfData"] # to be completed
87
+ rows.append("#-----------------------------------------------------------")
88
+ rows.append(f"# Name: {pathlib.Path(file_name_subject).name}")
89
+ rows.append("# Format: Focal Data File 1.0")
90
+ rows.append(f"# Updated: {dt.datetime.now().isoformat()}")
91
+ rows.append("#-----------------------------------------------------------")
92
+ rows.append("")
93
+ rows.append(f"FocalMasterFile={pathlib.Path(file_name_subject).with_suffix('.fmf')}")
94
+ rows.append("")
95
+
96
+ rows.append(f"# Observation started: {observation['date']}")
97
+ try:
98
+ start_time = dt.datetime.strptime(observation["date"], "%Y-%m-%dT%H:%M:%S")
99
+ except ValueError:
100
+ start_time = dt.datetime(1970, 1, 1, 0, 0)
101
+ start_time_epoch = int((start_time - dt.datetime(1970, 1, 1, 0, 0)).total_seconds() * 1000)
102
+ rows.append(f"StartTime={start_time_epoch}")
103
+
104
+ stop_time = (start_time + dt.timedelta(seconds=float(total_length))).isoformat()
105
+ stop_time_epoch = int(start_time_epoch + float(total_length) * 1000)
106
+
107
+ rows.append(f"# Observation stopped: {stop_time}")
108
+ rows.append(f"StopTime={stop_time_epoch}")
109
+
110
+ rows.extend([""] * 3)
111
+ rows.append("#BEGIN DATA")
112
+ rows[0] = f"FirstLineOfData={len(rows) + 1}"
113
+
114
+ all_observed_behaviors = []
115
+ mem_number_of_state_events = {}
116
+ for event in events:
117
+ behav_code = event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
118
+
119
+ try:
120
+ behavior_key = [ethogram[k][cfg.BEHAVIOR_KEY] for k in ethogram if ethogram[k][cfg.BEHAVIOR_CODE] == behav_code][0]
121
+ except Exception:
122
+ # coded behavior not defined in ethogram
123
+ continue
124
+ if [ethogram[k][cfg.TYPE] for k in ethogram if ethogram[k][cfg.BEHAVIOR_CODE] == behav_code] in [
125
+ [cfg.STATE_EVENT],
126
+ [cfg.STATE_EVENT_WITH_CODING_MAP],
127
+ ]:
128
+ if behav_code in mem_number_of_state_events:
129
+ mem_number_of_state_events[behav_code] += 1
130
+ else:
131
+ mem_number_of_state_events[behav_code] = 1
132
+ # skip the STOP event in case of STATE
133
+ if mem_number_of_state_events[behav_code] % 2 == 0:
134
+ continue
135
+
136
+ rows.append(f"{int(event[cfg.EVENT_TIME_FIELD_IDX] * 1000)}, {behavior_key}")
137
+ if (event[cfg.EVENT_BEHAVIOR_FIELD_IDX], behavior_key) not in all_observed_behaviors:
138
+ all_observed_behaviors.append((event[cfg.EVENT_BEHAVIOR_FIELD_IDX], behavior_key))
139
+
140
+ rows.append(f"{int(events[-1][0] * 1000)}, EOF\n")
141
+
142
+ try:
143
+ with open(file_name_subject, "w") as f_out:
144
+ f_out.write("\n".join(rows))
145
+ except Exception:
146
+ return False, f"File DAT not created for subject {subject}: {sys.exc_info()[1]}"
147
+
148
+ # create fmf file
149
+ fmf_file_path = pathlib.Path(file_name_subject).with_suffix(".fmf")
150
+ fmf_creation_answer = ""
151
+ if fmf_file_path.exists():
152
+ fmf_creation_answer = dialog.MessageDialog(
153
+ cfg.programName,
154
+ (f"The {fmf_file_path} file already exists.<br>What do you want to do?"),
155
+ [cfg.OVERWRITE, "Skip file creation", cfg.CANCEL],
156
+ )
157
+
158
+ if fmf_creation_answer == cfg.CANCEL:
159
+ return True, ""
160
+
161
+ rows = []
162
+ rows.append("#-----------------------------------------------------------")
163
+ rows.append(f"# Name: {pathlib.Path(file_name_subject).with_suffix('.fmf').name}")
164
+ rows.append("# Format: Focal Master File 1.0")
165
+ rows.append(f"# Updated: {dt.datetime.now().isoformat()}")
166
+ rows.append("#-----------------------------------------------------------")
167
+ for behav, key in all_observed_behaviors:
168
+ rows.append(f"Behaviour.name.{key}={behav}")
169
+ behav_description = [ethogram[k][cfg.DESCRIPTION] for k in ethogram if ethogram[k][cfg.BEHAVIOR_CODE] == behav][0]
170
+ rows.append(f"Behaviour.description.{key}={behav_description}")
171
+
172
+ rows.append(f"DurationMilliseconds={int(float(total_length) * 1000)}")
173
+ rows.append("CountUp=false")
174
+ rows.append("Question.1=")
175
+ rows.append("Question.2=")
176
+ rows.append("Question.3=")
177
+ rows.append("Question.4=")
178
+ rows.append("Question.5=")
179
+ rows.append("Question.6=")
180
+ rows.append("Notes=")
181
+ rows.append("Supplementary=\n")
182
+
183
+ if fmf_creation_answer == cfg.OVERWRITE or fmf_creation_answer == "":
184
+ try:
185
+ with open(fmf_file_path, "w") as f_out:
186
+ f_out.write("\n".join(rows))
187
+ except Exception:
188
+ return False, f"File FMF not created: {sys.exc_info()[1]}"
189
+
190
+ # create FAF file
191
+ faf_file_path = pathlib.Path(file_name_subject).with_suffix(".faf")
192
+ faf_creation_answer = ""
193
+ if faf_file_path.exists():
194
+ faf_creation_answer = dialog.MessageDialog(
195
+ cfg.programName,
196
+ (f"The {faf_file_path} file already exists.<br>What do you want to do?"),
197
+ [cfg.OVERWRITE, "Skip file creation", cfg.CANCEL],
198
+ )
199
+ if faf_creation_answer == cfg.CANCEL:
200
+ return True, ""
201
+
202
+ rows = []
203
+ rows.append("#-----------------------------------------------------------")
204
+ rows.append("# Name: {}".format(pathlib.Path(file_name_subject).with_suffix(".faf").name))
205
+ rows.append("# Format: Focal Analysis Master File 1.0")
206
+ rows.append("# Updated: {}".format(dt.datetime.now().isoformat()))
207
+ rows.append("#-----------------------------------------------------------")
208
+ rows.append("FocalMasterFile={}".format(str(pathlib.Path(file_name_subject).with_suffix(".fmf"))))
209
+ rows.append("")
210
+ rows.append("TimeBinDuration=0.0")
211
+ rows.append("EndWithLastCompleteBin=true")
212
+ rows.append("")
213
+ rows.append("ScoreFromBeginning=true")
214
+ rows.append("ScoreFromBehavior=false")
215
+ rows.append("ScoreFromFirstBehavior=false")
216
+ rows.append("ScoreFromOffset=false")
217
+ rows.append("")
218
+ rows.append("Offset=0.0")
219
+ rows.append("BehaviorToScoreFrom=")
220
+ rows.append("")
221
+ rows.append("OutOfSightCode=")
222
+ rows.append("")
223
+ rows.append("Report.StateNaturalInterval.Occurrence=false")
224
+ rows.append("Report.StateNaturalInterval.TotalTime=false")
225
+ rows.append("Report.StateNaturalInterval.Average=false")
226
+ rows.append("Report.StateNaturalInterval.StandardDeviation=false")
227
+ rows.append("Report.StateNaturalInterval.ProportionOfTime=false")
228
+ rows.append("Report.StateNaturalInterval.ProportionOfTimeInSight=false")
229
+ rows.append("Report.StateNaturalInterval.ConditionalProportionOfTime=false")
230
+ rows.append("")
231
+ rows.append("Report.StateNaturalDuration.Occurrence=false")
232
+ rows.append("Report.StateNaturalDuration.TotalTime=false")
233
+ rows.append("Report.StateNaturalDuration.Average=false")
234
+ rows.append("Report.StateNaturalDuration.StandardDeviation=false")
235
+ rows.append("Report.StateNaturalDuration.ProportionOfTime=false")
236
+ rows.append("Report.StateNaturalDuration.ProportionOfTimeInSight=false")
237
+ rows.append("Report.StateNaturalDuration.ConditionalProportionOfTime=false")
238
+ rows.append("")
239
+ rows.append("Report.StateAllInterval.Occurrence=false")
240
+ rows.append("Report.StateAllInterval.TotalTime=false")
241
+ rows.append("Report.StateAllInterval.Average=false")
242
+ rows.append("Report.StateAllInterval.StandardDeviation=false")
243
+ rows.append("Report.StateAllInterval.ProportionOfTime=false")
244
+ rows.append("Report.StateAllInterval.ProportionOfTimeInSight=false")
245
+ rows.append("Report.StateAllInterval.ConditionalProportionOfTime=false")
246
+ rows.append("")
247
+ rows.append("Report.StateAllDuration.Occurrence=true")
248
+ rows.append("Report.StateAllDuration.TotalTime=true")
249
+ rows.append("Report.StateAllDuration.Average=true")
250
+ rows.append("Report.StateAllDuration.StandardDeviation=false")
251
+ rows.append("Report.StateAllDuration.ProportionOfTime=false")
252
+ rows.append("Report.StateAllDuration.ProportionOfTimeInSight=true")
253
+ rows.append("Report.StateAllDuration.ConditionalProportionOfTime=false")
254
+ rows.append("")
255
+ rows.append("Report.EventNaturalInterval.EventCount=false")
256
+ rows.append("Report.EventNaturalInterval.Occurrence=false")
257
+ rows.append("Report.EventNaturalInterval.Average=false")
258
+ rows.append("Report.EventNaturalInterval.StandardDeviation=false")
259
+ rows.append("Report.EventNaturalInterval.ConditionalNatEventCount=false")
260
+ rows.append("Report.EventNaturalInterval.ConditionalNatRate=false")
261
+ rows.append("Report.EventNaturalInterval.ConditionalNatIntervalOccurance=false")
262
+ rows.append("Report.EventNaturalInterval.ConditionalNatIntervalAverage=false")
263
+ rows.append("Report.EventNaturalInterval.ConditionalNatIntervalStandardDeviation=false")
264
+ rows.append("Report.EventNaturalInterval.ConditionalAllEventCount=false")
265
+ rows.append("Report.EventNaturalInterval.ConditionalAllRate=false")
266
+ rows.append("Report.EventNaturalInterval.ConditionalAllIntervalOccurance=false")
267
+ rows.append("Report.EventNaturalInterval.ConditionalAllIntervalAverage=false")
268
+ rows.append("Report.EventNaturalInterval.ConditionalAllIntervalStandardDeviation=false")
269
+ rows.append("")
270
+ rows.append("AllCodesMutuallyExclusive=true")
271
+ rows.append("")
272
+
273
+ for behav, key in all_observed_behaviors:
274
+ rows.append(f"Behavior.isModified.{key}=false")
275
+ rows.append(f"Behavior.isSubtracted.{key}=false")
276
+ rows.append(f"Behavior.isIgnored.{key}=false")
277
+ rows.append(f"Behavior.isEventAnalyzed.{key}=false")
278
+ rows.append(f"Behavior.switchesOff.{key}=")
279
+ rows.append("")
280
+
281
+ if faf_creation_answer == "" or faf_creation_answer == cfg.OVERWRITE:
282
+ try:
283
+ with open(pathlib.Path(file_name_subject).with_suffix(".faf"), "w") as f_out:
284
+ f_out.write("\n".join(rows))
285
+ except Exception:
286
+ return False, f"File FAF not created: {sys.exc_info()[1]}"
287
+
288
+ return True, ""
289
+
290
+
291
+ def export_tabular_events(
292
+ pj: dict, parameters: dict, obs_id: str, observation: dict, ethogram: dict, file_name: str, output_format: str
293
+ ) -> Tuple[bool, str]:
294
+ """
295
+ export events for one observation (obs_id)
296
+
297
+ Args:
298
+ parameters (dict): subjects, behaviors
299
+ obs_id (str): observation id
300
+ observation (dict): observation
301
+ ethogram (dict): ethogram of project
302
+ file_name (str): file name for exporting events
303
+ output_format (str): output for exporting events
304
+
305
+ Returns:
306
+ bool: result: True if OK else False
307
+ str: error message
308
+ """
309
+
310
+ logging.debug(f"function: export tabular events for {obs_id}")
311
+ logging.debug(f"parameters: {parameters}")
312
+
313
+ interval = parameters["time"]
314
+
315
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(pj[cfg.OBSERVATIONS], [obs_id])
316
+ start_interval, end_interval = observation_operations.time_intervals_range(pj[cfg.OBSERVATIONS], [obs_id])
317
+
318
+ if interval == cfg.TIME_EVENTS:
319
+ min_time = start_coding
320
+ max_time = end_coding
321
+
322
+ if interval == cfg.TIME_FULL_OBS:
323
+ if observation[cfg.TYPE] == cfg.MEDIA:
324
+ max_media_duration, _ = observation_operations.media_duration(pj[cfg.OBSERVATIONS], [obs_id])
325
+ min_time = dec("0")
326
+ max_time = max_media_duration
327
+ coding_duration = max_media_duration
328
+
329
+ if observation[cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
330
+ min_time = start_coding
331
+ max_time = end_coding
332
+
333
+ if interval == cfg.TIME_ARBITRARY_INTERVAL:
334
+ min_time = parameters[cfg.START_TIME]
335
+ max_time = parameters[cfg.END_TIME]
336
+
337
+ if interval == cfg.TIME_OBS_INTERVAL:
338
+ max_media_duration, _ = observation_operations.media_duration(pj[cfg.OBSERVATIONS], [obs_id])
339
+ min_time = start_interval
340
+ # Use max media duration for max time if no interval is defined (=0)
341
+ max_time = end_interval if end_interval != 0 else max_media_duration
342
+
343
+ logging.debug(f"min_time: {min_time} max_time: {max_time}")
344
+
345
+ events_with_status = project_functions.events_start_stop(ethogram, observation[cfg.EVENTS], observation[cfg.TYPE])
346
+
347
+ # check max number of modifiers
348
+ max_modifiers = 0
349
+ for event in events_with_status:
350
+ if not math.isnan(min_time) and not math.isnan(max_time): # obs not from pictures
351
+ if min_time <= event[cfg.EVENT_TIME_FIELD_IDX] <= max_time:
352
+ if event[cfg.EVENT_MODIFIER_FIELD_IDX]:
353
+ max_modifiers = max(max_modifiers, len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")))
354
+ else:
355
+ if event[cfg.EVENT_MODIFIER_FIELD_IDX]:
356
+ max_modifiers = max(max_modifiers, len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")))
357
+
358
+ # media file number
359
+ media_nb = util.count_media_file(observation[cfg.FILE])
360
+
361
+ rows: list = []
362
+
363
+ # fields and type
364
+ fields_type: dict = {
365
+ "Observation id": str,
366
+ "Observation date": dt.datetime,
367
+ "Description": str,
368
+ "Observation duration": float,
369
+ "Observation type": str,
370
+ "Source": str,
371
+ "Time offset (s)": str,
372
+ }
373
+ if media_nb == 1:
374
+ fields_type["Media duration (s)"] = float
375
+ fields_type["FPS"] = float
376
+ else:
377
+ fields_type["Media duration (s)"] = str
378
+ fields_type["FPS"] = str
379
+
380
+ # independent variables
381
+ if cfg.INDEPENDENT_VARIABLES in observation:
382
+ for variable in observation[cfg.INDEPENDENT_VARIABLES]:
383
+ # TODO check variable type
384
+ fields_type[variable] = str
385
+
386
+ fields_type.update({"Subject": str, "Behavior": str, "Behavioral category": str})
387
+
388
+ # modifiers
389
+ for idx in range(max_modifiers):
390
+ fields_type[f"Modifier #{idx + 1}"] = str
391
+
392
+ fields_type.update(
393
+ {
394
+ "Behavior type": str,
395
+ "Time": float,
396
+ "Media file name": str,
397
+ "Image index": float, # add image index and image file path to header
398
+ "Image file path": str,
399
+ "Comment": str,
400
+ }
401
+ )
402
+
403
+ # add header
404
+ rows.append(list(fields_type.keys()))
405
+
406
+ behavioral_category = project_functions.behavior_category(ethogram)
407
+
408
+ duration1 = [] # in seconds
409
+ if observation[cfg.TYPE] == cfg.MEDIA:
410
+ try:
411
+ for mediaFile in observation[cfg.FILE][cfg.PLAYER1]:
412
+ duration1.append(observation[cfg.MEDIA_INFO][cfg.LENGTH][mediaFile])
413
+ except KeyError:
414
+ pass
415
+
416
+ for event in events_with_status:
417
+ if (not math.isnan(min_time)) and not (min_time <= event[cfg.EVENT_TIME_FIELD_IDX] <= max_time):
418
+ continue
419
+ if (
420
+ (event[cfg.EVENT_SUBJECT_FIELD_IDX] in parameters[cfg.SELECTED_SUBJECTS])
421
+ or (event[cfg.EVENT_SUBJECT_FIELD_IDX] == "" and cfg.NO_FOCAL_SUBJECT in parameters[cfg.SELECTED_SUBJECTS])
422
+ ) and (event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in parameters[cfg.SELECTED_BEHAVIORS]):
423
+ fields: list = []
424
+ fields.append(obs_id)
425
+ fields.append(observation.get("date", "").replace("T", " "))
426
+ fields.append(util.eol2space(observation.get(cfg.DESCRIPTION, "")))
427
+ # total length
428
+ fields.append(coding_duration if not coding_duration.is_nan() else cfg.NA)
429
+
430
+ if observation[cfg.TYPE] == cfg.MEDIA:
431
+ fields.append("Media file(s)")
432
+
433
+ media_file_str, fps_str, media_durations_str = "", "", ""
434
+ for player in observation[cfg.FILE]:
435
+ media_file_lst, fps_lst, media_durations_lst = [], [], []
436
+ if observation[cfg.FILE][player]:
437
+ for media_file in observation[cfg.FILE][player]:
438
+ media_file_lst.append(media_file)
439
+ fps_lst.append(f"{observation[cfg.MEDIA_INFO][cfg.FPS].get(media_file, cfg.NA):.3f}")
440
+ media_durations_lst.append(f"{observation[cfg.MEDIA_INFO][cfg.LENGTH].get(media_file, cfg.NA):.3f}")
441
+ if player > "1":
442
+ media_file_str += "|"
443
+ fps_str += "|"
444
+ media_durations_str += "|"
445
+ media_file_str += f"player #{player}:" + ";".join(media_file_lst)
446
+ fps_str += ";".join(fps_lst)
447
+ media_durations_str += ";".join(media_durations_lst)
448
+
449
+ """
450
+ # number of players
451
+ n_players = len([x for x in observation[cfg.FILE] if observation[cfg.FILE][x]])
452
+ media_file_str, fps_str = "", ""
453
+ for player in observation[cfg.FILE]:
454
+ if observation[cfg.FILE][player]:
455
+ if media_file_str:
456
+ media_file_str += " "
457
+ if fps_str:
458
+ fps_str += " "
459
+ if n_players > 1:
460
+ media_file_str += f"player #{player}: "
461
+ fps_str += f"player #{player}: "
462
+ media_list, fps_list = [], []
463
+ for media_file in observation[cfg.FILE][player]:
464
+ media_list.append(media_file)
465
+ fps_list.append(f"{observation[cfg.MEDIA_INFO][cfg.FPS].get(media_file, cfg.NA):.3f}")
466
+ media_file_str += ";".join(media_list)
467
+ fps_str += ";".join(fps_list)
468
+ """
469
+
470
+ fields.append(media_file_str)
471
+
472
+ elif observation[cfg.TYPE] == cfg.LIVE:
473
+ fields.append("Live observation")
474
+ fields.append(cfg.NA)
475
+ media_durations_str = cfg.NA
476
+ fps_str = cfg.NA
477
+
478
+ elif observation[cfg.TYPE] == cfg.IMAGES:
479
+ fields.append("From directories of images")
480
+ dir_list = []
481
+ for dir in observation[cfg.DIRECTORIES_LIST]:
482
+ dir_list.append(dir)
483
+ fields.append(";".join(dir_list))
484
+ media_durations_str = cfg.NA
485
+ fps_str = cfg.NA
486
+
487
+ else:
488
+ fields.append("")
489
+
490
+ # time offset
491
+ fields.append(observation[cfg.TIME_OFFSET])
492
+
493
+ # media duration
494
+ fields.append(media_durations_str)
495
+
496
+ # FPS
497
+ fields.append(fps_str)
498
+
499
+ # indep var
500
+ if cfg.INDEPENDENT_VARIABLES in observation:
501
+ for variable in observation[cfg.INDEPENDENT_VARIABLES]:
502
+ fields.append(observation[cfg.INDEPENDENT_VARIABLES][variable])
503
+
504
+ fields.append(event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.SUBJECT]])
505
+ fields.append(event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.BEHAVIOR_CODE]])
506
+
507
+ # behavioral category
508
+ try:
509
+ behav_category = behavioral_category[event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.BEHAVIOR_CODE]]]
510
+ except Exception:
511
+ behav_category = ""
512
+ fields.append(behav_category)
513
+
514
+ # modifiers
515
+ if max_modifiers:
516
+ modifiers = event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.MODIFIER]].split("|")
517
+ while len(modifiers) < max_modifiers:
518
+ modifiers.append("")
519
+
520
+ for m in modifiers:
521
+ fields.append(m)
522
+
523
+ # status (START/STOP)
524
+ fields.append(event[-1])
525
+
526
+ # time
527
+ fields.append(util.convertTime(time_format=cfg.S, sec=event[cfg.EVENT_TIME_FIELD_IDX]))
528
+
529
+ # check video file name containing the event
530
+ if observation[cfg.TYPE] == cfg.MEDIA:
531
+ video_file_name = observation_operations.event2media_file_name(observation, event[cfg.EVENT_TIME_FIELD_IDX])
532
+ if video_file_name is None:
533
+ video_file_name = "Not found"
534
+
535
+ elif observation[cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
536
+ video_file_name = cfg.NA
537
+ fields.append(video_file_name)
538
+
539
+ # image file index
540
+ if observation[cfg.TYPE] == cfg.IMAGES:
541
+ fields.append(event[cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]]) # image file index
542
+ elif observation[cfg.TYPE] == cfg.MEDIA:
543
+ frame_idx = event_operations.read_event_field(event, cfg.MEDIA, cfg.FRAME_INDEX)
544
+ fields.append(frame_idx) # frame index
545
+ elif observation[cfg.TYPE] == cfg.LIVE:
546
+ fields.append(cfg.NA)
547
+ else:
548
+ fields.append("")
549
+
550
+ # image file path
551
+ if observation[cfg.TYPE] == cfg.IMAGES:
552
+ fields.append(event[cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_PATH]]) # image file path
553
+ elif observation[cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
554
+ fields.append(cfg.NA)
555
+ else:
556
+ fields.append("")
557
+
558
+ # comment
559
+ fields.append(event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.COMMENT]].replace(os.linesep, " "))
560
+
561
+ rows.append(fields)
562
+
563
+ max_len = max([len(r) for r in rows])
564
+ data = tablib.Dataset()
565
+
566
+ data.title = util.safe_xl_worksheet_title(obs_id, output_format)
567
+
568
+ for row in rows:
569
+ data.append(util.complete(row, max_len))
570
+
571
+ r, msg = dataset_write(data, file_name, output_format, dtype=fields_type)
572
+
573
+ return r, msg
574
+
575
+
576
+ def dataset_write(dataset: tablib.Dataset, file_name: str, output_format: str, dtype: dict = {}) -> tuple: # -> tuple[bool, str]:
577
+ """
578
+ write a tablib dataset with aggregated events or tabular events to file in specified format (output_format)
579
+
580
+ Args:
581
+ dataset (tablib.dataset): dataset to write
582
+ file_name (str): file name
583
+ output_format (str): format of output
584
+ dtype (dict): type of field
585
+
586
+ Returns:
587
+ bool: result. True if OK else False
588
+ str: error message
589
+ """
590
+
591
+ logging.debug("function: dataset_write")
592
+
593
+ try:
594
+ if output_format in (cfg.PANDAS_DF_EXT, cfg.RDS_EXT):
595
+ # build pandas dataframe from the tsv export of tablib dataset
596
+ date_type: list = []
597
+ for field_name in dtype:
598
+ if dtype[field_name] == dt.datetime:
599
+ date_type.append(field_name)
600
+ # delete data type from dtype
601
+ for field_name in date_type:
602
+ del dtype[field_name]
603
+
604
+ df = pd.read_csv(
605
+ StringIO(dataset.export(cfg.TSV_EXT)),
606
+ sep="\t",
607
+ dtype=dtype,
608
+ parse_dates=date_type,
609
+ )
610
+
611
+ if output_format == cfg.PANDAS_DF_EXT:
612
+ df.to_pickle(file_name)
613
+
614
+ if output_format == cfg.RDS_EXT and flag_pyreadr_loaded:
615
+ pyreadr.write_rds(file_name, df)
616
+
617
+ return True, ""
618
+
619
+ if output_format in (cfg.CSV_EXT, cfg.TSV_EXT, cfg.HTML_EXT):
620
+ with open(file_name, "wb") as f:
621
+ f.write(str.encode(dataset.export(output_format)))
622
+ return True, ""
623
+
624
+ if output_format in (cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT):
625
+ dataset.title = util.safe_xl_worksheet_title(dataset.title, output_format)
626
+
627
+ with open(file_name, "wb") as f:
628
+ f.write(dataset.export(output_format))
629
+ return True, ""
630
+
631
+ return False, f"Format {output_format} not found"
632
+
633
+ except Exception:
634
+ return False, str(sys.exc_info()[1])
635
+
636
+
637
+ def export_aggregated_events(pj: dict, parameters: dict, obsId: str, force_number_modifiers: int = 0) -> Tuple[tablib.Dataset, int]:
638
+ """
639
+ export aggregated events of one observation
640
+
641
+ Args:
642
+ pj (dict): BORIS project
643
+ parameters (dict): subjects, behaviors
644
+ obsId (str): observation id
645
+ force_number_modifiers (int): force the number of modifiers to return
646
+
647
+ Returns:
648
+ tablib.Dataset:
649
+ int: Maximum number of modifiers
650
+
651
+ """
652
+ logging.debug(f"function: export aggregated events {obsId} parameters: {parameters} ")
653
+
654
+ observation = pj[cfg.OBSERVATIONS][obsId]
655
+ interval = parameters["time"]
656
+
657
+ data = tablib.Dataset()
658
+
659
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(pj[cfg.OBSERVATIONS], [obsId])
660
+ start_interval, end_interval = observation_operations.time_intervals_range(pj[cfg.OBSERVATIONS], [obsId])
661
+
662
+ if start_coding is None and end_coding is None: # no events
663
+ return data, 0
664
+
665
+ if interval == cfg.TIME_EVENTS:
666
+ min_time = float(start_coding)
667
+ max_time = float(end_coding)
668
+
669
+ if interval == cfg.TIME_FULL_OBS:
670
+ if observation[cfg.TYPE] == cfg.MEDIA:
671
+ max_media_duration, _ = observation_operations.media_duration(pj[cfg.OBSERVATIONS], [obsId])
672
+ min_time = float(0)
673
+ max_time = float(max_media_duration)
674
+ coding_duration = max_media_duration
675
+ if observation[cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
676
+ min_time = float(start_coding)
677
+ max_time = float(end_coding)
678
+
679
+ if interval == cfg.TIME_ARBITRARY_INTERVAL:
680
+ min_time = float(parameters[cfg.START_TIME])
681
+ max_time = float(parameters[cfg.END_TIME])
682
+
683
+ if interval == cfg.TIME_OBS_INTERVAL:
684
+ max_media_duration, _ = observation_operations.media_duration(pj[cfg.OBSERVATIONS], [obsId])
685
+ min_time = float(start_interval)
686
+ # Use max media duration for max time if no interval is defined (=0)
687
+ max_time = float(end_interval) if end_interval != 0 else float(max_media_duration)
688
+
689
+ logging.debug(f"min_time: {min_time} max_time: {max_time}")
690
+
691
+ # obs description
692
+ obs_description = util.eol2space(observation[cfg.DESCRIPTION])
693
+
694
+ _, _, connector = db_functions.load_aggregated_events_in_db(
695
+ pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
696
+ )
697
+ if connector is None:
698
+ logging.critical("error when loading aggregated events in DB")
699
+ return data, 0
700
+
701
+ cursor = connector.cursor()
702
+
703
+ # adapt start and stop to the selected time interval
704
+ if not math.isnan(min_time) and not math.isnan(max_time): # obs with timestamp
705
+ # delete events outside time interval
706
+ cursor.execute(
707
+ "DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
708
+ (
709
+ obsId,
710
+ min_time,
711
+ min_time,
712
+ max_time,
713
+ max_time,
714
+ ),
715
+ )
716
+
717
+ cursor.execute(
718
+ "UPDATE aggregated_events SET start = ? WHERE observation = ? AND start < ? AND stop BETWEEN ? AND ?",
719
+ (
720
+ min_time,
721
+ obsId,
722
+ min_time,
723
+ min_time,
724
+ max_time,
725
+ ),
726
+ )
727
+ cursor.execute(
728
+ "UPDATE aggregated_events SET stop = ? WHERE observation = ? AND stop > ? AND start BETWEEN ? AND ?",
729
+ (
730
+ max_time,
731
+ obsId,
732
+ max_time,
733
+ min_time,
734
+ max_time,
735
+ ),
736
+ )
737
+
738
+ cursor.execute(
739
+ "UPDATE aggregated_events SET start = ?, stop = ? WHERE observation = ? AND start < ? AND stop > ?",
740
+ (
741
+ min_time,
742
+ max_time,
743
+ obsId,
744
+ min_time,
745
+ max_time,
746
+ ),
747
+ )
748
+
749
+ behavioral_category = project_functions.behavior_category(pj[cfg.ETHOGRAM])
750
+
751
+ cursor.execute("SELECT DISTINCT modifiers FROM aggregated_events")
752
+
753
+ if not force_number_modifiers:
754
+ max_modifiers: int = 0
755
+ for row in cursor.fetchall():
756
+ if row["modifiers"]:
757
+ max_modifiers = max(max_modifiers, row["modifiers"].count("|") + 1)
758
+ else:
759
+ max_modifiers = force_number_modifiers
760
+
761
+ for subject in parameters[cfg.SELECTED_SUBJECTS]:
762
+ # calculate observation duration by subject (by obs)
763
+ cursor.execute(("SELECT SUM(stop - start) AS duration FROM aggregated_events WHERE subject = ? "), (subject,))
764
+ duration_by_subject_by_obs = cursor.fetchone()["duration"]
765
+ if duration_by_subject_by_obs is not None:
766
+ duration_by_subject_by_obs = round(duration_by_subject_by_obs, 3)
767
+
768
+ for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
769
+ cursor.execute(
770
+ "SELECT DISTINCT modifiers FROM aggregated_events WHERE subject=? AND behavior=? ORDER BY modifiers",
771
+ (
772
+ subject,
773
+ behavior,
774
+ ),
775
+ )
776
+
777
+ rows_distinct_modifiers = list(x[0] for x in cursor.fetchall())
778
+
779
+ for distinct_modifiers in rows_distinct_modifiers:
780
+ cursor.execute(
781
+ (
782
+ "SELECT start, stop, type, modifiers, comment, comment_stop, "
783
+ "image_index_start, image_index_stop, image_path_start, image_path_stop "
784
+ "FROM aggregated_events "
785
+ "WHERE subject = ? AND behavior = ? AND modifiers = ? ORDER BY start, image_index_start"
786
+ ),
787
+ (subject, behavior, distinct_modifiers),
788
+ )
789
+
790
+ for row in cursor.fetchall():
791
+ media_file_name = cfg.NA
792
+
793
+ if observation[cfg.TYPE] == cfg.MEDIA:
794
+ observation_type = "Media file"
795
+
796
+ # get the media file name of the start of event
797
+ media_file_name = observation_operations.event2media_file_name(observation, row["start"])
798
+ if media_file_name is None:
799
+ media_file_name = "Not found"
800
+
801
+ media_file_str, fps_str, media_durations_str = "", "", ""
802
+
803
+ for player in observation[cfg.FILE]:
804
+ media_file_lst, fps_lst, media_durations_lst = [], [], []
805
+ if observation[cfg.FILE][player]:
806
+ for media_file in observation[cfg.FILE][player]:
807
+ media_file_lst.append(media_file)
808
+ fps_lst.append(f"{observation[cfg.MEDIA_INFO][cfg.FPS].get(media_file, cfg.NA):.3f}")
809
+ media_durations_lst.append(f"{observation[cfg.MEDIA_INFO][cfg.LENGTH].get(media_file, cfg.NA):.3f}")
810
+ if player > "1":
811
+ media_file_str += "|"
812
+ fps_str += "|"
813
+ media_durations_str += "|"
814
+ media_file_str += f"player #{player}:" + ";".join(media_file_lst)
815
+ fps_str += ";".join(fps_lst)
816
+ media_durations_str += ";".join(media_durations_lst)
817
+
818
+ if observation[cfg.TYPE] == cfg.LIVE:
819
+ observation_type = "Live observation"
820
+ media_file_str = cfg.NA
821
+ fps_str = cfg.NA
822
+ media_durations_str = cfg.NA
823
+
824
+ if observation[cfg.TYPE] == cfg.IMAGES:
825
+ observation_type = "From pictures"
826
+ media_file_str = ""
827
+ for dir in observation[cfg.DIRECTORIES_LIST]:
828
+ media_file_str += f"{dir}; "
829
+ fps_str = cfg.NA
830
+ # TODO: number of pictures in each directory
831
+ media_durations_str = cfg.NA
832
+
833
+ row_data = []
834
+
835
+ row_data.extend(
836
+ [
837
+ obsId,
838
+ observation["date"].replace("T", " "),
839
+ obs_description,
840
+ observation_type,
841
+ media_file_str, # list of media used in observation
842
+ pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET],
843
+ f"{coding_duration:.3f}" if not coding_duration.is_nan() else cfg.NA,
844
+ media_durations_str,
845
+ fps_str,
846
+ ]
847
+ )
848
+
849
+ # independent variables
850
+ if cfg.INDEPENDENT_VARIABLES in pj:
851
+ for idx_var in util.sorted_keys(pj[cfg.INDEPENDENT_VARIABLES]):
852
+ if pj[cfg.INDEPENDENT_VARIABLES][idx_var]["label"] in observation[cfg.INDEPENDENT_VARIABLES]:
853
+ var_value = observation[cfg.INDEPENDENT_VARIABLES][pj[cfg.INDEPENDENT_VARIABLES][idx_var]["label"]]
854
+ if pj[cfg.INDEPENDENT_VARIABLES][idx_var]["type"] == "timestamp":
855
+ var_value = var_value.replace("T", " ")
856
+
857
+ row_data.append(var_value)
858
+ else:
859
+ row_data.append("")
860
+
861
+ row_data.extend(
862
+ [
863
+ subject,
864
+ duration_by_subject_by_obs,
865
+ behavior,
866
+ behavioral_category[behavior] if behavioral_category[behavior] else "Not defined",
867
+ ]
868
+ )
869
+
870
+ # modifiers
871
+ if max_modifiers:
872
+ modifiers = row["modifiers"].split("|")
873
+ while len(modifiers) < max_modifiers:
874
+ modifiers.append("")
875
+ for modifier in modifiers:
876
+ row_data.append(modifier)
877
+
878
+ if row["type"] == cfg.POINT:
879
+ row_data.extend(
880
+ [
881
+ cfg.POINT,
882
+ f"{row['start']:.3f}" if row["start"] is not None else cfg.NA, # start
883
+ f"{row['stop']:.3f}" if row["stop"] is not None else cfg.NA, # stop
884
+ cfg.NA, # duration
885
+ media_file_name, # Media file name
886
+ ]
887
+ )
888
+
889
+ if row["type"] == cfg.STATE:
890
+ row_data.extend(
891
+ [
892
+ cfg.STATE,
893
+ f"{row['start']:.3f}" if row["start"] is not None else cfg.NA,
894
+ f"{row['stop']:.3f}" if row["stop"] is not None else cfg.NA,
895
+ # duration
896
+ f"{row['stop'] - row['start']:.3f}" if (row["stop"] is not None) and (row["start"] is not None) else cfg.NA,
897
+ media_file_name, # Media file name
898
+ ]
899
+ )
900
+
901
+ row_data.extend(
902
+ [
903
+ row["image_index_start"],
904
+ row["image_index_stop"],
905
+ row["image_path_start"],
906
+ row["image_path_stop"],
907
+ row["comment"],
908
+ row["comment_stop"] if (row["type"] == cfg.STATE) else "",
909
+ ]
910
+ )
911
+
912
+ data.append(row_data)
913
+
914
+ return data, max_modifiers
915
+
916
+
917
+ def events_to_behavioral_sequences(pj, obs_id: str, subj: str, parameters: dict, behav_seq_separator: str) -> str:
918
+ """
919
+ return the behavioral sequence (behavioral string) for subject in obs_id
920
+
921
+ Args:
922
+ pj (dict): project
923
+ obs_id (str): observation id
924
+ subj (str): subject
925
+ parameters (dict): parameters
926
+ behav_seq_separator (str): separator of behviors in behavioral sequences
927
+
928
+ Returns:
929
+ str: behavioral string for selected subject in selected observation
930
+ """
931
+
932
+ out: str = ""
933
+ current_states: list = []
934
+ # add status (POINT, START, STOP) to event
935
+ events_with_status = project_functions.events_start_stop(
936
+ pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS], pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]
937
+ )
938
+
939
+ for event in events_with_status:
940
+ # check if event in selected behaviors
941
+
942
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in parameters[cfg.SELECTED_BEHAVIORS]:
943
+ continue
944
+
945
+ if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subj or (subj == cfg.NO_FOCAL_SUBJECT and event[cfg.EVENT_SUBJECT_FIELD_IDX] == ""):
946
+ # if event[cfg.EVENT_STATUS_FIELD_IDX] == cfg.POINT:
947
+ if event[-1] == cfg.POINT: # status is last element
948
+ if current_states:
949
+ out += "+".join(current_states) + "+" + event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
950
+ else:
951
+ out += event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
952
+
953
+ if parameters[cfg.INCLUDE_MODIFIERS]:
954
+ out += "&" + event[cfg.EVENT_MODIFIER_FIELD_IDX].replace("|", "+")
955
+
956
+ out += behav_seq_separator
957
+
958
+ # if event[cfg.EVENT_STATUS_FIELD_IDX] == cfg.START:
959
+ if event[-1] == cfg.START: # status is last element
960
+ if parameters[cfg.INCLUDE_MODIFIERS]:
961
+ current_states.append(
962
+ (
963
+ f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]}"
964
+ f"{'&' if event[cfg.EVENT_MODIFIER_FIELD_IDX] else ''}"
965
+ f"{event[cfg.EVENT_MODIFIER_FIELD_IDX].replace('|', ';')}"
966
+ )
967
+ )
968
+ else:
969
+ current_states.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
970
+
971
+ out += "+".join(sorted(current_states))
972
+
973
+ out += behav_seq_separator
974
+
975
+ # if event[cfg.EVENT_STATUS_FIELD_IDX] == cfg.STOP:
976
+ if event[-1] == cfg.STOP:
977
+ if parameters[cfg.INCLUDE_MODIFIERS]:
978
+ behav_modif = (
979
+ f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]}"
980
+ f"{'&' if event[cfg.EVENT_MODIFIER_FIELD_IDX] else ''}"
981
+ f"{event[cfg.EVENT_MODIFIER_FIELD_IDX].replace('|', ';')}"
982
+ )
983
+ else:
984
+ behav_modif = event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
985
+ if behav_modif in current_states:
986
+ current_states.remove(behav_modif)
987
+
988
+ if current_states:
989
+ out += "+".join(sorted(current_states))
990
+
991
+ out += behav_seq_separator
992
+
993
+ # remove last separator (if separator not empty)
994
+ if behav_seq_separator:
995
+ out = out[0 : -len(behav_seq_separator)]
996
+
997
+ return out
998
+
999
+
1000
+ def events_to_behavioral_sequences_all_subj(pj, obs_id: str, subjects_list: list, parameters: dict, behav_seq_separator: str) -> str:
1001
+ """
1002
+ return the behavioral sequences for all selected subjects in obs_id
1003
+
1004
+ Args:
1005
+ pj (dict): project
1006
+ obs_id (str): observation id
1007
+ subjects_list (list): list of subjects
1008
+ parameters (dict): parameters
1009
+ behav_seq_separator (str): separator of behviors in behavioral sequences
1010
+
1011
+ Returns:
1012
+ str: behavioral sequences for all selected subjects in selected observation
1013
+ """
1014
+
1015
+ out = ""
1016
+ current_states = {i: [] for i in subjects_list}
1017
+ events_with_status = project_functions.events_start_stop(
1018
+ pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS], pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]
1019
+ )
1020
+
1021
+ for event in events_with_status:
1022
+ # check if event in selected behaviors
1023
+ if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in parameters[cfg.SELECTED_BEHAVIORS]:
1024
+ continue
1025
+
1026
+ if (event[cfg.EVENT_SUBJECT_FIELD_IDX] in subjects_list) or (
1027
+ event[cfg.EVENT_SUBJECT_FIELD_IDX] == "" and cfg.NO_FOCAL_SUBJECT in subjects_list
1028
+ ):
1029
+ subject = event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
1030
+
1031
+ if event[-1] == cfg.POINT:
1032
+ if current_states[subject]:
1033
+ out += f"[{subject}]" + "+".join(current_states[subject]) + "+" + event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1034
+ else:
1035
+ out += f"[{subject}]" + event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1036
+
1037
+ if parameters[cfg.INCLUDE_MODIFIERS]:
1038
+ out += "&" + event[cfg.EVENT_MODIFIER_FIELD_IDX].replace("|", "+")
1039
+
1040
+ out += behav_seq_separator
1041
+
1042
+ if event[-1] == cfg.START:
1043
+ if parameters[cfg.INCLUDE_MODIFIERS]:
1044
+ current_states[subject].append(
1045
+ (
1046
+ f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]}"
1047
+ f"{'&' if event[cfg.EVENT_MODIFIER_FIELD_IDX] else ''}"
1048
+ f"{event[cfg.EVENT_MODIFIER_FIELD_IDX].replace('|', ';')}"
1049
+ )
1050
+ )
1051
+ else:
1052
+ current_states[subject].append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
1053
+
1054
+ out += f"[{subject}]" + "+".join(sorted(current_states[subject]))
1055
+
1056
+ out += behav_seq_separator
1057
+
1058
+ if event[-1] == cfg.STOP:
1059
+ if parameters[cfg.INCLUDE_MODIFIERS]:
1060
+ behav_modif = (
1061
+ f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]}"
1062
+ f"{'&' if event[cfg.EVENT_MODIFIER_FIELD_IDX] else ''}"
1063
+ f"{event[cfg.EVENT_MODIFIER_FIELD_IDX].replace('|', ';')}"
1064
+ )
1065
+ else:
1066
+ behav_modif = event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
1067
+ if behav_modif in current_states[subject]:
1068
+ current_states[subject].remove(behav_modif)
1069
+
1070
+ if current_states[subject]:
1071
+ out += f"[{subject}]" + "+".join(sorted(current_states[subject]))
1072
+
1073
+ out += behav_seq_separator
1074
+
1075
+ # remove last separator (if separator not empty)
1076
+ if behav_seq_separator:
1077
+ out = out[0 : -len(behav_seq_separator)]
1078
+
1079
+ return out
1080
+
1081
+
1082
+ def events_to_timed_behavioral_sequences(
1083
+ pj: dict, obs_id: str, subject: str, parameters: dict, precision: float, behav_seq_separator: str
1084
+ ) -> str:
1085
+ """
1086
+ return the behavioral string for subject in obsId
1087
+
1088
+ Args:
1089
+ pj (dict): project
1090
+ obs_id (str): observation id
1091
+ subj (str): subject
1092
+ parameters (dict): parameters
1093
+ precision (float): time value for scan sample
1094
+ behav_seq_separator (str): separator of behviors in behavioral sequences
1095
+
1096
+ Returns:
1097
+ str: behavioral string for selected subject in selected observation
1098
+ """
1099
+
1100
+ out: str = ""
1101
+
1102
+ state_behaviors_codes = util.state_behavior_codes(pj[cfg.ETHOGRAM])
1103
+ delta = dec(str(round(precision, 3)))
1104
+ out = ""
1105
+ t = dec("0.000")
1106
+
1107
+ current = []
1108
+ while t < pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0]:
1109
+ """
1110
+ if out:
1111
+ out += behav_seq_separator
1112
+ """
1113
+ csbs = util.get_current_states_modifiers_by_subject(
1114
+ state_behaviors_codes,
1115
+ pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS],
1116
+ {"": {"name": subject}},
1117
+ t,
1118
+ include_modifiers=False,
1119
+ )[""]
1120
+ if csbs:
1121
+ if current:
1122
+ if csbs == current[-1]:
1123
+ current.append("+".join(csbs))
1124
+ else:
1125
+ out.append(current)
1126
+ current = [csbs]
1127
+ else:
1128
+ current = [csbs]
1129
+
1130
+ t += delta
1131
+
1132
+ return out
1133
+
1134
+
1135
+ def observation_to_behavioral_sequences(pj, selected_observations, parameters, behaviors_separator, separated_subjects, timed, file_name):
1136
+ try:
1137
+ with open(file_name, "w", encoding="utf-8") as out_file:
1138
+ for obs_id in selected_observations:
1139
+ # observation id
1140
+ out_file.write("\n" + f"# observation id: {obs_id}" + "\n")
1141
+ # observation description
1142
+ descr = pj[cfg.OBSERVATIONS][obs_id]["description"]
1143
+ if "\r\n" in descr:
1144
+ descr = descr.replace("\r\n", "\n# ")
1145
+ elif "\r" in descr:
1146
+ descr = descr.replace("\r", "\n# ")
1147
+ out_file.write(f"# observation description: {descr}\n\n")
1148
+ # media file name
1149
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
1150
+ out_file.write(f"# Observation type: Media file{os.linesep}")
1151
+ media_file_str = ""
1152
+
1153
+ for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
1154
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
1155
+ media_file_str += f"player #{player}: "
1156
+ # fps_str += f"player #{player}: "
1157
+ for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
1158
+ media_file_str += f"{media_file}; "
1159
+ # fps_str += f"{pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.FPS].get(media_file, cfg.NA):.3f}; "
1160
+
1161
+ out_file.write(f"# Media file path: {media_file_str}\n\n")
1162
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.LIVE:
1163
+ out_file.write(f"# Observation type: Live observation{os.linesep}{os.linesep}")
1164
+
1165
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
1166
+ out_file.write(f"# Observation type: From pictures{os.linesep}{os.linesep}")
1167
+
1168
+ # independent variables
1169
+ if cfg.INDEPENDENT_VARIABLES in pj[cfg.OBSERVATIONS][obs_id]:
1170
+ out_file.write("# Independent variables\n")
1171
+
1172
+ for variable in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
1173
+ out_file.write(f"# {variable}: {pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][variable]}\n")
1174
+ out_file.write("\n")
1175
+
1176
+ # one sequence for all subjects
1177
+ if not separated_subjects:
1178
+ out = events_to_behavioral_sequences_all_subj(
1179
+ pj, obs_id, parameters[cfg.SELECTED_SUBJECTS], parameters, behaviors_separator
1180
+ )
1181
+ if out:
1182
+ out_file.write(out + "\n")
1183
+
1184
+ # one sequence by subject
1185
+ if separated_subjects:
1186
+ # selected subjects
1187
+ for subject in parameters[cfg.SELECTED_SUBJECTS]:
1188
+ out_file.write(f"\n# {subject if subject else cfg.NO_FOCAL_SUBJECT}:\n")
1189
+
1190
+ if not timed:
1191
+ out = events_to_behavioral_sequences(pj, obs_id, subject, parameters, behaviors_separator)
1192
+
1193
+ if timed:
1194
+ out = events_to_timed_behavioral_sequences(pj, obs_id, subject, parameters, 0.001, behaviors_separator)
1195
+
1196
+ if out:
1197
+ out_file.write(out + "\n")
1198
+
1199
+ return True, ""
1200
+
1201
+ except Exception:
1202
+ error_type, error_file_name, error_lineno = util.error_info(sys.exc_info())
1203
+ return False, f"{error_type} {error_file_name} {error_lineno}"