boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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 (126) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +266 -144
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_export_to_feral.py +225 -0
  9. boris/analysis_plugins/_latency.py +59 -0
  10. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  11. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  13. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  14. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  15. boris/analysis_plugins/number_of_occurences.py +22 -0
  16. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  17. boris/analysis_plugins/time_budget.py +61 -0
  18. boris/behav_coding_map_creator.py +235 -236
  19. boris/behavior_binary_table.py +33 -50
  20. boris/behaviors_coding_map.py +17 -18
  21. boris/boris_cli.py +6 -25
  22. boris/cmd_arguments.py +12 -1
  23. boris/coding_pad.py +19 -36
  24. boris/config.py +109 -50
  25. boris/config_file.py +58 -67
  26. boris/connections.py +105 -58
  27. boris/converters.py +13 -37
  28. boris/converters_ui.py +187 -110
  29. boris/cooccurence.py +250 -0
  30. boris/core.py +2174 -1303
  31. boris/core_qrc.py +15892 -10829
  32. boris/core_ui.py +941 -806
  33. boris/db_functions.py +17 -42
  34. boris/dev.py +27 -7
  35. boris/dialog.py +461 -242
  36. boris/duration_widget.py +9 -14
  37. boris/edit_event.py +61 -31
  38. boris/edit_event_ui.py +208 -97
  39. boris/event_operations.py +405 -281
  40. boris/events_cursor.py +25 -17
  41. boris/events_snapshots.py +36 -82
  42. boris/exclusion_matrix.py +4 -9
  43. boris/export_events.py +180 -203
  44. boris/export_observation.py +60 -73
  45. boris/external_processes.py +123 -98
  46. boris/geometric_measurement.py +427 -218
  47. boris/gui_utilities.py +91 -14
  48. boris/image_overlay.py +4 -4
  49. boris/import_observations.py +190 -98
  50. boris/ipc_mpv.py +325 -0
  51. boris/irr.py +20 -57
  52. boris/latency.py +31 -24
  53. boris/measurement_widget.py +14 -18
  54. boris/media_file.py +17 -19
  55. boris/menu_options.py +16 -6
  56. boris/modifier_coding_map_creator.py +1013 -0
  57. boris/modifiers_coding_map.py +7 -9
  58. boris/mpv2.py +128 -35
  59. boris/observation.py +501 -211
  60. boris/observation_operations.py +1037 -393
  61. boris/observation_ui.py +573 -363
  62. boris/observations_list.py +51 -58
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +45 -59
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +91 -56
  67. boris/plot_data_module.py +20 -53
  68. boris/plot_events.py +56 -153
  69. boris/plot_events_rt.py +16 -30
  70. boris/plot_spectrogram_rt.py +83 -56
  71. boris/plot_waveform_rt.py +27 -49
  72. boris/plugins.py +468 -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 +307 -123
  80. boris/preferences_ui.py +686 -227
  81. boris/project.py +294 -271
  82. boris/project_functions.py +626 -537
  83. boris/project_import_export.py +204 -213
  84. boris/project_ui.py +673 -441
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +62 -90
  88. boris/select_observations.py +19 -197
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +51 -33
  91. boris/subjects_pad.py +7 -9
  92. boris/synthetic_time_budget.py +42 -26
  93. boris/time_budget_functions.py +169 -169
  94. boris/time_budget_widget.py +77 -89
  95. boris/transitions.py +41 -41
  96. boris/utilities.py +594 -226
  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 +86 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +240 -136
  104. boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
  106. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.12.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 -37
  112. boris/core.ui +0 -1571
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -982
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1074
  120. boris/vlc_local.py +0 -90
  121. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  122. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  123. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  124. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  125. {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
  126. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.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 == {}:
@@ -225,7 +223,12 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
225
223
  return
226
224
 
227
225
  if len(selected_observations) == 1:
228
- file_name, filter_ = QFileDialog().getSaveFileName(self, "Export events", "", ";;".join(available_formats))
226
+ file_dialog_options = QFileDialog.Options()
227
+ file_dialog_options |= QFileDialog.DontConfirmOverwrite
228
+
229
+ file_name, filter_ = QFileDialog().getSaveFileName(
230
+ self, "Export events", "", ";;".join(available_formats), options=file_dialog_options
231
+ )
229
232
  if not file_name:
230
233
  return
231
234
 
@@ -233,11 +236,9 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
233
236
  if pl.Path(file_name).suffix != "." + output_format:
234
237
  file_name = str(pl.Path(file_name)) + "." + output_format
235
238
  # check if file with new extension already exists
236
- if pl.Path(file_name).is_file():
239
+ if pl.Path(file_name).exists():
237
240
  if (
238
- dialog.MessageDialog(
239
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
240
- )
241
+ dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
241
242
  == cfg.CANCEL
242
243
  ):
243
244
  return
@@ -295,7 +296,7 @@ def export_aggregated_events(self):
295
296
  - select subjects and behaviors
296
297
  - export events in aggregated format
297
298
 
298
- 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
299
300
  """
300
301
 
301
302
  def fields_type(max_modif_number: int) -> dict:
@@ -305,7 +306,8 @@ def export_aggregated_events(self):
305
306
  "Description": str,
306
307
  "Observation type": str,
307
308
  "Source": str,
308
- "Total duration": float,
309
+ "Time offset (s)": str,
310
+ "Coding duration": float,
309
311
  "Media duration (s)": str,
310
312
  "FPS (frame/s)": str,
311
313
  }
@@ -351,9 +353,7 @@ def export_aggregated_events(self):
351
353
 
352
354
  return fields_type_dict
353
355
 
354
- _, selected_observations = select_observations.select_observations2(
355
- self, cfg.MULTIPLE, "Select observations for exporting events"
356
- )
356
+ _, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, "Select observations for exporting events")
357
357
  if not selected_observations:
358
358
  return
359
359
 
@@ -367,40 +367,37 @@ def export_aggregated_events(self):
367
367
  return
368
368
 
369
369
  if len(selected_observations) == 1:
370
- max_media_duration_all_obs, _ = observation_operations.media_duration(
371
- self.pj[cfg.OBSERVATIONS], selected_observations
372
- )
373
- start_coding, end_coding, _ = observation_operations.coding_time(
374
- self.pj[cfg.OBSERVATIONS], selected_observations
375
- )
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)
376
373
  else:
377
374
  max_media_duration_all_obs = None
378
375
  start_coding, end_coding = dec("NaN"), dec("NaN")
376
+ start_interval, end_interval = None, None
379
377
 
380
378
  parameters = select_subj_behav.choose_obs_subj_behav_category(
381
379
  self,
382
380
  selected_observations,
383
381
  start_coding=start_coding,
384
382
  end_coding=end_coding,
383
+ start_interval=start_interval,
384
+ end_interval=end_interval,
385
385
  maxTime=max_media_duration_all_obs,
386
- flagShowIncludeModifiers=False,
387
- flagShowExcludeBehaviorsWoEvents=False,
386
+ show_include_modifiers=False,
387
+ show_exclude_non_coded_behaviors=False,
388
388
  n_observations=len(selected_observations),
389
389
  )
390
390
  if parameters == {}:
391
391
  return
392
392
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
393
- 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")
394
394
  return
395
395
 
396
396
  # check for grouping results
397
397
  flag_group = True
398
398
  if len(selected_observations) > 1:
399
399
  flag_group = (
400
- dialog.MessageDialog(
401
- cfg.programName, "Group events from selected observations in one file?", [cfg.YES, cfg.NO]
402
- )
403
- == cfg.YES
400
+ dialog.MessageDialog(cfg.programName, "Group events from selected observations in one file?", [cfg.YES, cfg.NO]) == cfg.YES
404
401
  )
405
402
 
406
403
  if flag_group:
@@ -418,7 +415,12 @@ def export_aggregated_events(self):
418
415
  cfg.RDS,
419
416
  )
420
417
 
421
- 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
+ )
422
424
 
423
425
  if not fileName:
424
426
  return
@@ -427,13 +429,8 @@ def export_aggregated_events(self):
427
429
  if pl.Path(fileName).suffix != "." + outputFormat:
428
430
  # check if file with new extension already exists
429
431
  fileName = str(pl.Path(fileName)) + "." + outputFormat
430
- if pl.Path(fileName).is_file():
431
- if (
432
- dialog.MessageDialog(
433
- cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
434
- )
435
- == cfg.CANCEL
436
- ):
432
+ if pl.Path(fileName).exists():
433
+ if dialog.MessageDialog(cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
437
434
  return
438
435
 
439
436
  else: # not grouping
@@ -481,11 +478,13 @@ def export_aggregated_events(self):
481
478
  return
482
479
 
483
480
  # compute the maximum number of modifiers
484
- tot_max_modifiers = 0
481
+ tot_max_modifiers: int = 0
485
482
  for obs_id in selected_observations:
486
483
  _, max_modifiers = export_observation.export_aggregated_events(self.pj, parameters, obs_id)
487
484
  tot_max_modifiers = max(tot_max_modifiers, max_modifiers)
488
485
 
486
+ logging.debug(f"tot_max_modifiers: {tot_max_modifiers}")
487
+
489
488
  data_grouped_obs = tablib.Dataset()
490
489
 
491
490
  mem_command: str = "" # remember user choice when file already exists
@@ -494,31 +493,30 @@ def export_aggregated_events(self):
494
493
  for obs_id in selected_observations:
495
494
  logging.debug(f"Exporting aggregated events for obs Id: {obs_id}")
496
495
 
497
- 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
+ )
498
499
 
499
500
  try:
500
501
  # order by start time
501
502
  index = header.index("Start (s)")
502
503
  if cfg.NA not in [x[index] for x in list(data_single_obs)]:
503
504
  data_single_obs_sorted = tablib.Dataset(
504
- # *data.sort(col=index),
505
505
  *sorted(list(data_single_obs), key=lambda x: float(x[index])),
506
- headers=list(fields_type(max_modifiers).keys()),
506
+ headers=list(fields_type(tot_max_modifiers).keys()),
507
507
  )
508
508
  else:
509
509
  # order by image index
510
510
  index = header.index("Image index start")
511
511
  data_single_obs_sorted = tablib.Dataset(
512
- # *data.sort(col=index),
513
512
  *sorted(list(data_single_obs), key=lambda x: float(x[index])),
514
- headers=list(fields_type(max_modifiers).keys()),
513
+ headers=list(fields_type(tot_max_modifiers).keys()),
515
514
  )
516
515
  except Exception:
517
516
  # if error no order
518
517
  data_single_obs_sorted = tablib.Dataset(
519
- # data.sort(col=0), # Observation id
520
518
  *list(data_single_obs),
521
- headers=list(fields_type(max_modifiers).keys()),
519
+ headers=list(fields_type(tot_max_modifiers).keys()),
522
520
  )
523
521
 
524
522
  data_single_obs_sorted.title = obs_id
@@ -539,14 +537,12 @@ def export_aggregated_events(self):
539
537
  if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
540
538
  continue
541
539
 
542
- r, msg = export_observation.dataset_write(
543
- data_single_obs_sorted, fileName, outputFormat, dtype=fields_type(max_modifiers)
544
- )
540
+ r, msg = export_observation.dataset_write(data_single_obs_sorted, fileName, outputFormat, dtype=fields_type(max_modifiers))
545
541
  if not r:
546
- QMessageBox.warning(
547
- None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton
548
- )
542
+ QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
549
543
 
544
+ """
545
+ # disabled after introduction of the force_number_modifiers parameter in export_aggregated_events function
550
546
  if len(data_single_obs_sorted) and max_modifiers < tot_max_modifiers:
551
547
  for i in range(tot_max_modifiers - max_modifiers):
552
548
  data_single_obs_sorted.insert_col(
@@ -554,9 +550,12 @@ def export_aggregated_events(self):
554
550
  col=[""] * (len(list(data_single_obs_sorted))),
555
551
  header=f"Modif #{i}",
556
552
  )
553
+ """
554
+
557
555
  data_grouped_obs.extend(data_single_obs_sorted)
558
556
 
559
557
  data_grouped_obs_all = tablib.Dataset(headers=list(fields_type(tot_max_modifiers).keys()))
558
+
560
559
  data_grouped_obs_all.extend(data_grouped_obs)
561
560
  data_grouped_obs_all.title = "Aggregated events"
562
561
 
@@ -600,10 +599,7 @@ def export_aggregated_events(self):
600
599
  return
601
600
 
602
601
  if outputFormat == cfg.SDIS_EXT: # SDIS format
603
- out: str = (
604
- "% SDIS file created by BORIS (www.boris.unito.it) "
605
- f"at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
606
- )
602
+ out: str = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
607
603
  for obs_id in selected_observations:
608
604
  # observation id
609
605
  out += "\n<{}>\n".format(obs_id)
@@ -629,10 +625,7 @@ def export_aggregated_events(self):
629
625
  fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
630
626
  with open(fileName, "wb") as f:
631
627
  f.write(str.encode(out))
632
- out = (
633
- "% SDIS file created by BORIS (www.boris.unito.it) "
634
- f"at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
635
- )
628
+ out = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
636
629
 
637
630
  if flag_group:
638
631
  with open(fileName, "wb") as f:
@@ -640,9 +633,7 @@ def export_aggregated_events(self):
640
633
  return
641
634
 
642
635
  if flag_group:
643
- r, msg = export_observation.dataset_write(
644
- data_grouped_obs_all, fileName, outputFormat, dtype=fields_type(max_modifiers)
645
- )
636
+ r, msg = export_observation.dataset_write(data_grouped_obs_all, fileName, outputFormat, dtype=fields_type(max_modifiers))
646
637
  if not r:
647
638
  QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
648
639
 
@@ -683,13 +674,19 @@ def export_events_as_textgrid(self) -> None:
683
674
 
684
675
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
685
676
 
677
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
678
+
686
679
  parameters = select_subj_behav.choose_obs_subj_behav_category(
687
680
  self,
688
681
  selected_observations,
689
682
  start_coding=start_coding,
690
683
  end_coding=end_coding,
691
- flagShowIncludeModifiers=False,
692
- 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,
693
690
  maxTime=max_obs_length,
694
691
  n_observations=len(selected_observations),
695
692
  )
@@ -699,13 +696,13 @@ def export_events_as_textgrid(self) -> None:
699
696
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
700
697
  return
701
698
 
702
- export_dir = QFileDialog(self).getExistingDirectory(
703
- 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
704
701
  )
705
702
  if not export_dir:
706
703
  return
707
704
 
708
- mem_command = ""
705
+ mem_command: str = ""
709
706
 
710
707
  # see https://www.fon.hum.uva.nl/praat/manual/TextGrid_file_formats.html
711
708
 
@@ -718,12 +715,7 @@ def export_events_as_textgrid(self) -> None:
718
715
  " intervals: size = {intervalsSize}\n"
719
716
  )
720
717
 
721
- interval_template = (
722
- " intervals [{count}]:\n"
723
- " xmin = {xmin}\n"
724
- " xmax = {xmax}\n"
725
- ' text = "{name}"\n'
726
- )
718
+ interval_template = ' intervals [{count}]:\n xmin = {xmin}\n xmax = {xmax}\n text = "{name}"\n'
727
719
 
728
720
  point_subject_header = (
729
721
  " item [{subject_index}]:\n"
@@ -734,7 +726,7 @@ def export_events_as_textgrid(self) -> None:
734
726
  " points: size = {intervalsSize}\n"
735
727
  )
736
728
 
737
- point_template = " points [{count}]:\n" " number = {number}\n" ' mark = "{mark}"\n'
729
+ point_template = ' points [{count}]:\n number = {number}\n mark = "{mark}"\n'
738
730
 
739
731
  # widget for results
740
732
  self.results = dialog.Results_dialog()
@@ -746,7 +738,7 @@ def export_events_as_textgrid(self) -> None:
746
738
  )
747
739
 
748
740
  if db_connector is None:
749
- logging.critical(f"Error when loading aggregated events in DB")
741
+ logging.critical("Error when loading aggregated events in DB")
750
742
  return
751
743
 
752
744
  cursor = db_connector.cursor()
@@ -755,9 +747,7 @@ def export_events_as_textgrid(self) -> None:
755
747
 
756
748
  for obs_id in selected_observations:
757
749
  if parameters["time"] == cfg.TIME_EVENTS:
758
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
759
- self.pj[cfg.OBSERVATIONS], [obs_id]
760
- )
750
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
761
751
  if start_coding is None and end_coding is None: # no events
762
752
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
763
753
  QApplication.processEvents()
@@ -779,9 +769,7 @@ def export_events_as_textgrid(self) -> None:
779
769
  coding_duration = max_media_duration
780
770
 
781
771
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
782
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
783
- self.pj[cfg.OBSERVATIONS], [obs_id]
784
- )
772
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
785
773
  if start_coding is None and end_coding is None: # no events
786
774
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
787
775
  QApplication.processEvents()
@@ -798,7 +786,16 @@ def export_events_as_textgrid(self) -> None:
798
786
  min_time = float(parameters[cfg.START_TIME])
799
787
  max_time = float(parameters[cfg.END_TIME])
800
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
+
801
797
  # delete events outside time interval
798
+
802
799
  cursor.execute(
803
800
  "DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
804
801
  (
@@ -842,37 +839,30 @@ def export_events_as_textgrid(self) -> None:
842
839
 
843
840
  next_obs: bool = False
844
841
 
845
- """
846
- total_media_duration = round(
847
- observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id]), 3
848
- )
849
- """
850
-
842
+ # number of items for size parameter
851
843
  cursor.execute(
852
844
  (
853
- "SELECT COUNT(DISTINCT subject) FROM aggregated_events "
854
- "WHERE observation = ? AND subject IN ({}) AND type = 'STATE' ".format(
855
- ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS]))
856
- )
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) "
857
847
  ),
858
848
  [obs_id] + parameters[cfg.SELECTED_SUBJECTS],
859
849
  )
860
850
 
861
- subjectsNum = int(cursor.fetchone()[0])
862
- subjectsMin, subjectsMax = min_time, max_time
851
+ subjects_num = int(cursor.fetchone()[0])
852
+ subjects_max = max_time
863
853
 
864
854
  out = (
865
855
  'File type = "ooTextFile"\n'
866
856
  'Object class = "TextGrid"\n'
867
857
  "\n"
868
858
  f"xmin = 0.0\n"
869
- f"xmax = {subjectsMax}\n"
859
+ f"xmax = {subjects_max}\n"
870
860
  "tiers? <exists>\n"
871
- f"size = {subjectsNum}\n"
861
+ f"size = {subjects_num}\n"
872
862
  "item []:\n"
873
863
  )
874
864
 
875
- subject_index = 0
865
+ subject_index: int = 0
876
866
  for subject in parameters[cfg.SELECTED_SUBJECTS]:
877
867
  if subject not in [
878
868
  x[cfg.EVENT_SUBJECT_FIELD_IDX] if x[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
@@ -880,7 +870,8 @@ def export_events_as_textgrid(self) -> None:
880
870
  ]:
881
871
  continue
882
872
 
883
- intervalsMin, intervalsMax = min_time, max_time
873
+ intervalsMin = min_time
874
+ intervalsMax = max_time
884
875
 
885
876
  # STATE events
886
877
  cursor.execute(
@@ -895,77 +886,70 @@ def export_events_as_textgrid(self) -> None:
895
886
  {"start": util.float2decimal(r["start"]), "stop": util.float2decimal(r["stop"]), "code": r["behavior"]}
896
887
  for r in cursor.fetchall()
897
888
  ]
898
- if not rows:
899
- continue
900
-
901
- out += interval_subject_header
889
+ if rows:
890
+ out += interval_subject_header
902
891
 
903
- count = 0
904
-
905
- # check if 1st behavior starts at the beginning
906
- if rows[0]["start"] > 0:
907
- count += 1
908
- out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
892
+ count = 0
909
893
 
910
- for idx, row in enumerate(rows):
911
- # check if events are overlapping
912
- if (idx + 1 < len(rows)) and (row["stop"] > rows[idx + 1]["start"]):
913
- self.results.ptText.appendHtml(
914
- (
915
- f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
916
- "It is not possible to create the Praat TextGrid file."
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
+ )
917
907
  )
918
- )
919
- QApplication.processEvents()
908
+ QApplication.processEvents()
920
909
 
921
- next_obs = True
922
- break
910
+ next_obs = True
911
+ break
923
912
 
924
- count += 1
913
+ count += 1
925
914
 
926
- if (idx + 1 < len(rows)) and (
927
- rows[idx + 1]["start"] - dec("0.001") <= row["stop"] < rows[idx + 1]["start"]
928
- ):
929
- xmax = rows[idx + 1]["start"]
930
- else:
931
- xmax = row["stop"]
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"],
930
+ )
932
931
 
933
- out += interval_template.format(count=count, name=row["code"], xmin=row["start"], xmax=xmax)
932
+ if next_obs:
933
+ break
934
934
 
935
- # check if no behavior
936
- 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:
937
937
  count += 1
938
- out += interval_template.format(
939
- count=count,
940
- name="null",
941
- xmin=row["stop"],
942
- xmax=rows[idx + 1]["start"],
943
- )
944
-
945
- if next_obs:
946
- break
947
-
948
- # check if last event ends at the end of media file
949
- if rows[-1]["stop"] < max_time:
950
- count += 1
951
- out += interval_template.format(count=count, name="null", xmin=rows[-1]["stop"], xmax=max_time)
952
-
953
- # add info
954
- subject_index += 1
955
- out = out.format(
956
- subject_index=subject_index,
957
- subject=subject,
958
- intervalsSize=count,
959
- intervalsMin=intervalsMin,
960
- intervalsMax=intervalsMax,
961
- )
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
+ )
962
949
 
963
950
  # POINT events
964
951
  cursor.execute(
965
- (
966
- "SELECT start, behavior FROM aggregated_events "
967
- "WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"
968
- ),
952
+ ("SELECT start, behavior FROM aggregated_events WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"),
969
953
  (obs_id, subject),
970
954
  )
971
955
 
@@ -995,15 +979,12 @@ def export_events_as_textgrid(self) -> None:
995
979
  continue
996
980
 
997
981
  # check if file already exists
998
- if (
999
- mem_command != cfg.OVERWRITE_ALL
1000
- and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid").is_file()
1001
- ):
982
+ if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid").is_file():
1002
983
  if mem_command == cfg.SKIP_ALL:
1003
984
  continue
1004
985
  mem_command = dialog.MessageDialog(
1005
986
  cfg.programName,
1006
- 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.",
1007
988
  [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
1008
989
  )
1009
990
  if mem_command == cfg.CANCEL:
@@ -1012,17 +993,13 @@ def export_events_as_textgrid(self) -> None:
1012
993
  continue
1013
994
 
1014
995
  try:
1015
- 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:
1016
997
  f.write(out)
1017
998
  file_count += 1
1018
- self.results.ptText.appendHtml(
1019
- f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid was created."
1020
- )
999
+ self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid was created.")
1021
1000
  QApplication.processEvents()
1022
1001
  except Exception:
1023
- self.results.ptText.appendHtml(
1024
- f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid can not be created."
1025
- )
1002
+ self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid can not be created.")
1026
1003
  QApplication.processEvents()
1027
1004
 
1028
1005
  self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")