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
@@ -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
 
@@ -26,6 +26,7 @@ import pathlib as pl
26
26
  from decimal import Decimal as dec
27
27
  from io import StringIO
28
28
  import pandas as pd
29
+ import time
29
30
 
30
31
  try:
31
32
  import pyreadr
@@ -36,8 +37,8 @@ except ModuleNotFoundError:
36
37
 
37
38
 
38
39
  import tablib
39
- from PyQt5.QtCore import Qt
40
- from PyQt5.QtWidgets import (
40
+ from PySide6.QtCore import Qt
41
+ from PySide6.QtWidgets import (
41
42
  QFileDialog,
42
43
  QHBoxLayout,
43
44
  QInputDialog,
@@ -51,6 +52,7 @@ from PyQt5.QtWidgets import (
51
52
  QTableWidgetItem,
52
53
  QVBoxLayout,
53
54
  QWidget,
55
+ QApplication,
54
56
  )
55
57
 
56
58
  from . import config as cfg
@@ -110,7 +112,7 @@ class timeBudgetResults(QWidget):
110
112
  spacerItem = QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
111
113
  hbox2.addItem(spacerItem)
112
114
 
113
- self.pbClose = QPushButton("Close", clicked=self.close_clicked)
115
+ self.pbClose = QPushButton(cfg.CLOSE, clicked=self.close_clicked)
114
116
  hbox2.addWidget(self.pbClose)
115
117
 
116
118
  hbox.addLayout(hbox2)
@@ -146,24 +148,23 @@ class timeBudgetResults(QWidget):
146
148
 
147
149
  logging.debug("save time budget results to file")
148
150
 
149
- file_formats = [cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML, cfg.PANDAS_DF, cfg.RDS]
151
+ file_formats = (cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML, cfg.TEXT_FILE, cfg.PANDAS_DF, cfg.RDS)
150
152
 
151
- file_name, filter_ = QFileDialog().getSaveFileName(
152
- self, "Save Time budget analysis", "", ";;".join(file_formats)
153
- )
153
+ file_name, filter_ = QFileDialog().getSaveFileName(self, "Save Time budget analysis", "", ";;".join(file_formats))
154
154
 
155
155
  if not file_name:
156
156
  return
157
157
 
158
158
  # add correct file extension if not present
159
159
  if pl.Path(file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
160
- file_name = str(pl.Path(file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
160
+ if cfg.FILE_NAME_SUFFIX[filter_] != "cli":
161
+ file_name = str(pl.Path(file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
162
+ else:
163
+ file_name = str(pl.Path(file_name))
161
164
  # check if file with new extension already exists
162
165
  if pl.Path(file_name).is_file():
163
166
  if (
164
- dialog.MessageDialog(
165
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
166
- )
167
+ dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
167
168
  == cfg.CANCEL
168
169
  ):
169
170
  return
@@ -203,9 +204,9 @@ class timeBudgetResults(QWidget):
203
204
  for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
204
205
  if self.lw.count() == 1:
205
206
  # var has value in obs?
206
- if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][
207
- self.lw.item(0).text()
208
- ].get(cfg.INDEPENDENT_VARIABLES, []):
207
+ if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get(
208
+ cfg.INDEPENDENT_VARIABLES, []
209
+ ):
209
210
  col1.append(
210
211
  self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
211
212
  self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
@@ -376,7 +377,7 @@ class timeBudgetResults(QWidget):
376
377
 
377
378
  # write results
378
379
  with open(file_name, "wb") as f:
379
- if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML):
380
+ if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML, cfg.TEXT_FILE):
380
381
  f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
381
382
  if filter_ in (cfg.ODS, cfg.XLSX, cfg.XLS):
382
383
  f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
@@ -389,6 +390,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
389
390
  Args:
390
391
  mode (str): ["by_behavior", "by_category"]
391
392
  mode2 (str): must be in ["list", "current"]
393
+ "current" time budget of current observation
392
394
  """
393
395
 
394
396
  if mode2 == "current":
@@ -415,9 +417,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
415
417
  flagGroup: bool = False
416
418
  if len(selected_observations) > 1:
417
419
  flagGroup = (
418
- dialog.MessageDialog(
419
- cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO]
420
- )
420
+ dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
421
421
  == cfg.YES
422
422
  )
423
423
 
@@ -425,20 +425,25 @@ def time_budget(self, mode: str, mode2: str = "list"):
425
425
  self.pj[cfg.OBSERVATIONS], selected_observations
426
426
  )
427
427
 
428
- logging.debug(
429
- f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
430
- )
428
+ logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
431
429
 
432
430
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
433
431
 
434
- parameters = select_subj_behav.choose_obs_subj_behav_category(
432
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
433
+
434
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
435
435
  self,
436
436
  selected_observations,
437
437
  start_coding=start_coding,
438
438
  end_coding=end_coding,
439
+ # start_interval=start_interval,
440
+ # end_interval=end_interval,
441
+ start_interval=None,
442
+ end_interval=None,
439
443
  maxTime=max_media_duration_all_obs,
440
444
  by_category=(mode == "by_category"),
441
445
  n_observations=len(selected_observations),
446
+ show_exclude_non_coded_modifiers=True,
442
447
  )
443
448
  if parameters == {}:
444
449
  return
@@ -447,21 +452,28 @@ def time_budget(self, mode: str, mode2: str = "list"):
447
452
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
448
453
  return
449
454
 
455
+ logging.debug(f"{parameters=}")
456
+
450
457
  # ask for excluding behaviors durations from total time
451
- if not start_coding.is_nan():
458
+ if start_coding is not None and not start_coding.is_nan():
452
459
  cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
453
460
  title="Select behaviors to exclude from the total time",
454
- text=("The duration of the selected behaviors will " "be subtracted from the total time"),
461
+ text=("The duration of the selected behaviors will be subtracted from the total time"),
455
462
  table="",
456
- behavior_type=[cfg.STATE_EVENT],
463
+ behavior_type=cfg.STATE_EVENT_TYPES,
457
464
  )
458
465
  if cancel_pressed:
459
466
  return
460
467
  else:
461
468
  parameters[cfg.EXCLUDED_BEHAVIORS] = []
462
469
 
470
+ self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
471
+ QApplication.processEvents()
472
+
463
473
  # check if time_budget window must be used
464
474
  if flagGroup or len(selected_observations) == 1:
475
+ t0 = time.time()
476
+
465
477
  cursor = db_functions.load_events_in_db(
466
478
  self.pj,
467
479
  parameters[cfg.SELECTED_SUBJECTS],
@@ -480,7 +492,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
480
492
  if obs_length == dec(-2): # images obs without time
481
493
  parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
482
494
 
483
- if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
495
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
484
496
  min_time = float(0)
485
497
  # check if the last event is recorded after media file length
486
498
  try:
@@ -491,7 +503,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
491
503
  except Exception:
492
504
  max_time = float(obs_length)
493
505
 
494
- if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
506
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_OBS_INTERVAL:
507
+ obs_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
508
+ offset = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
509
+ min_time = float(obs_interval[0]) + offset
510
+ # Use max media duration for max time if no interval is defined (=0)
511
+ max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(obs_length)
512
+
513
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS: # events duration
495
514
  try:
496
515
  min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
497
516
  except Exception:
@@ -509,8 +528,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
509
528
  # check intervals
510
529
  for subj in parameters[cfg.SELECTED_SUBJECTS]:
511
530
  for behav in parameters[cfg.SELECTED_BEHAVIORS]:
512
- if cfg.POINT in self.eventType(behav).upper():
531
+ # if cfg.POINT in self.eventType(behav).upper():
532
+ if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
513
533
  continue
534
+
514
535
  # extract modifiers
515
536
 
516
537
  cursor.execute(
@@ -539,10 +560,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
539
560
  % 2
540
561
  ):
541
562
  cursor.execute(
542
- (
543
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
544
- "VALUES (?,?,?,?,?,?)"
545
- ),
563
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
546
564
  (obsId, subj, behav, "STATE", modifier[0], min_time),
547
565
  )
548
566
 
@@ -559,10 +577,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
559
577
  % 2
560
578
  ):
561
579
  cursor.execute(
562
- (
563
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
564
- "VALUES (?,?,?,?,?,?)"
565
- ),
580
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
566
581
  (obsId, subj, behav, "STATE", modifier[0], max_time),
567
582
  )
568
583
  try:
@@ -577,6 +592,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
577
592
  "DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
578
593
  (obsId, min_time, max_time),
579
594
  )
595
+ try:
596
+ cursor.execute("COMMIT")
597
+ except Exception:
598
+ pass
580
599
 
581
600
  out, categories = time_budget_functions.time_budget_analysis(
582
601
  self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
@@ -588,9 +607,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
588
607
  if element["subject"] not in excl_behaviors_total_time:
589
608
  excl_behaviors_total_time[element["subject"]] = 0
590
609
  if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
591
- excl_behaviors_total_time[element["subject"]] += (
592
- element["duration"] if not isinstance(element["duration"], str) else 0
593
- )
610
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
594
611
 
595
612
  # widget for results visualization
596
613
  self.tb = timeBudgetResults(self.pj, self.config_param)
@@ -610,20 +627,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
610
627
  if len(selected_observations) > 1:
611
628
  if total_observation_time:
612
629
  if self.timeFormat == cfg.HHMMSS:
613
- self.tb.lbTotalObservedTime.setText(
614
- f"Total observation length: {util.seconds2time(total_observation_time)}"
615
- )
630
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
616
631
  if self.timeFormat == cfg.S:
617
- self.tb.lbTotalObservedTime.setText(
618
- f"Total observation length: {float(total_observation_time):0.3f}"
619
- )
632
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
620
633
  else:
621
634
  self.tb.lbTotalObservedTime.setText("Total observation length: not available")
622
635
  else:
623
636
  if self.timeFormat == cfg.HHMMSS:
624
- self.tb.lbTotalObservedTime.setText(
625
- f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}"
626
- )
637
+ self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
627
638
  if self.timeFormat == cfg.S:
628
639
  self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
629
640
 
@@ -635,6 +646,9 @@ def time_budget(self, mode: str, mode2: str = "list"):
635
646
  else:
636
647
  self.tb.excluded_behaviors_list.setVisible(False)
637
648
 
649
+ self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
650
+ logging.debug("Time budget generated")
651
+
638
652
  if mode == "by_behavior":
639
653
  tb_fields = [
640
654
  "Subject",
@@ -682,10 +696,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
682
696
  elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
683
697
  tot_time = float(total_observation_time)
684
698
  # substract time of excluded behaviors from the total for the subject
685
- if (
686
- row["subject"] in excl_behaviors_total_time
687
- and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
688
- ):
699
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
689
700
  tot_time -= excl_behaviors_total_time[row["subject"]]
690
701
  item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
691
702
 
@@ -735,7 +746,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
735
746
 
736
747
  self.tb.twTB.resizeColumnsToContents()
737
748
 
738
- gui_utilities.restore_geometry(self.tb, "time budget", (0, 0))
749
+ gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
739
750
 
740
751
  self.tb.show()
741
752
 
@@ -765,9 +776,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
765
776
  if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
766
777
  workbook = tablib.Databook()
767
778
 
768
- wb_file_name, filter_ = QFileDialog(self).getSaveFileName(
769
- self, "Save Time budget analysis", "", output_format
770
- )
779
+ wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
771
780
  if not wb_file_name:
772
781
  return
773
782
 
@@ -776,9 +785,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
776
785
  # check if file with new extension already exists
777
786
  if pl.Path(wb_file_name).is_file():
778
787
  if (
779
- dialog.MessageDialog(
780
- cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
781
- )
788
+ dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
782
789
  == cfg.CANCEL
783
790
  ):
784
791
  return
@@ -824,16 +831,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
824
831
 
825
832
  mem_command = ""
826
833
  for obsId in selected_observations:
827
- cursor = db_functions.load_events_in_db(
828
- self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
829
- )
834
+ cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
830
835
 
831
836
  obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
832
837
 
833
838
  if obs_length == -1:
834
839
  obs_length = 0
835
840
 
836
- if parameters["time"] == cfg.TIME_FULL_OBS:
841
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
837
842
  min_time = float(0)
838
843
  # check if the last event is recorded after media file length
839
844
  try:
@@ -844,7 +849,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
844
849
  except Exception:
845
850
  max_time = float(obs_length)
846
851
 
847
- if parameters["time"] == cfg.TIME_EVENTS:
852
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
848
853
  try:
849
854
  min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
850
855
  except Exception:
@@ -854,19 +859,15 @@ def time_budget(self, mode: str, mode2: str = "list"):
854
859
  except Exception:
855
860
  max_time = float(obs_length)
856
861
 
857
- if parameters["time"] == cfg.TIME_ARBITRARY_INTERVAL:
862
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
858
863
  min_time = float(parameters[cfg.START_TIME])
859
864
  max_time = float(parameters[cfg.END_TIME])
860
865
 
861
866
  # check intervals
862
867
  for subj in parameters[cfg.SELECTED_SUBJECTS]:
863
868
  for behav in parameters[cfg.SELECTED_BEHAVIORS]:
864
- if cfg.POINT in project_functions.event_type(
865
- behav, self.pj[cfg.ETHOGRAM]
866
- ): # self.eventType(behav).upper():
869
+ if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
867
870
  continue
868
- # extract modifiers
869
- # if plot_parameters["include modifiers"]:
870
871
 
871
872
  cursor.execute(
872
873
  "SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
@@ -889,10 +890,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
889
890
  % 2
890
891
  ):
891
892
  cursor.execute(
892
- (
893
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
894
- "VALUES (?,?,?,?,?,?)"
895
- ),
893
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
896
894
  (obsId, subj, behav, "STATE", modifier[0], min_time),
897
895
  )
898
896
  if (
@@ -908,10 +906,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
908
906
  % 2
909
907
  ):
910
908
  cursor.execute(
911
- (
912
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
913
- "VALUES (?,?,?,?,?,?)"
914
- ),
909
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
915
910
  (obsId, subj, behav, cfg.STATE, modifier[0], max_time),
916
911
  )
917
912
  try:
@@ -934,9 +929,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
934
929
  if element["subject"] not in excl_behaviors_total_time:
935
930
  excl_behaviors_total_time[element["subject"]] = 0
936
931
  if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
937
- excl_behaviors_total_time[element["subject"]] += (
938
- element["duration"] if element["duration"] != "NA" else 0
939
- )
932
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
940
933
 
941
934
  rows: list = []
942
935
  col1: list = []
@@ -950,9 +943,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
950
943
  indep_var_values: list = []
951
944
  for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
952
945
  indep_var_label.append(v["label"])
953
- indep_var_values.append(
954
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], "")
955
- )
946
+ indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
956
947
 
957
948
  header.extend(indep_var_label)
958
949
  col1.extend(indep_var_values)
@@ -978,10 +969,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
978
969
  elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
979
970
  tot_time = float(max_time - min_time)
980
971
  # substract duration of excluded behaviors from total time for each subject
981
- if (
982
- row["subject"] in excl_behaviors_total_time
983
- and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
984
- ):
972
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
985
973
  tot_time -= excl_behaviors_total_time[row["subject"]]
986
974
  # % of tot time
987
975
  values.append(round(row["duration"] / tot_time * 100, 1) if tot_time > 0 else cfg.NA)
boris/transitions.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
 
@@ -24,8 +24,9 @@ import logging
24
24
  import os
25
25
  import subprocess
26
26
  import tempfile
27
+ from pathlib import Path
27
28
 
28
- from PyQt5.QtWidgets import QFileDialog, QMessageBox
29
+ from PySide6.QtWidgets import QFileDialog, QMessageBox
29
30
 
30
31
  from . import config as cfg
31
32
  from . import dialog, export_observation, select_subj_behav
@@ -134,7 +135,7 @@ def create_transitions_gv_from_matrix(matrix, cutoff_all=0, cutoff_behavior=0, e
134
135
  else:
135
136
  transitions[row.split("\t")[0]][behaviours[idx]] = int(r)
136
137
 
137
- '''transitions_total_number = sum([sum(transitions[x].values()) for x in transitions])'''
138
+ """transitions_total_number = sum([sum(transitions[x].values()) for x in transitions])"""
138
139
 
139
140
  out = "digraph G { \n"
140
141
 
@@ -170,6 +171,8 @@ def transitions_matrix(self, mode):
170
171
  * number
171
172
  * frequencies_after_behaviors
172
173
  """
174
+ logging.debug("flag transitions_matrix function")
175
+
173
176
  # ask user observations to analyze
174
177
  _, selected_observations = select_observations.select_observations2(
175
178
  self, cfg.MULTIPLE, windows_title="Select observations for transitions matrix"
@@ -181,8 +184,8 @@ def transitions_matrix(self, mode):
181
184
  parameters = select_subj_behav.choose_obs_subj_behav_category(
182
185
  self,
183
186
  selected_observations,
184
- flagShowIncludeModifiers=True,
185
- flagShowExcludeBehaviorsWoEvents=False,
187
+ show_include_modifiers=True,
188
+ show_exclude_non_coded_behaviors=False,
186
189
  n_observations=len(selected_observations),
187
190
  )
188
191
 
@@ -194,20 +197,20 @@ def transitions_matrix(self, mode):
194
197
 
195
198
  flagMulti = False
196
199
  if len(parameters[cfg.SELECTED_SUBJECTS]) == 1:
197
- fn = QFileDialog().getSaveFileName(
200
+ file_name, _ = QFileDialog().getSaveFileName(
198
201
  None,
199
202
  "Create matrix of transitions " + mode,
200
203
  "",
201
204
  "Transitions matrix files (*.txt *.tsv);;All files (*)",
202
205
  )
203
- fileName = fn[0] if type(fn) is tuple else fn # PyQt4/5
204
-
206
+ if not file_name:
207
+ return
205
208
  else:
206
- exportDir = QFileDialog(self).getExistingDirectory(
209
+ exportDir = QFileDialog.getExistingDirectory(
207
210
  self,
208
211
  "Choose a directory to save the transitions matrices",
209
- os.path.expanduser("~"),
210
- options=QFileDialog(self).ShowDirsOnly,
212
+ str(Path.home()),
213
+ options=QFileDialog.ShowDirsOnly,
211
214
  )
212
215
  if not exportDir:
213
216
  return
@@ -220,9 +223,7 @@ def transitions_matrix(self, mode):
220
223
  strings_list = []
221
224
  for obs_id in selected_observations:
222
225
  strings_list.append(
223
- export_observation.events_to_behavioral_sequences(
224
- self.pj, obs_id, subject, parameters, self.behav_seq_separator
225
- )
226
+ export_observation.events_to_behavioral_sequences(self.pj, obs_id, subject, parameters, self.behav_seq_separator)
226
227
  )
227
228
 
228
229
  sequences, observed_behaviors = behavioral_strings_analysis(strings_list, self.behav_seq_separator)
@@ -258,11 +259,11 @@ def transitions_matrix(self, mode):
258
259
  QMessageBox.critical(self, cfg.programName, f"The file {nf} can not be saved")
259
260
  else:
260
261
  try:
261
- with open(fileName, "w") as outfile:
262
+ with open(file_name, "w") as outfile:
262
263
  outfile.write(observed_matrix)
263
264
 
264
265
  except Exception:
265
- QMessageBox.critical(self, cfg.programName, f"The file {fileName} can not be saved")
266
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
266
267
 
267
268
 
268
269
  def transitions_dot_script():
@@ -270,21 +271,18 @@ def transitions_dot_script():
270
271
  create dot script (graphviz language) from transitions frequencies matrix
271
272
  """
272
273
 
273
- fn = QFileDialog().getOpenFileNames(
274
+ file_names, _ = QFileDialog().getOpenFileNames(
274
275
  None,
275
276
  "Select one or more transitions matrix files",
276
277
  "",
277
278
  "Transitions matrix files (*.txt *.tsv);;All files (*)",
278
279
  )
279
- fileNames = fn[0] if type(fn) is tuple else fn
280
280
 
281
281
  out = ""
282
282
 
283
- for fileName in fileNames:
284
- with open(fileName, "r") as infile:
285
- result, gv = create_transitions_gv_from_matrix(
286
- infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node"
287
- )
283
+ for file_name in file_names:
284
+ with open(file_name, "r") as infile:
285
+ result, gv = create_transitions_gv_from_matrix(infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node")
288
286
  if result:
289
287
  QMessageBox.critical(
290
288
  None,
@@ -293,16 +291,24 @@ def transitions_dot_script():
293
291
  )
294
292
  return
295
293
 
296
- with open(fileName + ".gv", "w") as f:
297
- f.write(gv)
294
+ try:
295
+ with open(file_name + ".gv", "w") as file_out:
296
+ file_out.write(gv)
297
+ except Exception:
298
+ QMessageBox.critical(
299
+ None,
300
+ cfg.programName,
301
+ ("Error saving the file"),
302
+ )
303
+ return
298
304
 
299
- out += f"<b>{fileName}.gv</b> created<br>"
305
+ out += f"<b>{file_name}.gv</b> created<br>"
300
306
 
301
307
  if out:
302
308
  QMessageBox.information(
303
309
  None,
304
310
  cfg.programName,
305
- (f"{out}<br><br>The DOT scripts can be used with Graphviz or WebGraphviz " "to generate diagram"),
311
+ (f"{out}<br><br>The DOT scripts can be used with the Graphviz package or WebGraphviz to generate diagram"),
306
312
  )
307
313
 
308
314
 
@@ -326,20 +332,17 @@ def transitions_flow_diagram():
326
332
  )
327
333
  return
328
334
 
329
- fn = QFileDialog().getOpenFileNames(
335
+ file_names, _ = QFileDialog().getOpenFileNames(
330
336
  None,
331
337
  "Select one or more transitions matrix files",
332
338
  "",
333
339
  "Transitions matrix files (*.txt *.tsv);;All files (*)",
334
340
  )
335
- fileNames = fn[0] if type(fn) is tuple else fn
336
341
 
337
342
  out = ""
338
- for fileName in fileNames:
339
- with open(fileName, "r") as infile:
340
- result, gv = create_transitions_gv_from_matrix(
341
- infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node"
342
- )
343
+ for file_name in file_names:
344
+ with open(file_name, "r") as infile:
345
+ result, gv = create_transitions_gv_from_matrix(infile.read(), cutoff_all=0, cutoff_behavior=0, edge_label="percent_node")
343
346
  if result:
344
347
  QMessageBox.critical(
345
348
  None,
@@ -348,18 +351,15 @@ def transitions_flow_diagram():
348
351
  )
349
352
  return
350
353
 
351
- with open(tempfile.gettempdir() + os.sep + os.path.basename(fileName) + ".tmp.gv", "w") as f:
354
+ with open(tempfile.gettempdir() + os.sep + os.path.basename(file_name) + ".tmp.gv", "w") as f:
352
355
  f.write(gv)
353
356
  result = subprocess.getoutput(
354
- (
355
- f'dot -Tpng -o "{fileName}.png" '
356
- f'"{tempfile.gettempdir() + os.sep + os.path.basename(fileName)}.tmp.gv"'
357
- )
357
+ (f'dot -Tpng -o "{file_name}.png" "{tempfile.gettempdir() + os.sep + os.path.basename(file_name)}.tmp.gv"')
358
358
  )
359
359
  if not result:
360
- out += f"<b>{fileName}.png</b> created<br>"
360
+ out += f"<b>{file_name}.png</b> created<br>"
361
361
  else:
362
- out += f"Problem with <b>{fileName}</b><br>"
362
+ out += f"Problem with <b>{file_name}</b><br>"
363
363
 
364
364
  if out:
365
365
  QMessageBox.information(None, cfg.programName, out)