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

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

Potentially problematic release.


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

Files changed (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +25 -33
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +627 -236
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +95 -29
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/export_events.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -38,14 +38,14 @@ from . import project_functions
38
38
  from . import dialog
39
39
  from . import db_functions
40
40
 
41
- from PyQt5.QtWidgets import QApplication, QFileDialog, QMessageBox, QInputDialog
41
+ from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QInputDialog
42
42
 
43
43
 
44
44
  def export_events_as_behavioral_sequences(self, separated_subjects=False, timed=False):
45
45
  """
46
46
  export events from selected observations by subject as behavioral sequences (plain text file)
47
47
  behaviors are separated by character specified in self.behav_seq_separator (usually pipe |)
48
- for use with Behatrix (see http://www.boris.unito.it/pages/behatrix)
48
+ for use with Behatrix (see https://www.boris.unito.it/pages/behatrix)
49
49
 
50
50
  Args:
51
51
  separated_subjects (bool):
@@ -70,24 +70,24 @@ def export_events_as_behavioral_sequences(self, separated_subjects=False, timed=
70
70
  return
71
71
 
72
72
  if len(selected_observations) == 1:
73
- max_media_duration_all_obs, _ = observation_operations.media_duration(
74
- self.pj[cfg.OBSERVATIONS], selected_observations
75
- )
76
- start_coding, end_coding, _ = observation_operations.coding_time(
77
- self.pj[cfg.OBSERVATIONS], selected_observations
78
- )
73
+ max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
74
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
75
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
79
76
  else:
80
77
  max_media_duration_all_obs = None
81
78
  start_coding, end_coding = dec("NaN"), dec("NaN")
79
+ start_interval, end_interval = None, None
82
80
 
83
81
  parameters = select_subj_behav.choose_obs_subj_behav_category(
84
82
  self,
85
83
  selected_observations,
86
84
  start_coding=start_coding,
87
85
  end_coding=end_coding,
86
+ start_interval=start_interval,
87
+ end_interval=end_interval,
88
88
  maxTime=max_media_duration_all_obs,
89
- flagShowIncludeModifiers=True,
90
- flagShowExcludeBehaviorsWoEvents=False,
89
+ show_include_modifiers=True,
90
+ show_exclude_non_coded_behaviors=False,
91
91
  n_observations=len(selected_observations),
92
92
  )
93
93
 
@@ -97,30 +97,28 @@ def export_events_as_behavioral_sequences(self, separated_subjects=False, timed=
97
97
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
98
98
  return
99
99
 
100
- fn = QFileDialog().getSaveFileName(
101
- self, "Export events as behavioral sequences", "", "Text files (*.txt);;All files (*)"
100
+ file_name, _ = QFileDialog.getSaveFileName(self, "Export events as behavioral sequences", "", "Text files (*.txt);;All files (*)")
101
+
102
+ if not file_name:
103
+ return
104
+ r, msg = export_observation.observation_to_behavioral_sequences(
105
+ pj=self.pj,
106
+ selected_observations=selected_observations,
107
+ parameters=parameters,
108
+ behaviors_separator=self.behav_seq_separator,
109
+ separated_subjects=separated_subjects,
110
+ timed=timed,
111
+ file_name=file_name,
102
112
  )
103
- file_name = fn[0] if type(fn) is tuple else fn
104
-
105
- if file_name:
106
- r, msg = export_observation.observation_to_behavioral_sequences(
107
- pj=self.pj,
108
- selected_observations=selected_observations,
109
- parameters=parameters,
110
- behaviors_separator=self.behav_seq_separator,
111
- separated_subjects=separated_subjects,
112
- timed=timed,
113
- file_name=file_name,
113
+ if not r:
114
+ logging.critical(f"Error while exporting events as behavioral sequences: {msg}")
115
+ QMessageBox.critical(
116
+ None,
117
+ cfg.programName,
118
+ f"Error while exporting events as behavioral sequences:<br>{msg}",
119
+ QMessageBox.Ok | QMessageBox.Default,
120
+ QMessageBox.NoButton,
114
121
  )
115
- if not r:
116
- logging.critical(f"Error while exporting events as behavioral sequences: {msg}")
117
- QMessageBox.critical(
118
- None,
119
- cfg.programName,
120
- f"Error while exporting events as behavioral sequences:<br>{msg}",
121
- QMessageBox.Ok | QMessageBox.Default,
122
- QMessageBox.NoButton,
123
- )
124
122
 
125
123
 
126
124
  def export_tabular_events(self, mode: str = "tabular") -> None:
@@ -165,24 +163,24 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
165
163
  return
166
164
 
167
165
  if len(selected_observations) == 1:
168
- max_media_duration_all_obs, _ = observation_operations.media_duration(
169
- self.pj[cfg.OBSERVATIONS], selected_observations
170
- )
171
- start_coding, end_coding, _ = observation_operations.coding_time(
172
- self.pj[cfg.OBSERVATIONS], selected_observations
173
- )
166
+ max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
167
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
168
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
174
169
  else:
175
170
  max_media_duration_all_obs = None
176
171
  start_coding, end_coding = dec("NaN"), dec("NaN")
172
+ start_interval, end_interval = None, None
177
173
 
178
174
  parameters = select_subj_behav.choose_obs_subj_behav_category(
179
175
  self,
180
176
  selected_observations,
181
177
  start_coding=start_coding,
182
178
  end_coding=end_coding,
179
+ start_interval=start_interval,
180
+ end_interval=end_interval,
183
181
  maxTime=max_media_duration_all_obs,
184
- flagShowIncludeModifiers=False,
185
- flagShowExcludeBehaviorsWoEvents=False,
182
+ show_include_modifiers=False,
183
+ show_exclude_non_coded_behaviors=False,
186
184
  n_observations=len(selected_observations),
187
185
  )
188
186
  if parameters == {}:
@@ -203,7 +201,6 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
203
201
  cfg.RDS,
204
202
  )
205
203
  if len(selected_observations) > 1: # choose directory for exporting observations
206
-
207
204
  item, ok = QInputDialog.getItem(
208
205
  self,
209
206
  "Export events format",
@@ -226,8 +223,12 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
226
223
  return
227
224
 
228
225
  if len(selected_observations) == 1:
226
+ file_dialog_options = QFileDialog.Options()
227
+ file_dialog_options |= QFileDialog.DontConfirmOverwrite
229
228
 
230
- file_name, filter_ = QFileDialog().getSaveFileName(self, "Export events", "", ";;".join(available_formats))
229
+ file_name, filter_ = QFileDialog().getSaveFileName(
230
+ self, "Export events", "", ";;".join(available_formats), options=file_dialog_options
231
+ )
231
232
  if not file_name:
232
233
  return
233
234
 
@@ -235,11 +236,9 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
235
236
  if pl.Path(file_name).suffix != "." + output_format:
236
237
  file_name = str(pl.Path(file_name)) + "." + output_format
237
238
  # check if file with new extension already exists
238
- if pl.Path(file_name).is_file():
239
+ if pl.Path(file_name).exists():
239
240
  if (
240
- dialog.MessageDialog(
241
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
242
- )
241
+ dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
243
242
  == cfg.CANCEL
244
243
  ):
245
244
  return
@@ -297,21 +296,23 @@ def export_aggregated_events(self):
297
296
  - select subjects and behaviors
298
297
  - export events in aggregated format
299
298
 
300
- Formats can be SQL (sql), SDIS (sds) or Tabular format (tsv, csv, ods, xlsx, xls, html)
299
+ Formats can be SQL (sql), SDIS (sds), Tabular format (tsv, csv, ods, xlsx, xls, html) or Pandas dataframe
301
300
  """
302
301
 
303
302
  def fields_type(max_modif_number: int) -> dict:
304
-
305
303
  fields_type_dict: dict = {
306
304
  "Observation id": str,
307
305
  "Observation date": dt.datetime,
308
306
  "Description": str,
309
307
  "Observation type": str,
310
308
  "Source": str,
311
- "Total duration": float,
312
- "Media duration (s)": float,
313
- "FPS (frame/s)": float,
309
+ "Time offset (s)": str,
310
+ "Coding duration": float,
311
+ "Media duration (s)": str,
312
+ "FPS (frame/s)": str,
314
313
  }
314
+ # TODO: "Media duration (s)" and "FPS (frame/s)" can be float for observation from 1 video
315
+
315
316
  if cfg.INDEPENDENT_VARIABLES in self.pj:
316
317
  for idx in util.sorted_keys(self.pj[cfg.INDEPENDENT_VARIABLES]):
317
318
  if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "timestamp":
@@ -331,16 +332,6 @@ def export_aggregated_events(self):
331
332
  )
332
333
 
333
334
  # max number of modifiers
334
- """
335
- max_modif_number = max(
336
- [
337
- len(self.pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
338
- for idx in self.pj[cfg.ETHOGRAM]
339
- if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] in parameters[cfg.SELECTED_BEHAVIORS]
340
- ]
341
- )
342
- """
343
-
344
335
  for i in range(max_modif_number):
345
336
  fields_type_dict[f"Modifier #{i + 1}"] = str
346
337
 
@@ -362,9 +353,7 @@ def export_aggregated_events(self):
362
353
 
363
354
  return fields_type_dict
364
355
 
365
- _, selected_observations = select_observations.select_observations2(
366
- self, cfg.MULTIPLE, "Select observations for exporting events"
367
- )
356
+ _, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, "Select observations for exporting events")
368
357
  if not selected_observations:
369
358
  return
370
359
 
@@ -378,44 +367,40 @@ def export_aggregated_events(self):
378
367
  return
379
368
 
380
369
  if len(selected_observations) == 1:
381
- max_media_duration_all_obs, _ = observation_operations.media_duration(
382
- self.pj[cfg.OBSERVATIONS], selected_observations
383
- )
384
- start_coding, end_coding, _ = observation_operations.coding_time(
385
- self.pj[cfg.OBSERVATIONS], selected_observations
386
- )
370
+ max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
371
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
372
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
387
373
  else:
388
374
  max_media_duration_all_obs = None
389
375
  start_coding, end_coding = dec("NaN"), dec("NaN")
376
+ start_interval, end_interval = None, None
390
377
 
391
378
  parameters = select_subj_behav.choose_obs_subj_behav_category(
392
379
  self,
393
380
  selected_observations,
394
381
  start_coding=start_coding,
395
382
  end_coding=end_coding,
383
+ start_interval=start_interval,
384
+ end_interval=end_interval,
396
385
  maxTime=max_media_duration_all_obs,
397
- flagShowIncludeModifiers=False,
398
- flagShowExcludeBehaviorsWoEvents=False,
386
+ show_include_modifiers=False,
387
+ show_exclude_non_coded_behaviors=False,
399
388
  n_observations=len(selected_observations),
400
389
  )
401
390
  if parameters == {}:
402
391
  return
403
392
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
404
- QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
393
+ QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
405
394
  return
406
395
 
407
396
  # check for grouping results
408
397
  flag_group = True
409
398
  if len(selected_observations) > 1:
410
399
  flag_group = (
411
- dialog.MessageDialog(
412
- cfg.programName, "Group events from selected observations in one file?", [cfg.YES, cfg.NO]
413
- )
414
- == cfg.YES
400
+ dialog.MessageDialog(cfg.programName, "Group events from selected observations in one file?", [cfg.YES, cfg.NO]) == cfg.YES
415
401
  )
416
402
 
417
403
  if flag_group:
418
-
419
404
  file_formats = (
420
405
  cfg.TSV,
421
406
  cfg.CSV,
@@ -430,7 +415,12 @@ def export_aggregated_events(self):
430
415
  cfg.RDS,
431
416
  )
432
417
 
433
- fileName, filter_ = QFileDialog().getSaveFileName(self, "Export aggregated events", "", ";;".join(file_formats))
418
+ file_dialog_options = QFileDialog.Options()
419
+ file_dialog_options |= QFileDialog.DontConfirmOverwrite
420
+
421
+ fileName, filter_ = QFileDialog().getSaveFileName(
422
+ self, "Export aggregated events", "", ";;".join(file_formats), options=file_dialog_options
423
+ )
434
424
 
435
425
  if not fileName:
436
426
  return
@@ -439,17 +429,11 @@ def export_aggregated_events(self):
439
429
  if pl.Path(fileName).suffix != "." + outputFormat:
440
430
  # check if file with new extension already exists
441
431
  fileName = str(pl.Path(fileName)) + "." + outputFormat
442
- if pl.Path(fileName).is_file():
443
- if (
444
- dialog.MessageDialog(
445
- cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
446
- )
447
- == cfg.CANCEL
448
- ):
432
+ if pl.Path(fileName).exists():
433
+ if dialog.MessageDialog(cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
449
434
  return
450
435
 
451
436
  else: # not grouping
452
-
453
437
  file_formats = (
454
438
  cfg.TSV,
455
439
  cfg.CSV,
@@ -474,7 +458,7 @@ def export_aggregated_events(self):
474
458
  if not exportDir:
475
459
  return
476
460
 
477
- if outputFormat == "sql":
461
+ if outputFormat == cfg.SQL_EXT:
478
462
  _, _, conn = db_functions.load_aggregated_events_in_db(
479
463
  self.pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
480
464
  )
@@ -483,7 +467,6 @@ def export_aggregated_events(self):
483
467
  for line in conn.iterdump():
484
468
  f.write(f"{line}\n")
485
469
  except Exception:
486
-
487
470
  QMessageBox.critical(
488
471
  None,
489
472
  cfg.programName,
@@ -495,50 +478,50 @@ def export_aggregated_events(self):
495
478
  return
496
479
 
497
480
  # compute the maximum number of modifiers
498
- tot_max_modifiers = 0
481
+ tot_max_modifiers: int = 0
499
482
  for obs_id in selected_observations:
500
483
  _, max_modifiers = export_observation.export_aggregated_events(self.pj, parameters, obs_id)
501
484
  tot_max_modifiers = max(tot_max_modifiers, max_modifiers)
502
485
 
486
+ logging.debug(f"tot_max_modifiers: {tot_max_modifiers}")
487
+
503
488
  data_grouped_obs = tablib.Dataset()
504
489
 
505
490
  mem_command: str = "" # remember user choice when file already exists
506
491
  header = list(fields_type(tot_max_modifiers).keys())
507
492
 
508
493
  for obs_id in selected_observations:
509
-
510
494
  logging.debug(f"Exporting aggregated events for obs Id: {obs_id}")
511
495
 
512
- data_single_obs, max_modifiers = export_observation.export_aggregated_events(self.pj, parameters, obs_id)
496
+ data_single_obs, _ = export_observation.export_aggregated_events(
497
+ self.pj, parameters, obs_id, force_number_modifiers=tot_max_modifiers
498
+ )
513
499
 
514
500
  try:
515
501
  # order by start time
516
502
  index = header.index("Start (s)")
517
503
  if cfg.NA not in [x[index] for x in list(data_single_obs)]:
518
504
  data_single_obs_sorted = tablib.Dataset(
519
- # *data.sort(col=index),
520
505
  *sorted(list(data_single_obs), key=lambda x: float(x[index])),
521
- headers=list(fields_type(max_modifiers).keys()),
506
+ headers=list(fields_type(tot_max_modifiers).keys()),
522
507
  )
523
508
  else:
524
509
  # order by image index
525
510
  index = header.index("Image index start")
526
511
  data_single_obs_sorted = tablib.Dataset(
527
- # *data.sort(col=index),
528
512
  *sorted(list(data_single_obs), key=lambda x: float(x[index])),
529
- headers=list(fields_type(max_modifiers).keys()),
513
+ headers=list(fields_type(tot_max_modifiers).keys()),
530
514
  )
531
515
  except Exception:
532
516
  # if error no order
533
517
  data_single_obs_sorted = tablib.Dataset(
534
- # data.sort(col=0), # Observation id
535
518
  *list(data_single_obs),
536
- headers=list(fields_type(max_modifiers).keys()),
519
+ headers=list(fields_type(tot_max_modifiers).keys()),
537
520
  )
538
521
 
539
522
  data_single_obs_sorted.title = obs_id
540
523
 
541
- if (not flag_group) and (outputFormat not in (cfg.SDIS_EXT, "tbs")):
524
+ if (not flag_group) and (outputFormat not in (cfg.SDIS_EXT, cfg.TBS_EXT)):
542
525
  fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
543
526
  # check if file with new extension already exists
544
527
  if mem_command != cfg.OVERWRITE_ALL and pl.Path(fileName).is_file():
@@ -554,12 +537,12 @@ def export_aggregated_events(self):
554
537
  if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
555
538
  continue
556
539
 
557
- r, msg = export_observation.dataset_write(data_single_obs_sorted, fileName, outputFormat, dtype=fields_type)
540
+ r, msg = export_observation.dataset_write(data_single_obs_sorted, fileName, outputFormat, dtype=fields_type(max_modifiers))
558
541
  if not r:
559
- QMessageBox.warning(
560
- None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton
561
- )
542
+ QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
562
543
 
544
+ """
545
+ # disabled after introduction of the force_number_modifiers parameter in export_aggregated_events function
563
546
  if len(data_single_obs_sorted) and max_modifiers < tot_max_modifiers:
564
547
  for i in range(tot_max_modifiers - max_modifiers):
565
548
  data_single_obs_sorted.insert_col(
@@ -567,9 +550,12 @@ def export_aggregated_events(self):
567
550
  col=[""] * (len(list(data_single_obs_sorted))),
568
551
  header=f"Modif #{i}",
569
552
  )
553
+ """
554
+
570
555
  data_grouped_obs.extend(data_single_obs_sorted)
571
556
 
572
557
  data_grouped_obs_all = tablib.Dataset(headers=list(fields_type(tot_max_modifiers).keys()))
558
+
573
559
  data_grouped_obs_all.extend(data_grouped_obs)
574
560
  data_grouped_obs_all.title = "Aggregated events"
575
561
 
@@ -613,10 +599,7 @@ def export_aggregated_events(self):
613
599
  return
614
600
 
615
601
  if outputFormat == cfg.SDIS_EXT: # SDIS format
616
- out: str = (
617
- "% SDIS file created by BORIS (www.boris.unito.it) "
618
- f"at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
619
- )
602
+ out: str = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
620
603
  for obs_id in selected_observations:
621
604
  # observation id
622
605
  out += "\n<{}>\n".format(obs_id)
@@ -642,10 +625,7 @@ def export_aggregated_events(self):
642
625
  fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
643
626
  with open(fileName, "wb") as f:
644
627
  f.write(str.encode(out))
645
- out = (
646
- "% SDIS file created by BORIS (www.boris.unito.it) "
647
- f"at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
648
- )
628
+ out = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
649
629
 
650
630
  if flag_group:
651
631
  with open(fileName, "wb") as f:
@@ -653,7 +633,7 @@ def export_aggregated_events(self):
653
633
  return
654
634
 
655
635
  if flag_group:
656
- r, msg = export_observation.dataset_write(data_grouped_obs_all, fileName, outputFormat, dtype=fields_type)
636
+ r, msg = export_observation.dataset_write(data_grouped_obs_all, fileName, outputFormat, dtype=fields_type(max_modifiers))
657
637
  if not r:
658
638
  QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
659
639
 
@@ -694,13 +674,19 @@ def export_events_as_textgrid(self) -> None:
694
674
 
695
675
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
696
676
 
677
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
678
+
697
679
  parameters = select_subj_behav.choose_obs_subj_behav_category(
698
680
  self,
699
681
  selected_observations,
700
682
  start_coding=start_coding,
701
683
  end_coding=end_coding,
702
- flagShowIncludeModifiers=False,
703
- flagShowExcludeBehaviorsWoEvents=False,
684
+ # start_interval=start_interval,
685
+ # end_interval=end_interval,
686
+ start_interval=None,
687
+ end_interval=None,
688
+ show_include_modifiers=False,
689
+ show_exclude_non_coded_behaviors=False,
704
690
  maxTime=max_obs_length,
705
691
  n_observations=len(selected_observations),
706
692
  )
@@ -710,13 +696,13 @@ def export_events_as_textgrid(self) -> None:
710
696
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
711
697
  return
712
698
 
713
- export_dir = QFileDialog(self).getExistingDirectory(
714
- self, "Export events as Praat TextGrid", os.path.expanduser("~"), options=QFileDialog(self).ShowDirsOnly
699
+ export_dir = QFileDialog.getExistingDirectory(
700
+ self, "Export events as Praat TextGrid", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly
715
701
  )
716
702
  if not export_dir:
717
703
  return
718
704
 
719
- mem_command = ""
705
+ mem_command: str = ""
720
706
 
721
707
  # see https://www.fon.hum.uva.nl/praat/manual/TextGrid_file_formats.html
722
708
 
@@ -729,12 +715,7 @@ def export_events_as_textgrid(self) -> None:
729
715
  " intervals: size = {intervalsSize}\n"
730
716
  )
731
717
 
732
- interval_template = (
733
- " intervals [{count}]:\n"
734
- " xmin = {xmin}\n"
735
- " xmax = {xmax}\n"
736
- ' text = "{name}"\n'
737
- )
718
+ interval_template = ' intervals [{count}]:\n xmin = {xmin}\n xmax = {xmax}\n text = "{name}"\n'
738
719
 
739
720
  point_subject_header = (
740
721
  " item [{subject_index}]:\n"
@@ -745,7 +726,7 @@ def export_events_as_textgrid(self) -> None:
745
726
  " points: size = {intervalsSize}\n"
746
727
  )
747
728
 
748
- point_template = " points [{count}]:\n" " number = {number}\n" ' mark = "{mark}"\n'
729
+ point_template = ' points [{count}]:\n number = {number}\n mark = "{mark}"\n'
749
730
 
750
731
  # widget for results
751
732
  self.results = dialog.Results_dialog()
@@ -757,7 +738,7 @@ def export_events_as_textgrid(self) -> None:
757
738
  )
758
739
 
759
740
  if db_connector is None:
760
- logging.critical(f"Error when loading aggregated events in DB")
741
+ logging.critical("Error when loading aggregated events in DB")
761
742
  return
762
743
 
763
744
  cursor = db_connector.cursor()
@@ -765,11 +746,8 @@ def export_events_as_textgrid(self) -> None:
765
746
  file_count: int = 0
766
747
 
767
748
  for obs_id in selected_observations:
768
-
769
749
  if parameters["time"] == cfg.TIME_EVENTS:
770
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
771
- self.pj[cfg.OBSERVATIONS], [obs_id]
772
- )
750
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
773
751
  if start_coding is None and end_coding is None: # no events
774
752
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
775
753
  QApplication.processEvents()
@@ -784,7 +762,6 @@ def export_events_as_textgrid(self) -> None:
784
762
  max_time = float(end_coding)
785
763
 
786
764
  if parameters["time"] == cfg.TIME_FULL_OBS:
787
-
788
765
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
789
766
  max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
790
767
  min_time = float(0)
@@ -792,9 +769,7 @@ def export_events_as_textgrid(self) -> None:
792
769
  coding_duration = max_media_duration
793
770
 
794
771
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
795
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
796
- self.pj[cfg.OBSERVATIONS], [obs_id]
797
- )
772
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
798
773
  if start_coding is None and end_coding is None: # no events
799
774
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
800
775
  QApplication.processEvents()
@@ -811,7 +786,16 @@ def export_events_as_textgrid(self) -> None:
811
786
  min_time = float(parameters[cfg.START_TIME])
812
787
  max_time = float(parameters[cfg.END_TIME])
813
788
 
789
+ if parameters["time"] == cfg.TIME_OBS_INTERVAL:
790
+ max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
791
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
792
+ offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
793
+ min_time = float(obs_interval[0]) + offset
794
+ # Use max media duration for max time if no interval is defined (=0)
795
+ max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(max_media_duration)
796
+
814
797
  # delete events outside time interval
798
+
815
799
  cursor.execute(
816
800
  "DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
817
801
  (
@@ -855,37 +839,30 @@ def export_events_as_textgrid(self) -> None:
855
839
 
856
840
  next_obs: bool = False
857
841
 
858
- """
859
- total_media_duration = round(
860
- observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id]), 3
861
- )
862
- """
863
-
842
+ # number of items for size parameter
864
843
  cursor.execute(
865
844
  (
866
- "SELECT COUNT(DISTINCT subject) FROM aggregated_events "
867
- "WHERE observation = ? AND subject IN ({}) AND type = 'STATE' ".format(
868
- ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS]))
869
- )
845
+ "SELECT COUNT(*) FROM (SELECT * FROM aggregated_events "
846
+ f"WHERE observation = ? AND subject IN ({','.join(['?'] * len(parameters[cfg.SELECTED_SUBJECTS]))}) GROUP BY subject, behavior) "
870
847
  ),
871
848
  [obs_id] + parameters[cfg.SELECTED_SUBJECTS],
872
849
  )
873
850
 
874
- subjectsNum = int(cursor.fetchone()[0])
875
- subjectsMin, subjectsMax = min_time, max_time
851
+ subjects_num = int(cursor.fetchone()[0])
852
+ subjects_max = max_time
876
853
 
877
854
  out = (
878
855
  'File type = "ooTextFile"\n'
879
856
  'Object class = "TextGrid"\n'
880
857
  "\n"
881
858
  f"xmin = 0.0\n"
882
- f"xmax = {subjectsMax}\n"
859
+ f"xmax = {subjects_max}\n"
883
860
  "tiers? <exists>\n"
884
- f"size = {subjectsNum}\n"
861
+ f"size = {subjects_num}\n"
885
862
  "item []:\n"
886
863
  )
887
864
 
888
- subject_index = 0
865
+ subject_index: int = 0
889
866
  for subject in parameters[cfg.SELECTED_SUBJECTS]:
890
867
  if subject not in [
891
868
  x[cfg.EVENT_SUBJECT_FIELD_IDX] if x[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
@@ -893,7 +870,8 @@ def export_events_as_textgrid(self) -> None:
893
870
  ]:
894
871
  continue
895
872
 
896
- intervalsMin, intervalsMax = min_time, max_time
873
+ intervalsMin = min_time
874
+ intervalsMax = max_time
897
875
 
898
876
  # STATE events
899
877
  cursor.execute(
@@ -908,79 +886,70 @@ def export_events_as_textgrid(self) -> None:
908
886
  {"start": util.float2decimal(r["start"]), "stop": util.float2decimal(r["stop"]), "code": r["behavior"]}
909
887
  for r in cursor.fetchall()
910
888
  ]
911
- if not rows:
912
- continue
913
-
914
- out += interval_subject_header
889
+ if rows:
890
+ out += interval_subject_header
915
891
 
916
- count = 0
892
+ count = 0
917
893
 
918
- # check if 1st behavior starts at the beginning
919
- if rows[0]["start"] > 0:
920
- count += 1
921
- out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
894
+ # check if 1st behavior starts at the beginning
895
+ if rows[0]["start"] > 0:
896
+ count += 1
897
+ out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
898
+
899
+ for idx, row in enumerate(rows):
900
+ # check if events are overlapping
901
+ if (idx + 1 < len(rows)) and (row["stop"] > rows[idx + 1]["start"]):
902
+ self.results.ptText.appendHtml(
903
+ (
904
+ f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
905
+ "It is not possible to create the Praat TextGrid file."
906
+ )
907
+ )
908
+ QApplication.processEvents()
922
909
 
923
- for idx, row in enumerate(rows):
910
+ next_obs = True
911
+ break
924
912
 
925
- # check if events are overlapping
926
- if (idx + 1 < len(rows)) and (row["stop"] > rows[idx + 1]["start"]):
913
+ count += 1
927
914
 
928
- self.results.ptText.appendHtml(
929
- (
930
- f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
931
- "It is not possible to create the Praat TextGrid file."
915
+ if (idx + 1 < len(rows)) and (rows[idx + 1]["start"] - dec("0.001") <= row["stop"] < rows[idx + 1]["start"]):
916
+ xmax = rows[idx + 1]["start"]
917
+ else:
918
+ xmax = row["stop"]
919
+
920
+ out += interval_template.format(count=count, name=row["code"], xmin=row["start"], xmax=xmax)
921
+
922
+ # check if no behavior
923
+ if (idx + 1 < len(rows)) and (row["stop"] < rows[idx + 1]["start"] - dec("0.001")):
924
+ count += 1
925
+ out += interval_template.format(
926
+ count=count,
927
+ name="null",
928
+ xmin=row["stop"],
929
+ xmax=rows[idx + 1]["start"],
932
930
  )
933
- )
934
- QApplication.processEvents()
935
931
 
936
- next_obs = True
932
+ if next_obs:
937
933
  break
938
934
 
939
- count += 1
940
-
941
- if (idx + 1 < len(rows)) and (
942
- rows[idx + 1]["start"] - dec("0.001") <= row["stop"] < rows[idx + 1]["start"]
943
- ):
944
- xmax = rows[idx + 1]["start"]
945
- else:
946
- xmax = row["stop"]
947
-
948
- out += interval_template.format(count=count, name=row["code"], xmin=row["start"], xmax=xmax)
949
-
950
- # check if no behavior
951
- if (idx + 1 < len(rows)) and (row["stop"] < rows[idx + 1]["start"] - dec("0.001")):
935
+ # check if last event ends at the end of media file
936
+ if rows[-1]["stop"] < max_time:
952
937
  count += 1
953
- out += interval_template.format(
954
- count=count,
955
- name="null",
956
- xmin=row["stop"],
957
- xmax=rows[idx + 1]["start"],
958
- )
959
-
960
- if next_obs:
961
- break
962
-
963
- # check if last event ends at the end of media file
964
- if rows[-1]["stop"] < max_time:
965
- count += 1
966
- out += interval_template.format(count=count, name="null", xmin=rows[-1]["stop"], xmax=max_time)
967
-
968
- # add info
969
- subject_index += 1
970
- out = out.format(
971
- subject_index=subject_index,
972
- subject=subject,
973
- intervalsSize=count,
974
- intervalsMin=intervalsMin,
975
- intervalsMax=intervalsMax,
976
- )
938
+ out += interval_template.format(count=count, name="null", xmin=rows[-1]["stop"], xmax=max_time)
939
+
940
+ # add info
941
+ subject_index += 1
942
+ out = out.format(
943
+ subject_index=subject_index,
944
+ subject=subject,
945
+ intervalsSize=count,
946
+ intervalsMin=intervalsMin,
947
+ intervalsMax=intervalsMax,
948
+ )
977
949
 
978
950
  # POINT events
979
951
  cursor.execute(
980
- (
981
- "SELECT start, behavior FROM aggregated_events "
982
- "WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"
983
- ),
952
+ ("SELECT start, behavior FROM aggregated_events WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"),
984
953
  (obs_id, subject),
985
954
  )
986
955
 
@@ -993,7 +962,6 @@ def export_events_as_textgrid(self) -> None:
993
962
  count = 0
994
963
 
995
964
  for idx, row in enumerate(rows):
996
-
997
965
  count += 1
998
966
  out += point_template.format(count=count, mark=row["code"], number=row["start"])
999
967
 
@@ -1011,15 +979,12 @@ def export_events_as_textgrid(self) -> None:
1011
979
  continue
1012
980
 
1013
981
  # check if file already exists
1014
- if (
1015
- mem_command != cfg.OVERWRITE_ALL
1016
- and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid").is_file()
1017
- ):
982
+ if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid").is_file():
1018
983
  if mem_command == cfg.SKIP_ALL:
1019
984
  continue
1020
985
  mem_command = dialog.MessageDialog(
1021
986
  cfg.programName,
1022
- f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid</b> already exists.",
987
+ f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid</b> already exists.",
1023
988
  [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
1024
989
  )
1025
990
  if mem_command == cfg.CANCEL:
@@ -1028,17 +993,13 @@ def export_events_as_textgrid(self) -> None:
1028
993
  continue
1029
994
 
1030
995
  try:
1031
- with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid", "w") as f:
996
+ with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid", "w") as f:
1032
997
  f.write(out)
1033
998
  file_count += 1
1034
- self.results.ptText.appendHtml(
1035
- f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid was created."
1036
- )
999
+ self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid was created.")
1037
1000
  QApplication.processEvents()
1038
1001
  except Exception:
1039
- self.results.ptText.appendHtml(
1040
- f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid can not be created."
1041
- )
1002
+ self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid can not be created.")
1042
1003
  QApplication.processEvents()
1043
1004
 
1044
1005
  self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")