boris-behav-obs 8.9.16__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 (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -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 +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  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 +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  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 +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  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 +43 -46
  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 +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  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 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  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 +766 -266
  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 +125 -28
  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.9.16.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/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.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,
@@ -424,12 +409,18 @@ def export_aggregated_events(self):
424
409
  cfg.XLS,
425
410
  cfg.HTML,
426
411
  cfg.SDIS,
412
+ cfg.TBS,
427
413
  cfg.SQL,
428
414
  cfg.PANDAS_DF,
429
415
  cfg.RDS,
430
416
  )
431
417
 
432
- 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
+ )
433
424
 
434
425
  if not fileName:
435
426
  return
@@ -438,17 +429,11 @@ def export_aggregated_events(self):
438
429
  if pl.Path(fileName).suffix != "." + outputFormat:
439
430
  # check if file with new extension already exists
440
431
  fileName = str(pl.Path(fileName)) + "." + outputFormat
441
- if pl.Path(fileName).is_file():
442
- if (
443
- dialog.MessageDialog(
444
- cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
445
- )
446
- == cfg.CANCEL
447
- ):
432
+ if pl.Path(fileName).exists():
433
+ if dialog.MessageDialog(cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
448
434
  return
449
435
 
450
436
  else: # not grouping
451
-
452
437
  file_formats = (
453
438
  cfg.TSV,
454
439
  cfg.CSV,
@@ -473,7 +458,7 @@ def export_aggregated_events(self):
473
458
  if not exportDir:
474
459
  return
475
460
 
476
- if outputFormat == "sql":
461
+ if outputFormat == cfg.SQL_EXT:
477
462
  _, _, conn = db_functions.load_aggregated_events_in_db(
478
463
  self.pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
479
464
  )
@@ -482,7 +467,6 @@ def export_aggregated_events(self):
482
467
  for line in conn.iterdump():
483
468
  f.write(f"{line}\n")
484
469
  except Exception:
485
-
486
470
  QMessageBox.critical(
487
471
  None,
488
472
  cfg.programName,
@@ -494,51 +478,50 @@ def export_aggregated_events(self):
494
478
  return
495
479
 
496
480
  # compute the maximum number of modifiers
497
- tot_max_modifiers = 0
481
+ tot_max_modifiers: int = 0
498
482
  for obs_id in selected_observations:
499
483
  _, max_modifiers = export_observation.export_aggregated_events(self.pj, parameters, obs_id)
500
484
  tot_max_modifiers = max(tot_max_modifiers, max_modifiers)
501
485
 
486
+ logging.debug(f"tot_max_modifiers: {tot_max_modifiers}")
487
+
502
488
  data_grouped_obs = tablib.Dataset()
503
- # sort by start time
504
- start_idx = -9 # TODO: improve!
505
- stop_idx = -8
506
489
 
507
- mem_command = "" # remember user choice when file already exists
508
- for obs_id in selected_observations:
490
+ mem_command: str = "" # remember user choice when file already exists
491
+ header = list(fields_type(tot_max_modifiers).keys())
509
492
 
493
+ for obs_id in selected_observations:
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
- index = tuple(fields_type(max_modifiers).keys()).index("Start (s)")
517
- if "NA" not in [x[index] for x in list(data_single_obs)]:
502
+ index = header.index("Start (s)")
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
- index = tuple(fields_type(max_modifiers).keys()).index("Image index start")
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
- except:
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 ("sds", "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,18 +537,12 @@ def export_aggregated_events(self):
554
537
  if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
555
538
  continue
556
539
 
557
- """
558
- if sys.version_info.minor >= 8:
559
- print(f"{list(data_single_obs)=}")
560
- print(f"{list(fields_type(max_modifiers).keys())=}")
561
- """
562
-
563
- 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))
564
541
  if not r:
565
- QMessageBox.warning(
566
- None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton
567
- )
542
+ QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
568
543
 
544
+ """
545
+ # disabled after introduction of the force_number_modifiers parameter in export_aggregated_events function
569
546
  if len(data_single_obs_sorted) and max_modifiers < tot_max_modifiers:
570
547
  for i in range(tot_max_modifiers - max_modifiers):
571
548
  data_single_obs_sorted.insert_col(
@@ -573,44 +550,33 @@ def export_aggregated_events(self):
573
550
  col=[""] * (len(list(data_single_obs_sorted))),
574
551
  header=f"Modif #{i}",
575
552
  )
553
+ """
554
+
576
555
  data_grouped_obs.extend(data_single_obs_sorted)
577
556
 
578
- """
579
- try:
580
- # order by obs_id, start time
581
- data_grouped_obs_sorted = tablib.Dataset(
582
- *sorted(list(data_grouped_obs), key=lambda x: (x[obs_id_idx], float(x[start_idx]))),
583
- headers=list(fields_type(tot_max_modifiers).keys()),
584
- )
585
- except:
586
- # if error order by obs_id
587
- data_grouped_obs_sorted = tablib.Dataset(
588
- *sorted(list(data_grouped_obs), key=lambda x: x[obs_id_idx]),
589
- headers=list(fields_type(tot_max_modifiers).keys()),
590
- )
591
- """
592
557
  data_grouped_obs_all = tablib.Dataset(headers=list(fields_type(tot_max_modifiers).keys()))
558
+
593
559
  data_grouped_obs_all.extend(data_grouped_obs)
594
560
  data_grouped_obs_all.title = "Aggregated events"
595
561
 
596
- # TODO: finish
597
- if outputFormat == "tbs": # Timed behavioral sequences
598
- out = ""
599
- for obsId in selected_observations:
562
+ start_idx = header.index("Start (s)")
563
+ stop_idx = header.index("Stop (s)")
564
+
565
+ if outputFormat == cfg.TBS_EXT: # Timed behavioral sequences
566
+ out: str = ""
567
+ for obs_id in selected_observations:
600
568
  # observation id
601
- out += f"# {obsId}\n"
569
+ out += f"# {obs_id}\n"
602
570
 
603
- for event in list(data):
604
- if event[0] == obsId:
605
- behavior = event[-8]
571
+ for event in list(data_grouped_obs_all):
572
+ if event[0] == obs_id:
573
+ behavior = event[header.index("Behavior")]
574
+ subject = event[header.index("Subject")]
606
575
  # replace various char by _
607
- for char in [" ", "-", "/"]:
576
+ for char in (" ", "-", "/"):
608
577
  behavior = behavior.replace(char, "_")
609
- subject = event[-9]
610
- # replace various char by _
611
- for char in [" ", "-", "/"]:
612
578
  subject = subject.replace(char, "_")
613
- event_start = f"{float(event[start_idx]):.3f}" # start event (from end for independent variables)
579
+ event_start = f"{float(event[start_idx]):.3f}" # start event
614
580
  if not event[stop_idx]: # stop event (from end)
615
581
  event_stop = f"{float(event[start_idx]) + 0.001:.3f}"
616
582
  else:
@@ -622,7 +588,7 @@ def export_aggregated_events(self):
622
588
  out += "\n"
623
589
 
624
590
  if not flag_group:
625
- fileName = f"{pl.Path(exportDir) / util.safeFileName(obsId)}.{outputFormat}"
591
+ fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
626
592
  with open(fileName, "wb") as f:
627
593
  f.write(str.encode(out))
628
594
  out = ""
@@ -632,45 +598,34 @@ def export_aggregated_events(self):
632
598
  f.write(str.encode(out))
633
599
  return
634
600
 
635
- if outputFormat == "sds": # SDIS format
636
- out = ("% SDIS file created by BORIS (www.boris.unito.it) " "at {}\nTimed <seconds>;\n").format(
637
- util.datetime_iso8601(dt.datetime.now())
638
- )
639
- for obsId in selected_observations:
601
+ if outputFormat == cfg.SDIS_EXT: # SDIS format
602
+ out: str = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
603
+ for obs_id in selected_observations:
640
604
  # observation id
641
- out += "\n<{}>\n".format(obsId)
605
+ out += "\n<{}>\n".format(obs_id)
642
606
 
643
- for event in list(data):
644
- if event[0] == obsId:
645
- behavior = event[-8]
607
+ for event in list(data_grouped_obs_all):
608
+ if event[0] == obs_id:
609
+ behavior = event[header.index("Behavior")]
610
+ subject = event[header.index("Subject")]
646
611
  # replace various char by _
647
- for char in [" ", "-", "/"]:
612
+ for char in (" ", "-", "/"):
648
613
  behavior = behavior.replace(char, "_")
649
- subject = event[-9]
650
- # replace various char by _
651
- for char in [" ", "-", "/"]:
652
614
  subject = subject.replace(char, "_")
653
- event_start = "{0:.3f}".format(
654
- float(event[start_idx])
655
- ) # start event (from end for independent variables)
615
+
616
+ event_start = f"{float(event[start_idx]):.3f}" # start event
656
617
  if not event[stop_idx]: # stop event (from end)
657
- event_stop = "{0:.3f}".format(float(event[start_idx]) + 0.001)
618
+ event_stop = f"{float(event[start_idx]) + 0.001:.3f}"
658
619
  else:
659
- event_stop = "{0:.3f}".format(float(event[stop_idx]))
620
+ event_stop = f"{float(event[stop_idx]):.3f}"
660
621
  out += f"{subject}_{behavior},{event_start}-{event_stop} "
661
622
 
662
623
  out += "/\n\n"
663
624
  if not flag_group:
664
- """
665
- fileName = str(pathlib.Path(pathlib.Path(exportDir) / safeFileName(obsId)).with suffix("." + outputFormat))
666
- """
667
- fileName = f"{pl.Path(exportDir) / util.safeFileName(obsId)}.{outputFormat}"
625
+ fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
668
626
  with open(fileName, "wb") as f:
669
627
  f.write(str.encode(out))
670
- out = (
671
- "% SDIS file created by BORIS (www.boris.unito.it) "
672
- f"at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
673
- )
628
+ out = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
674
629
 
675
630
  if flag_group:
676
631
  with open(fileName, "wb") as f:
@@ -678,7 +633,7 @@ def export_aggregated_events(self):
678
633
  return
679
634
 
680
635
  if flag_group:
681
- 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))
682
637
  if not r:
683
638
  QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
684
639
 
@@ -719,13 +674,19 @@ def export_events_as_textgrid(self) -> None:
719
674
 
720
675
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
721
676
 
677
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
678
+
722
679
  parameters = select_subj_behav.choose_obs_subj_behav_category(
723
680
  self,
724
681
  selected_observations,
725
682
  start_coding=start_coding,
726
683
  end_coding=end_coding,
727
- flagShowIncludeModifiers=False,
728
- 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,
729
690
  maxTime=max_obs_length,
730
691
  n_observations=len(selected_observations),
731
692
  )
@@ -735,13 +696,13 @@ def export_events_as_textgrid(self) -> None:
735
696
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
736
697
  return
737
698
 
738
- export_dir = QFileDialog(self).getExistingDirectory(
739
- 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
740
701
  )
741
702
  if not export_dir:
742
703
  return
743
704
 
744
- mem_command = ""
705
+ mem_command: str = ""
745
706
 
746
707
  # see https://www.fon.hum.uva.nl/praat/manual/TextGrid_file_formats.html
747
708
 
@@ -754,12 +715,7 @@ def export_events_as_textgrid(self) -> None:
754
715
  " intervals: size = {intervalsSize}\n"
755
716
  )
756
717
 
757
- interval_template = (
758
- " intervals [{count}]:\n"
759
- " xmin = {xmin}\n"
760
- " xmax = {xmax}\n"
761
- ' text = "{name}"\n'
762
- )
718
+ interval_template = ' intervals [{count}]:\n xmin = {xmin}\n xmax = {xmax}\n text = "{name}"\n'
763
719
 
764
720
  point_subject_header = (
765
721
  " item [{subject_index}]:\n"
@@ -770,7 +726,7 @@ def export_events_as_textgrid(self) -> None:
770
726
  " points: size = {intervalsSize}\n"
771
727
  )
772
728
 
773
- point_template = " points [{count}]:\n" " number = {number}\n" ' mark = "{mark}"\n'
729
+ point_template = ' points [{count}]:\n number = {number}\n mark = "{mark}"\n'
774
730
 
775
731
  # widget for results
776
732
  self.results = dialog.Results_dialog()
@@ -782,7 +738,7 @@ def export_events_as_textgrid(self) -> None:
782
738
  )
783
739
 
784
740
  if db_connector is None:
785
- logging.critical(f"Error when loading aggregated events in DB")
741
+ logging.critical("Error when loading aggregated events in DB")
786
742
  return
787
743
 
788
744
  cursor = db_connector.cursor()
@@ -790,11 +746,8 @@ def export_events_as_textgrid(self) -> None:
790
746
  file_count: int = 0
791
747
 
792
748
  for obs_id in selected_observations:
793
-
794
749
  if parameters["time"] == cfg.TIME_EVENTS:
795
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
796
- self.pj[cfg.OBSERVATIONS], [obs_id]
797
- )
750
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
798
751
  if start_coding is None and end_coding is None: # no events
799
752
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
800
753
  QApplication.processEvents()
@@ -809,7 +762,6 @@ def export_events_as_textgrid(self) -> None:
809
762
  max_time = float(end_coding)
810
763
 
811
764
  if parameters["time"] == cfg.TIME_FULL_OBS:
812
-
813
765
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
814
766
  max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
815
767
  min_time = float(0)
@@ -817,9 +769,7 @@ def export_events_as_textgrid(self) -> None:
817
769
  coding_duration = max_media_duration
818
770
 
819
771
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
820
- start_coding, end_coding, coding_duration = observation_operations.coding_time(
821
- self.pj[cfg.OBSERVATIONS], [obs_id]
822
- )
772
+ start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
823
773
  if start_coding is None and end_coding is None: # no events
824
774
  self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
825
775
  QApplication.processEvents()
@@ -836,12 +786,16 @@ def export_events_as_textgrid(self) -> None:
836
786
  min_time = float(parameters[cfg.START_TIME])
837
787
  max_time = float(parameters[cfg.END_TIME])
838
788
 
839
- """
840
- print(f"min_time: {min_time}")
841
- print(f"max_time: {max_time}")
842
- """
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)
843
796
 
844
797
  # delete events outside time interval
798
+
845
799
  cursor.execute(
846
800
  "DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
847
801
  (
@@ -885,37 +839,30 @@ def export_events_as_textgrid(self) -> None:
885
839
 
886
840
  next_obs: bool = False
887
841
 
888
- """
889
- total_media_duration = round(
890
- observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id]), 3
891
- )
892
- """
893
-
842
+ # number of items for size parameter
894
843
  cursor.execute(
895
844
  (
896
- "SELECT COUNT(DISTINCT subject) FROM aggregated_events "
897
- "WHERE observation = ? AND subject IN ({}) AND type = 'STATE' ".format(
898
- ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS]))
899
- )
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) "
900
847
  ),
901
848
  [obs_id] + parameters[cfg.SELECTED_SUBJECTS],
902
849
  )
903
850
 
904
- subjectsNum = int(cursor.fetchone()[0])
905
- subjectsMin, subjectsMax = min_time, max_time
851
+ subjects_num = int(cursor.fetchone()[0])
852
+ subjects_max = max_time
906
853
 
907
854
  out = (
908
855
  'File type = "ooTextFile"\n'
909
856
  'Object class = "TextGrid"\n'
910
857
  "\n"
911
858
  f"xmin = 0.0\n"
912
- f"xmax = {subjectsMax}\n"
859
+ f"xmax = {subjects_max}\n"
913
860
  "tiers? <exists>\n"
914
- f"size = {subjectsNum}\n"
861
+ f"size = {subjects_num}\n"
915
862
  "item []:\n"
916
863
  )
917
864
 
918
- subject_index = 0
865
+ subject_index: int = 0
919
866
  for subject in parameters[cfg.SELECTED_SUBJECTS]:
920
867
  if subject not in [
921
868
  x[cfg.EVENT_SUBJECT_FIELD_IDX] if x[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
@@ -923,7 +870,8 @@ def export_events_as_textgrid(self) -> None:
923
870
  ]:
924
871
  continue
925
872
 
926
- intervalsMin, intervalsMax = min_time, max_time
873
+ intervalsMin = min_time
874
+ intervalsMax = max_time
927
875
 
928
876
  # STATE events
929
877
  cursor.execute(
@@ -938,325 +886,22 @@ def export_events_as_textgrid(self) -> None:
938
886
  {"start": util.float2decimal(r["start"]), "stop": util.float2decimal(r["stop"]), "code": r["behavior"]}
939
887
  for r in cursor.fetchall()
940
888
  ]
941
- if not rows:
942
- continue
889
+ if rows:
890
+ out += interval_subject_header
943
891
 
944
- out += interval_subject_header
892
+ count = 0
945
893
 
946
- count = 0
947
-
948
- # check if 1st behavior starts at the beginning
949
- if rows[0]["start"] > 0:
950
- count += 1
951
- out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
952
-
953
- for idx, row in enumerate(rows):
954
-
955
- # check if events are overlapping
956
- if (idx + 1 < len(rows)) and (row["stop"] > rows[idx + 1]["start"]):
957
-
958
- self.results.ptText.appendHtml(
959
- (
960
- f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
961
- "It is not possible to create the Praat TextGrid file."
962
- )
963
- )
964
- QApplication.processEvents()
965
-
966
- next_obs = True
967
- break
968
-
969
- count += 1
970
-
971
- """
972
- print(f'{row["start"]=} {row["stop"]=}')
973
- if idx + 1 < len(rows):
974
- print(f'{rows[idx + 1]["start"]=}')
975
- print()
976
- """
977
-
978
- if (idx + 1 < len(rows)) and (
979
- rows[idx + 1]["start"] - dec("0.001") <= row["stop"] < rows[idx + 1]["start"]
980
- ):
981
- xmax = rows[idx + 1]["start"]
982
- else:
983
- xmax = row["stop"]
984
-
985
- out += interval_template.format(count=count, name=row["code"], xmin=row["start"], xmax=xmax)
986
-
987
- # check if no behavior
988
- if (idx + 1 < len(rows)) and (row["stop"] < rows[idx + 1]["start"] - dec("0.001")):
894
+ # check if 1st behavior starts at the beginning
895
+ if rows[0]["start"] > 0:
989
896
  count += 1
990
- out += interval_template.format(
991
- count=count,
992
- name="null",
993
- xmin=row["stop"],
994
- xmax=rows[idx + 1]["start"],
995
- )
996
-
997
- if next_obs:
998
- break
999
-
1000
- # check if last event ends at the end of media file
1001
- if rows[-1]["stop"] < max_time:
1002
- count += 1
1003
- out += interval_template.format(count=count, name="null", xmin=rows[-1]["stop"], xmax=max_time)
1004
-
1005
- # add info
1006
- subject_index += 1
1007
- out = out.format(
1008
- subject_index=subject_index,
1009
- subject=subject,
1010
- intervalsSize=count,
1011
- intervalsMin=intervalsMin,
1012
- intervalsMax=intervalsMax,
1013
- )
1014
-
1015
- # POINT events
1016
- cursor.execute(
1017
- (
1018
- "SELECT start, behavior FROM aggregated_events "
1019
- "WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"
1020
- ),
1021
- (obs_id, subject),
1022
- )
1023
-
1024
- rows = [{"start": util.float2decimal(r["start"]), "code": r["behavior"]} for r in cursor.fetchall()]
1025
- if not rows:
1026
- continue
1027
-
1028
- out += point_subject_header
1029
-
1030
- count = 0
1031
-
1032
- for idx, row in enumerate(rows):
1033
-
1034
- count += 1
1035
- out += point_template.format(count=count, mark=row["code"], number=row["start"])
1036
-
1037
- # add info
1038
- subject_index += 1
1039
- out = out.format(
1040
- subject_index=subject_index,
1041
- subject=subject,
1042
- intervalsSize=count,
1043
- intervalsMin=intervalsMin,
1044
- intervalsMax=intervalsMax,
1045
- )
1046
-
1047
- if next_obs:
1048
- continue
1049
-
1050
- # check if file already exists
1051
- if (
1052
- mem_command != cfg.OVERWRITE_ALL
1053
- and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid").is_file()
1054
- ):
1055
- if mem_command == cfg.SKIP_ALL:
1056
- continue
1057
- mem_command = dialog.MessageDialog(
1058
- cfg.programName,
1059
- f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid</b> already exists.",
1060
- [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
1061
- )
1062
- if mem_command == cfg.CANCEL:
1063
- return
1064
- if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
1065
- continue
1066
-
1067
- try:
1068
- with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid", "w") as f:
1069
- f.write(out)
1070
- file_count += 1
1071
- self.results.ptText.appendHtml(
1072
- f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid was created."
1073
- )
1074
- QApplication.processEvents()
1075
- except Exception:
1076
- self.results.ptText.appendHtml(
1077
- f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.textGrid can not be created."
1078
- )
1079
- QApplication.processEvents()
1080
-
1081
- self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")
1082
- QApplication.processEvents()
1083
-
1084
-
1085
- def export_events_as_textgrid_old(self):
1086
- """
1087
- export state events as Praat textgrid
1088
- """
1089
-
1090
- _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
1091
-
1092
- if not selected_observations:
1093
- return
1094
-
1095
- # check if coded behaviors are defined in ethogram
1096
- if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
1097
- return
1098
-
1099
- # check if state events are paired
1100
- not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
1101
- if not_ok or not selected_observations:
1102
- return
1103
-
1104
- max_obs_length, _ = observation_operations.observation_length(self.pj, selected_observations)
1105
-
1106
- # exit with message if events do not have timestamp
1107
- if max_obs_length.is_nan():
1108
- QMessageBox.critical(
1109
- None,
1110
- cfg.programName,
1111
- ("This function is not available for observations with events that do not have timestamp"),
1112
- QMessageBox.Ok | QMessageBox.Default,
1113
- QMessageBox.NoButton,
1114
- )
1115
- return
1116
-
1117
- start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
1118
-
1119
- parameters = select_subj_behav.choose_obs_subj_behav_category(
1120
- self,
1121
- selected_observations,
1122
- start_coding=start_coding,
1123
- end_coding=end_coding,
1124
- flagShowIncludeModifiers=False,
1125
- flagShowExcludeBehaviorsWoEvents=False,
1126
- n_observations=len(selected_observations),
1127
- )
1128
- if parameters == {}:
1129
- return
1130
- if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
1131
- QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
1132
- return
1133
-
1134
- exportDir = QFileDialog(self).getExistingDirectory(
1135
- self, "Export events as Praat TextGrid", os.path.expanduser("~"), options=QFileDialog(self).ShowDirsOnly
1136
- )
1137
- if not exportDir:
1138
- return
1139
-
1140
- mem_command = ""
1141
-
1142
- # see https://www.fon.hum.uva.nl/praat/manual/TextGrid_file_formats.html
1143
-
1144
- interval_subject_header = (
1145
- " item [{subject_index}]:\n"
1146
- ' class = "IntervalTier"\n'
1147
- ' name = "{subject}"\n'
1148
- " xmin = {intervalsMin}\n"
1149
- " xmax = {intervalsMax}\n"
1150
- " intervals: size = {intervalsSize}\n"
1151
- )
1152
-
1153
- interval_template = (
1154
- " intervals [{count}]:\n"
1155
- " xmin = {xmin}\n"
1156
- " xmax = {xmax}\n"
1157
- ' text = "{name}"\n'
1158
- )
1159
-
1160
- point_subject_header = (
1161
- " item [{subject_index}]:\n"
1162
- ' class = "TextTier"\n'
1163
- ' name = "{subject}"\n'
1164
- " xmin = {intervalsMin}\n"
1165
- " xmax = {intervalsMax}\n"
1166
- " points: size = {intervalsSize}\n"
1167
- )
1168
-
1169
- point_template = " points [{count}]:\n" " number = {number}\n" ' mark = "{mark}"\n'
1170
-
1171
- # widget for results
1172
- self.results = dialog.Results_dialog()
1173
- self.results.setWindowTitle(f"{cfg.programName} - Export events as Praat TextGrid")
1174
- self.results.show()
1175
-
1176
- cursor = db_functions.load_events_in_db(
1177
- self.pj,
1178
- parameters[cfg.SELECTED_SUBJECTS],
1179
- selected_observations,
1180
- parameters[cfg.SELECTED_BEHAVIORS],
1181
- )
1182
-
1183
- file_count: int = 0
1184
-
1185
- for obs_id in selected_observations:
1186
-
1187
- next_obs: bool = False
1188
-
1189
- total_media_duration = round(
1190
- observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id]), 3
1191
- )
1192
-
1193
- cursor.execute(
1194
- (
1195
- "SELECT COUNT(DISTINCT subject) FROM events "
1196
- "WHERE observation = ? AND subject IN ({}) AND type = 'STATE' ".format(
1197
- ",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS]))
1198
- )
1199
- ),
1200
- [obs_id] + parameters[cfg.SELECTED_SUBJECTS],
1201
- )
1202
-
1203
- subjectsNum = int(list(cursor.fetchall())[0][0])
1204
-
1205
- subjectsMin, subjectsMax = 0, total_media_duration
1206
-
1207
- out = (
1208
- 'File type = "ooTextFile"\n'
1209
- 'Object class = "TextGrid"\n'
1210
- "\n"
1211
- f"xmin = {subjectsMin}\n"
1212
- f"xmax = {subjectsMax}\n"
1213
- "tiers? <exists>\n"
1214
- f"size = {subjectsNum}\n"
1215
- "item []:\n"
1216
- )
1217
-
1218
- subject_index = 0
1219
- for subject in parameters[cfg.SELECTED_SUBJECTS]:
1220
- if subject not in [
1221
- x[cfg.EVENT_SUBJECT_FIELD_IDX] if x[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
1222
- for x in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
1223
- ]:
1224
- continue
1225
-
1226
- intervalsMin, intervalsMax = 0, total_media_duration
1227
-
1228
- # STATE events
1229
- cursor.execute(
1230
- (
1231
- "SELECT occurence, code FROM events "
1232
- "WHERE observation = ? AND subject = ? AND type = 'STATE' ORDER BY occurence"
1233
- ),
1234
- (obs_id, subject),
1235
- )
1236
-
1237
- rows = [{"occurence": util.float2decimal(r["occurence"]), "code": r["code"]} for r in cursor.fetchall()]
1238
- if not rows:
1239
- continue
1240
-
1241
- out += interval_subject_header
1242
-
1243
- count = 0
1244
-
1245
- # check if 1st behavior starts at the beginning
1246
-
1247
- if rows[0]["occurence"] > 0:
1248
- count += 1
1249
- out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["occurence"])
1250
-
1251
- for idx, row in enumerate(rows):
1252
-
1253
- if idx % 2 == 0:
1254
- # check if events not interlacced
1255
- if row["code"] != rows[idx + 1]["code"]:
897
+ out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
1256
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"]):
1257
902
  self.results.ptText.appendHtml(
1258
903
  (
1259
- f"The events are interlaced for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
904
+ f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
1260
905
  "It is not possible to create the Praat TextGrid file."
1261
906
  )
1262
907
  )
@@ -1266,56 +911,49 @@ def export_events_as_textgrid_old(self):
1266
911
  break
1267
912
 
1268
913
  count += 1
1269
- out += interval_template.format(
1270
- count=count, name=row["code"], xmin=row["occurence"], xmax=rows[idx + 1]["occurence"]
1271
- )
1272
-
1273
- # check if difference is > 0.001
1274
- if len(rows) > idx + 2:
1275
- if rows[idx + 2]["occurence"] - rows[idx + 1]["occurence"] > 0.001:
1276
-
1277
- out += interval_template.format(
1278
- count=count + 1,
1279
- name="null",
1280
- xmin=rows[idx + 1]["occurence"],
1281
- xmax=rows[idx + 2]["occurence"],
1282
- )
1283
- count += 1
1284
- else:
1285
- rows[idx + 2]["occurence"] = rows[idx + 1]["occurence"]
1286
914
 
1287
- if next_obs:
1288
- break
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
+ )
1289
931
 
1290
- # check if last event ends at the end of media file
1291
- if rows[-1]["occurence"] < observation_operations.observation_total_length(
1292
- self.pj[cfg.OBSERVATIONS][obs_id]
1293
- ):
1294
- count += 1
1295
- out += interval_template.format(
1296
- count=count, name="null", xmin=rows[-1]["occurence"], xmax=total_media_duration
1297
- )
932
+ if next_obs:
933
+ break
1298
934
 
1299
- # add info
1300
- subject_index += 1
1301
- out = out.format(
1302
- subject_index=subject_index,
1303
- subject=subject,
1304
- intervalsSize=count,
1305
- intervalsMin=intervalsMin,
1306
- intervalsMax=intervalsMax,
1307
- )
935
+ # check if last event ends at the end of media file
936
+ if rows[-1]["stop"] < max_time:
937
+ count += 1
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
+ )
1308
949
 
1309
950
  # POINT events
1310
951
  cursor.execute(
1311
- (
1312
- "SELECT occurence, code FROM events "
1313
- "WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY occurence"
1314
- ),
952
+ ("SELECT start, behavior FROM aggregated_events WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"),
1315
953
  (obs_id, subject),
1316
954
  )
1317
955
 
1318
- rows = [{"occurence": util.float2decimal(r["occurence"]), "code": r["code"]} for r in cursor.fetchall()]
956
+ rows = [{"start": util.float2decimal(r["start"]), "code": r["behavior"]} for r in cursor.fetchall()]
1319
957
  if not rows:
1320
958
  continue
1321
959
 
@@ -1324,9 +962,8 @@ def export_events_as_textgrid_old(self):
1324
962
  count = 0
1325
963
 
1326
964
  for idx, row in enumerate(rows):
1327
-
1328
965
  count += 1
1329
- out += point_template.format(count=count, mark=row["code"], number=row["occurence"])
966
+ out += point_template.format(count=count, mark=row["code"], number=row["start"])
1330
967
 
1331
968
  # add info
1332
969
  subject_index += 1
@@ -1342,15 +979,12 @@ def export_events_as_textgrid_old(self):
1342
979
  continue
1343
980
 
1344
981
  # check if file already exists
1345
- if (
1346
- mem_command != cfg.OVERWRITE_ALL
1347
- and pl.Path(f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.textGrid").is_file()
1348
- ):
982
+ if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid").is_file():
1349
983
  if mem_command == cfg.SKIP_ALL:
1350
984
  continue
1351
985
  mem_command = dialog.MessageDialog(
1352
986
  cfg.programName,
1353
- f"The file <b>{pl.Path(exportDir) / 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.",
1354
988
  [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
1355
989
  )
1356
990
  if mem_command == cfg.CANCEL:
@@ -1359,18 +993,14 @@ def export_events_as_textgrid_old(self):
1359
993
  continue
1360
994
 
1361
995
  try:
1362
- with open(f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.textGrid", "w") as f:
996
+ with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid", "w") as f:
1363
997
  f.write(out)
1364
998
  file_count += 1
1365
- self.results.ptText.appendHtml(
1366
- f"File {pl.Path(exportDir) / util.safeFileName(obs_id)}.textGrid was created."
1367
- )
999
+ self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid was created.")
1368
1000
  QApplication.processEvents()
1369
1001
  except Exception:
1370
- self.results.ptText.appendHtml(
1371
- f"The file {pl.Path(exportDir) / util.safeFileName(obs_id)}.textGrid can not be created."
1372
- )
1002
+ self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid can not be created.")
1373
1003
  QApplication.processEvents()
1374
1004
 
1375
- self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {exportDir}.")
1005
+ self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")
1376
1006
  QApplication.processEvents()