boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.2__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 (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  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 +16 -34
  23. boris/config.py +108 -49
  24. boris/config_file.py +58 -67
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2106 -1277
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +134 -0
  34. boris/dialog.py +461 -242
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +405 -281
  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 +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +304 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +127 -36
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +304 -123
  79. boris/preferences_ui.py +684 -227
  80. boris/project.py +293 -270
  81. boris/project_functions.py +618 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +25 -17
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +71 -86
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.2.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.2.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.2.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
  123. boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.2.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.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)
@@ -148,9 +150,7 @@ class timeBudgetResults(QWidget):
148
150
 
149
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
@@ -164,9 +164,7 @@ class timeBudgetResults(QWidget):
164
164
  # check if file with new extension already exists
165
165
  if pl.Path(file_name).is_file():
166
166
  if (
167
- dialog.MessageDialog(
168
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
169
- )
167
+ dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
170
168
  == cfg.CANCEL
171
169
  ):
172
170
  return
@@ -206,9 +204,9 @@ class timeBudgetResults(QWidget):
206
204
  for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
207
205
  if self.lw.count() == 1:
208
206
  # var has value in obs?
209
- if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][
210
- self.lw.item(0).text()
211
- ].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
+ ):
212
210
  col1.append(
213
211
  self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
214
212
  self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
@@ -392,6 +390,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
392
390
  Args:
393
391
  mode (str): ["by_behavior", "by_category"]
394
392
  mode2 (str): must be in ["list", "current"]
393
+ "current" time budget of current observation
395
394
  """
396
395
 
397
396
  if mode2 == "current":
@@ -418,9 +417,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
418
417
  flagGroup: bool = False
419
418
  if len(selected_observations) > 1:
420
419
  flagGroup = (
421
- dialog.MessageDialog(
422
- cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO]
423
- )
420
+ dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
424
421
  == cfg.YES
425
422
  )
426
423
 
@@ -428,20 +425,25 @@ def time_budget(self, mode: str, mode2: str = "list"):
428
425
  self.pj[cfg.OBSERVATIONS], selected_observations
429
426
  )
430
427
 
431
- logging.debug(
432
- f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
433
- )
428
+ logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
434
429
 
435
430
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
436
431
 
437
- 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(
438
435
  self,
439
436
  selected_observations,
440
437
  start_coding=start_coding,
441
438
  end_coding=end_coding,
439
+ # start_interval=start_interval,
440
+ # end_interval=end_interval,
441
+ start_interval=None,
442
+ end_interval=None,
442
443
  maxTime=max_media_duration_all_obs,
443
444
  by_category=(mode == "by_category"),
444
445
  n_observations=len(selected_observations),
446
+ show_exclude_non_coded_modifiers=True,
445
447
  )
446
448
  if parameters == {}:
447
449
  return
@@ -450,21 +452,28 @@ def time_budget(self, mode: str, mode2: str = "list"):
450
452
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
451
453
  return
452
454
 
455
+ logging.debug(f"{parameters=}")
456
+
453
457
  # ask for excluding behaviors durations from total time
454
- if not start_coding.is_nan():
458
+ if start_coding is not None and not start_coding.is_nan():
455
459
  cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
456
460
  title="Select behaviors to exclude from the total time",
457
- 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"),
458
462
  table="",
459
- behavior_type=[cfg.STATE_EVENT],
463
+ behavior_type=cfg.STATE_EVENT_TYPES,
460
464
  )
461
465
  if cancel_pressed:
462
466
  return
463
467
  else:
464
468
  parameters[cfg.EXCLUDED_BEHAVIORS] = []
465
469
 
470
+ self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
471
+ QApplication.processEvents()
472
+
466
473
  # check if time_budget window must be used
467
474
  if flagGroup or len(selected_observations) == 1:
475
+ t0 = time.time()
476
+
468
477
  cursor = db_functions.load_events_in_db(
469
478
  self.pj,
470
479
  parameters[cfg.SELECTED_SUBJECTS],
@@ -483,7 +492,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
483
492
  if obs_length == dec(-2): # images obs without time
484
493
  parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
485
494
 
486
- if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
495
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
487
496
  min_time = float(0)
488
497
  # check if the last event is recorded after media file length
489
498
  try:
@@ -494,7 +503,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
494
503
  except Exception:
495
504
  max_time = float(obs_length)
496
505
 
497
- 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
498
514
  try:
499
515
  min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
500
516
  except Exception:
@@ -512,8 +528,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
512
528
  # check intervals
513
529
  for subj in parameters[cfg.SELECTED_SUBJECTS]:
514
530
  for behav in parameters[cfg.SELECTED_BEHAVIORS]:
515
- 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:
516
533
  continue
534
+
517
535
  # extract modifiers
518
536
 
519
537
  cursor.execute(
@@ -542,10 +560,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
542
560
  % 2
543
561
  ):
544
562
  cursor.execute(
545
- (
546
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
547
- "VALUES (?,?,?,?,?,?)"
548
- ),
563
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
549
564
  (obsId, subj, behav, "STATE", modifier[0], min_time),
550
565
  )
551
566
 
@@ -562,10 +577,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
562
577
  % 2
563
578
  ):
564
579
  cursor.execute(
565
- (
566
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
567
- "VALUES (?,?,?,?,?,?)"
568
- ),
580
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
569
581
  (obsId, subj, behav, "STATE", modifier[0], max_time),
570
582
  )
571
583
  try:
@@ -580,6 +592,10 @@ def time_budget(self, mode: str, mode2: str = "list"):
580
592
  "DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
581
593
  (obsId, min_time, max_time),
582
594
  )
595
+ try:
596
+ cursor.execute("COMMIT")
597
+ except Exception:
598
+ pass
583
599
 
584
600
  out, categories = time_budget_functions.time_budget_analysis(
585
601
  self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
@@ -591,9 +607,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
591
607
  if element["subject"] not in excl_behaviors_total_time:
592
608
  excl_behaviors_total_time[element["subject"]] = 0
593
609
  if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
594
- excl_behaviors_total_time[element["subject"]] += (
595
- element["duration"] if not isinstance(element["duration"], str) else 0
596
- )
610
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
597
611
 
598
612
  # widget for results visualization
599
613
  self.tb = timeBudgetResults(self.pj, self.config_param)
@@ -613,20 +627,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
613
627
  if len(selected_observations) > 1:
614
628
  if total_observation_time:
615
629
  if self.timeFormat == cfg.HHMMSS:
616
- self.tb.lbTotalObservedTime.setText(
617
- f"Total observation length: {util.seconds2time(total_observation_time)}"
618
- )
630
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
619
631
  if self.timeFormat == cfg.S:
620
- self.tb.lbTotalObservedTime.setText(
621
- f"Total observation length: {float(total_observation_time):0.3f}"
622
- )
632
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
623
633
  else:
624
634
  self.tb.lbTotalObservedTime.setText("Total observation length: not available")
625
635
  else:
626
636
  if self.timeFormat == cfg.HHMMSS:
627
- self.tb.lbTotalObservedTime.setText(
628
- f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}"
629
- )
637
+ self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
630
638
  if self.timeFormat == cfg.S:
631
639
  self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
632
640
 
@@ -638,6 +646,9 @@ def time_budget(self, mode: str, mode2: str = "list"):
638
646
  else:
639
647
  self.tb.excluded_behaviors_list.setVisible(False)
640
648
 
649
+ self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
650
+ logging.debug("Time budget generated")
651
+
641
652
  if mode == "by_behavior":
642
653
  tb_fields = [
643
654
  "Subject",
@@ -685,10 +696,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
685
696
  elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
686
697
  tot_time = float(total_observation_time)
687
698
  # substract time of excluded behaviors from the total for the subject
688
- if (
689
- row["subject"] in excl_behaviors_total_time
690
- and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
691
- ):
699
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
692
700
  tot_time -= excl_behaviors_total_time[row["subject"]]
693
701
  item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
694
702
 
@@ -738,7 +746,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
738
746
 
739
747
  self.tb.twTB.resizeColumnsToContents()
740
748
 
741
- gui_utilities.restore_geometry(self.tb, "time budget", (0, 0))
749
+ gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
742
750
 
743
751
  self.tb.show()
744
752
 
@@ -768,9 +776,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
768
776
  if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
769
777
  workbook = tablib.Databook()
770
778
 
771
- wb_file_name, filter_ = QFileDialog(self).getSaveFileName(
772
- self, "Save Time budget analysis", "", output_format
773
- )
779
+ wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
774
780
  if not wb_file_name:
775
781
  return
776
782
 
@@ -779,9 +785,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
779
785
  # check if file with new extension already exists
780
786
  if pl.Path(wb_file_name).is_file():
781
787
  if (
782
- dialog.MessageDialog(
783
- cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
784
- )
788
+ dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
785
789
  == cfg.CANCEL
786
790
  ):
787
791
  return
@@ -827,16 +831,14 @@ def time_budget(self, mode: str, mode2: str = "list"):
827
831
 
828
832
  mem_command = ""
829
833
  for obsId in selected_observations:
830
- cursor = db_functions.load_events_in_db(
831
- self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
832
- )
834
+ cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
833
835
 
834
836
  obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
835
837
 
836
838
  if obs_length == -1:
837
839
  obs_length = 0
838
840
 
839
- if parameters["time"] == cfg.TIME_FULL_OBS:
841
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
840
842
  min_time = float(0)
841
843
  # check if the last event is recorded after media file length
842
844
  try:
@@ -847,7 +849,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
847
849
  except Exception:
848
850
  max_time = float(obs_length)
849
851
 
850
- if parameters["time"] == cfg.TIME_EVENTS:
852
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
851
853
  try:
852
854
  min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
853
855
  except Exception:
@@ -857,19 +859,15 @@ def time_budget(self, mode: str, mode2: str = "list"):
857
859
  except Exception:
858
860
  max_time = float(obs_length)
859
861
 
860
- if parameters["time"] == cfg.TIME_ARBITRARY_INTERVAL:
862
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
861
863
  min_time = float(parameters[cfg.START_TIME])
862
864
  max_time = float(parameters[cfg.END_TIME])
863
865
 
864
866
  # check intervals
865
867
  for subj in parameters[cfg.SELECTED_SUBJECTS]:
866
868
  for behav in parameters[cfg.SELECTED_BEHAVIORS]:
867
- if cfg.POINT in project_functions.event_type(
868
- behav, self.pj[cfg.ETHOGRAM]
869
- ): # self.eventType(behav).upper():
869
+ if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
870
870
  continue
871
- # extract modifiers
872
- # if plot_parameters["include modifiers"]:
873
871
 
874
872
  cursor.execute(
875
873
  "SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
@@ -892,10 +890,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
892
890
  % 2
893
891
  ):
894
892
  cursor.execute(
895
- (
896
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
897
- "VALUES (?,?,?,?,?,?)"
898
- ),
893
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
899
894
  (obsId, subj, behav, "STATE", modifier[0], min_time),
900
895
  )
901
896
  if (
@@ -911,10 +906,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
911
906
  % 2
912
907
  ):
913
908
  cursor.execute(
914
- (
915
- "INSERT INTO events (observation, subject, code, type, modifiers, occurence) "
916
- "VALUES (?,?,?,?,?,?)"
917
- ),
909
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
918
910
  (obsId, subj, behav, cfg.STATE, modifier[0], max_time),
919
911
  )
920
912
  try:
@@ -937,9 +929,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
937
929
  if element["subject"] not in excl_behaviors_total_time:
938
930
  excl_behaviors_total_time[element["subject"]] = 0
939
931
  if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
940
- excl_behaviors_total_time[element["subject"]] += (
941
- element["duration"] if element["duration"] != "NA" else 0
942
- )
932
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
943
933
 
944
934
  rows: list = []
945
935
  col1: list = []
@@ -953,9 +943,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
953
943
  indep_var_values: list = []
954
944
  for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
955
945
  indep_var_label.append(v["label"])
956
- indep_var_values.append(
957
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], "")
958
- )
946
+ indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
959
947
 
960
948
  header.extend(indep_var_label)
961
949
  col1.extend(indep_var_values)
@@ -981,10 +969,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
981
969
  elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
982
970
  tot_time = float(max_time - min_time)
983
971
  # substract duration of excluded behaviors from total time for each subject
984
- if (
985
- row["subject"] in excl_behaviors_total_time
986
- and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]
987
- ):
972
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
988
973
  tot_time -= excl_behaviors_total_time[row["subject"]]
989
974
  # % of tot time
990
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)