boris-behav-obs 9.1.1__tar.gz → 9.2__tar.gz

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 (122) hide show
  1. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/PKG-INFO +1 -1
  2. boris_behav_obs-9.2/boris/analysis_plugins/number_of_occurences.py +22 -0
  3. boris_behav_obs-9.2/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +34 -0
  4. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/analysis_plugins/time_budget.py +9 -47
  5. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/cooccurence.py +2 -1
  6. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/core.py +11 -91
  7. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/observation_operations.py +5 -5
  8. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/param_panel.py +6 -0
  9. boris_behav_obs-9.2/boris/plugins.py +247 -0
  10. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/project_functions.py +37 -22
  11. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/select_subj_behav.py +4 -0
  12. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/version.py +2 -2
  13. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/PKG-INFO +1 -1
  14. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/pyproject.toml +2 -2
  15. boris_behav_obs-9.1.1/boris/analysis_plugins/number_of_occurences.py +0 -62
  16. boris_behav_obs-9.1.1/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -74
  17. boris_behav_obs-9.1.1/boris/plugins.py +0 -79
  18. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/LICENSE.TXT +0 -0
  19. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/MANIFEST.in +0 -0
  20. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/README.TXT +0 -0
  21. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/README.md +0 -0
  22. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/__init__.py +0 -0
  23. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/__main__.py +0 -0
  24. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/about.py +0 -0
  25. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/add_modifier.py +0 -0
  26. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/add_modifier_ui.py +0 -0
  27. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/advanced_event_filtering.py +0 -0
  28. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/analysis_plugins/__init__.py +0 -0
  29. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/behav_coding_map_creator.py +0 -0
  30. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/behavior_binary_table.py +0 -0
  31. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/behaviors_coding_map.py +0 -0
  32. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/boris_cli.py +0 -0
  33. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/cmd_arguments.py +0 -0
  34. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/coding_pad.py +0 -0
  35. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/config.py +0 -0
  36. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/config_file.py +0 -0
  37. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/connections.py +0 -0
  38. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/converters.py +0 -0
  39. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/converters_ui.py +0 -0
  40. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/core_qrc.py +0 -0
  41. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/core_ui.py +0 -0
  42. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/db_functions.py +0 -0
  43. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/dev.py +0 -0
  44. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/dialog.py +0 -0
  45. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/duration_widget.py +0 -0
  46. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/edit_event.py +0 -0
  47. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/edit_event_ui.py +0 -0
  48. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/event_operations.py +0 -0
  49. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/events_cursor.py +0 -0
  50. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/events_snapshots.py +0 -0
  51. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/exclusion_matrix.py +0 -0
  52. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/export_events.py +0 -0
  53. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/export_observation.py +0 -0
  54. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/external_processes.py +0 -0
  55. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/geometric_measurement.py +0 -0
  56. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/gui_utilities.py +0 -0
  57. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/image_overlay.py +0 -0
  58. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/import_observations.py +0 -0
  59. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/irr.py +0 -0
  60. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/latency.py +0 -0
  61. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/map_creator.py +0 -0
  62. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/measurement_widget.py +0 -0
  63. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/media_file.py +0 -0
  64. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/menu_options.py +0 -0
  65. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/modifiers_coding_map.py +0 -0
  66. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/mpv-1.0.3.py +0 -0
  67. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/mpv.py +0 -0
  68. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/mpv2.py +0 -0
  69. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/observation.py +0 -0
  70. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/observation_ui.py +0 -0
  71. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/observations_list.py +0 -0
  72. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/otx_parser.py +0 -0
  73. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/param_panel_ui.py +0 -0
  74. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/player_dock_widget.py +0 -0
  75. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/plot_data_module.py +0 -0
  76. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/plot_events.py +0 -0
  77. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/plot_events_rt.py +0 -0
  78. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/plot_spectrogram_rt.py +0 -0
  79. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/plot_waveform_rt.py +0 -0
  80. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/__init__.py +0 -0
  81. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/const.py +0 -0
  82. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/dict.py +0 -0
  83. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/func.py +0 -0
  84. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/interval.py +0 -0
  85. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/portion/io.py +0 -0
  86. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/preferences.py +0 -0
  87. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/preferences_ui.py +0 -0
  88. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/project.py +0 -0
  89. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/project_import_export.py +0 -0
  90. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/project_ui.py +0 -0
  91. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/qrc_boris.py +0 -0
  92. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/qrc_boris5.py +0 -0
  93. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/select_modifiers.py +0 -0
  94. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/select_observations.py +0 -0
  95. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/state_events.py +0 -0
  96. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/subjects_pad.py +0 -0
  97. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/synthetic_time_budget.py +0 -0
  98. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/time_budget_functions.py +0 -0
  99. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/time_budget_widget.py +0 -0
  100. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/transitions.py +0 -0
  101. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/utilities.py +0 -0
  102. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/video_equalizer.py +0 -0
  103. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/video_equalizer_ui.py +0 -0
  104. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/video_operations.py +0 -0
  105. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/view_df.py +0 -0
  106. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/view_df_ui.py +0 -0
  107. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris/write_event.py +0 -0
  108. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/SOURCES.txt +0 -0
  109. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  110. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  111. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/requires.txt +0 -0
  112. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/boris_behav_obs.egg-info/top_level.txt +0 -0
  113. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/setup.cfg +0 -0
  114. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_db_functions.py +0 -0
  115. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_export_observation.py +0 -0
  116. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_irr.py +0 -0
  117. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_observation_gui.py +0 -0
  118. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_otx_parser.py +0 -0
  119. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_preferences_gui.py +0 -0
  120. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_project_functions.py +0 -0
  121. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_time_budget.py +0 -0
  122. {boris_behav_obs-9.1.1 → boris_behav_obs-9.2}/tests/test_utilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: boris-behav-obs
3
- Version: 9.1.1
3
+ Version: 9.2
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -0,0 +1,22 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ number of occurences of behaviors
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ __version__ = "0.3.0"
10
+ __version_date__ = "2025-03-17"
11
+ __plugin_name__ = "Number of occurences of behaviors"
12
+ __author__ = "Olivier Friard - University of Torino - Italy"
13
+
14
+
15
+ def run(df: pd.DataFrame):
16
+ """
17
+ Calculate the number of occurrences of behaviors by subject.
18
+ """
19
+
20
+ df_results: pd.DataFrame = df.groupby(["Subject", "Behavior"])["Behavior"].count().reset_index(name="number of occurences")
21
+
22
+ return df_results
@@ -0,0 +1,34 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ number of occurences of behaviors by independent_variable
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ __version__ = "0.3.0"
10
+ __version_date__ = "2025-03-17"
11
+ __plugin_name__ = "Number of occurences of behaviors by subject by independent_variable"
12
+ __author__ = "Olivier Friard - University of Torino - Italy"
13
+
14
+
15
+ def run(df: pd.DataFrame):
16
+ """
17
+ Calculate the number of occurrences of behaviors by subject and by independent_variable.
18
+
19
+ This plugin returns a Pandas dataframe
20
+ """
21
+
22
+ df_results: df.DataFrame = (
23
+ df.groupby(
24
+ [
25
+ "independent variable 'Weather'",
26
+ "Subject",
27
+ "Behavior",
28
+ ]
29
+ )["Behavior"]
30
+ .count()
31
+ .reset_index(name="number of occurences")
32
+ )
33
+
34
+ return df_results
@@ -7,8 +7,8 @@ Time budget
7
7
  import pandas as pd
8
8
  import numpy as np
9
9
 
10
- __version__ = "0.2.0"
11
- __version_date__ = "2025-01-25"
10
+ __version__ = "0.3.0"
11
+ __version_date__ = "2025-03-17"
12
12
  __plugin_name__ = "Time budget"
13
13
  __author__ = "Olivier Friard - University of Torino - Italy"
14
14
 
@@ -18,7 +18,7 @@ def run(df: pd.DataFrame):
18
18
  Calculate the following values:
19
19
 
20
20
  - Total number of occurences of behavior
21
- - Total duration of behavior (in seconds)
21
+ - Total duration of behavior (in seconds) (pandas.DataFrame.sum() ignore NaN values when computing the sum. Use min_count=1)
22
22
  - Duration mean of behavior (in seconds)
23
23
  - Standard deviation of behavior duration (in seconds)
24
24
  - Inter-event intervals mean (in seconds)
@@ -26,11 +26,15 @@ def run(df: pd.DataFrame):
26
26
  - % of total subject observation duration
27
27
  """
28
28
 
29
+ print("running time budget plugin")
30
+
31
+ print(df)
32
+
29
33
  group_by = ["Subject", "Behavior"]
30
34
 
31
35
  dfs = [
32
36
  df.groupby(group_by)["Behavior"].count().reset_index(name="number of occurences"),
33
- df.groupby(group_by)["Duration (s)"].sum().reset_index(name="total duration"),
37
+ df.groupby(group_by)["Duration (s)"].sum(min_count=1).reset_index(name="total duration"),
34
38
  df.groupby(group_by)["Duration (s)"].mean().astype(float).round(3).reset_index(name="duration mean"),
35
39
  df.groupby(group_by)["Duration (s)"].std().astype(float).round(3).reset_index(name="duration std dev"),
36
40
  ]
@@ -48,7 +52,7 @@ def run(df: pd.DataFrame):
48
52
  interval = (df.groupby(["Subject"])["Stop (s)"].max() - df.groupby(["Subject"])["Start (s)"].min()).replace(0, np.nan)
49
53
 
50
54
  dfs.append(
51
- (100 * df.groupby(group_by)["Duration (s)"].sum() / interval)
55
+ (100 * df.groupby(group_by)["Duration (s)"].sum(min_count=1) / interval)
52
56
  .astype(float)
53
57
  .round(3)
54
58
  .reset_index(name="% of total subject observation duration")
@@ -59,45 +63,3 @@ def run(df: pd.DataFrame):
59
63
  merged_df = pd.merge(merged_df, df, on=group_by)
60
64
 
61
65
  return merged_df
62
-
63
-
64
- def main(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
65
- """
66
- filter by selected observations.
67
- filter by selected subjects.
68
- filter by selected behaviors.
69
- filter by time interval.
70
- """
71
-
72
- # filter selected observations
73
- if observations_list:
74
- df = df[df["Observation id"].isin(observations_list)]
75
-
76
- if parameters:
77
- # filter selected subjects
78
- df = df[df["Subject"].isin(parameters["selected subjects"])]
79
-
80
- # filter selected behaviors
81
- df = df[df["Behavior"].isin(parameters["selected behaviors"])]
82
-
83
- # filter selected time interval
84
- if parameters["start time"] is not None and parameters["end time"] is not None:
85
- MIN_TIME = parameters["start time"]
86
- MAX_TIME = parameters["end time"]
87
-
88
- df_interval = df[
89
- (
90
- ((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
91
- | ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
92
- )
93
- | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
94
- ]
95
-
96
- df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
97
- df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
98
-
99
- df_interval.loc[:, "Duration (s)"] = df_interval["Stop (s)"] - df_interval["Start (s)"]
100
-
101
- df = df_interval
102
-
103
- return run(df)
@@ -98,6 +98,7 @@ def get_cooccurence(self):
98
98
 
99
99
  start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
100
100
 
101
+ # loop on choose subjects /behaviors until parameters are OK
101
102
  while True:
102
103
  flag_ok: bool = True
103
104
  parameters = select_subj_behav.choose_obs_subj_behav_category(
@@ -113,7 +114,7 @@ def get_cooccurence(self):
113
114
  show_exclude_non_coded_behaviors=True,
114
115
  )
115
116
 
116
- if parameters == {}: # cancel button pressed
117
+ if not parameters: # cancel button pressed
117
118
  return
118
119
 
119
120
  if not parameters[cfg.SELECTED_SUBJECTS]:
@@ -34,7 +34,6 @@ import json
34
34
  import logging
35
35
  import pathlib as pl
36
36
  import platform
37
- import importlib
38
37
  import re
39
38
  import PIL.Image
40
39
  import PIL.ImageEnhance
@@ -52,7 +51,6 @@ from decimal import Decimal as dec
52
51
  from decimal import ROUND_DOWN
53
52
  import gzip
54
53
  from collections import deque
55
- import pandas as pd
56
54
  import matplotlib
57
55
  import zipfile
58
56
  import shutil
@@ -129,7 +127,6 @@ from . import config_file
129
127
  from . import select_subj_behav
130
128
  from . import observation_operations
131
129
  from . import write_event
132
- from . import view_df
133
130
 
134
131
 
135
132
  # matplotlib.pyplot.switch_backend("Qt5Agg")
@@ -5762,6 +5759,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5762
5759
  self.extract_frame(self.dw_player[0])
5763
5760
 
5764
5761
  def obs_param(self):
5762
+ """
5763
+ allow user to select observations and then subjects and behaviors
5764
+ """
5765
5765
  _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
5766
5766
 
5767
5767
  if not selected_observations:
@@ -5786,8 +5786,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5786
5786
 
5787
5787
  start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
5788
5788
 
5789
+ print(f"{start_coding=}")
5790
+ print(f"{end_coding=}")
5791
+
5789
5792
  start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
5790
5793
 
5794
+ print(f"{start_interval=}")
5795
+ print(f"{end_interval=}")
5796
+
5791
5797
  parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
5792
5798
  self,
5793
5799
  selected_observations,
@@ -5803,6 +5809,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5803
5809
  if parameters == {}:
5804
5810
  return [], {}
5805
5811
 
5812
+ print(f"{parameters=}")
5813
+
5806
5814
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
5807
5815
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
5808
5816
  return [], {}
@@ -5810,94 +5818,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5810
5818
  logging.debug(f"{parameters=}")
5811
5819
  return selected_observations, parameters
5812
5820
 
5813
- def run_plugin(self):
5814
- """
5815
- run plugin
5816
- """
5817
- if not self.project:
5818
- QMessageBox.warning(
5819
- self,
5820
- cfg.programName,
5821
- "No observations found. Open a project first",
5822
- QMessageBox.Ok | QMessageBox.Default,
5823
- QMessageBox.NoButton,
5824
- )
5825
- return
5826
-
5827
- logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
5828
-
5829
- plugin_name = self.sender().text()
5830
- if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
5831
- QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
5832
- return
5833
-
5834
- plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
5835
-
5836
- logging.debug(f"{plugin_path=}")
5837
-
5838
- if not pl.Path(plugin_path).is_file():
5839
- QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
5840
- return
5841
-
5842
- logging.debug(f"run plugin from {plugin_path}")
5843
-
5844
- module_name = pl.Path(plugin_path).stem
5845
-
5846
- spec = importlib.util.spec_from_file_location(module_name, plugin_path)
5847
- plugin_module = importlib.util.module_from_spec(spec)
5848
-
5849
- logging.debug(f"{plugin_module=}")
5850
-
5851
- spec.loader.exec_module(plugin_module)
5852
-
5853
- logging.info(
5854
- f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
5855
- )
5856
-
5857
- selected_observations, parameters = self.obs_param()
5858
- if not selected_observations:
5859
- return
5860
-
5861
- df = project_functions.project2dataframe(self.pj, selected_observations)
5862
-
5863
- logging.debug("dataframe info")
5864
- logging.debug(f"{df.info()}")
5865
- logging.debug(f"{df.head()}")
5866
-
5867
- # df_results, str_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5868
-
5869
- plugin_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5870
- # test if tuple: if not transform to tuple
5871
- if not isinstance(plugin_results, tuple):
5872
- plugin_results = tuple([plugin_results])
5873
-
5874
- self.plugin_visu: list = []
5875
- for result in plugin_results:
5876
- if isinstance(result, str):
5877
- self.plugin_visu.append(dialog.Results_dialog())
5878
- self.plugin_visu[-1].setWindowTitle(self.sender().text())
5879
- self.plugin_visu[-1].ptText.clear()
5880
- self.plugin_visu[-1].ptText.appendPlainText(result)
5881
- self.plugin_visu[-1].show()
5882
- elif isinstance(result, pd.DataFrame):
5883
- self.plugin_visu.append(
5884
- view_df.View_df(self.sender().text(), f"{plugin_module.__version__} ({plugin_module.__version_date__})", result)
5885
- )
5886
- self.plugin_visu[-1].show()
5887
- else:
5888
- # result is not str nor dataframe
5889
- QMessageBox.critical(
5890
- None,
5891
- cfg.programName,
5892
- (
5893
- f"Plugin returns an unknown object type: {type(result)}\n\n"
5894
- "Plugins must return str and/or Pandas Dataframes.\n"
5895
- "Check the plugin code."
5896
- ),
5897
- QMessageBox.Ok | QMessageBox.Default,
5898
- QMessageBox.NoButton,
5899
- )
5900
-
5901
5821
 
5902
5822
  def main():
5903
5823
  # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
@@ -43,9 +43,8 @@ from PySide6.QtWidgets import (
43
43
  QSlider,
44
44
  QMainWindow,
45
45
  QDockWidget,
46
- QPushButton,
47
46
  )
48
- from PySide6.QtCore import Qt, QDateTime, QTimer, QObject, QEvent
47
+ from PySide6.QtCore import Qt, QDateTime, QTimer
49
48
  from PySide6.QtGui import QFont, QIcon, QTextCursor
50
49
 
51
50
  from PySide6 import QtTest
@@ -365,8 +364,9 @@ def time_intervals_range(observations: dict, observations_list: list) -> Tuple[O
365
364
  for obs_id in observations_list:
366
365
  observation = observations[obs_id]
367
366
  offset = observation[cfg.TIME_OFFSET]
368
- start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
369
- end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
367
+ if dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset and dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset:
368
+ start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
369
+ end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
370
370
 
371
371
  if not start_interval_list:
372
372
  earliest_start_interval = None
@@ -376,7 +376,7 @@ def time_intervals_range(observations: dict, observations_list: list) -> Tuple[O
376
376
  if not end_interval_list:
377
377
  latest_end_interval = None
378
378
  else:
379
- latest_end_interval = min([x for x in end_interval_list])
379
+ latest_end_interval = max([x for x in end_interval_list])
380
380
 
381
381
  return earliest_start_interval, latest_end_interval
382
382
 
@@ -95,7 +95,13 @@ class Param_panel(QDialog, Ui_Dialog):
95
95
  if not ((self.start_interval is None) or self.start_interval.is_nan()):
96
96
  # Set start_time and end_time widgets values even if it is not shown with
97
97
  # more than 1 observation as some analyses might use it (eg: advanced event filtering)
98
+
99
+ print(f"{self.end_interval=}")
100
+
98
101
  end_interval = self.end_interval if self.end_interval != 0 else self.media_duration
102
+
103
+ print(f"{end_interval=}")
104
+
99
105
  self.start_time.set_time(self.start_interval)
100
106
  self.end_time.set_time(end_interval)
101
107
  self.frm_time_interval.setEnabled(False)
@@ -0,0 +1,247 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+ """
21
+
22
+ import importlib
23
+ import logging
24
+ import numpy as np
25
+ import pandas as pd
26
+ from pathlib import Path
27
+
28
+ from PySide6.QtGui import QAction
29
+ from PySide6.QtWidgets import QMessageBox
30
+
31
+ from . import config as cfg
32
+ from . import project_functions
33
+ from . import dialog
34
+ from . import view_df
35
+
36
+
37
+ def add_plugins_to_menu(self):
38
+ """
39
+ add plugins to the plugins menu
40
+ """
41
+ for plugin_name in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
42
+ logging.debug(f"adding plugin '{plugin_name}' to menu")
43
+ # Create an action for each submenu option
44
+ action = QAction(self, triggered=lambda checked=False, name=plugin_name: run_plugin(self, name))
45
+ action.setText(plugin_name)
46
+
47
+ self.menu_plugins.addAction(action)
48
+
49
+
50
+ def get_plugin_name(plugin_path: str):
51
+ """
52
+ get name of plugin
53
+ """
54
+ # search plugin name
55
+ plugin_name = None
56
+ with open(plugin_path, "r") as f_in:
57
+ for line in f_in:
58
+ if line.startswith("__plugin_name__"):
59
+ plugin_name = line.split("=")[1].strip().replace('"', "")
60
+ break
61
+ return plugin_name
62
+
63
+
64
+ def load_plugins(self):
65
+ """
66
+ load selected plugins in analysis menu
67
+ """
68
+ self.menu_plugins.clear()
69
+ self.config_param[cfg.ANALYSIS_PLUGINS] = {}
70
+
71
+ # load BORIS plugins
72
+ for file_ in (Path(__file__).parent / "analysis_plugins").glob("*.py"):
73
+ if file_.name == "__init__.py":
74
+ continue
75
+ plugin_name = get_plugin_name(file_)
76
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
77
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
78
+
79
+ # load personal plugins
80
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
81
+ for file_ in Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py"):
82
+ if file_.name == "__init__.py":
83
+ continue
84
+ plugin_name = get_plugin_name(file_)
85
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
86
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
87
+
88
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
89
+
90
+
91
+ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
92
+ """
93
+ filter the dataframe following parameters
94
+
95
+ filter by selected observations.
96
+ filter by selected subjects.
97
+ filter by selected behaviors.
98
+ filter by time interval.
99
+ """
100
+
101
+ # filter selected observations
102
+ df = df[df["Observation id"].isin(observations_list)]
103
+
104
+ if parameters:
105
+ # filter selected subjects
106
+ df = df[df["Subject"].isin(parameters["selected subjects"])]
107
+
108
+ # filter selected behaviors
109
+ df = df[df["Behavior"].isin(parameters["selected behaviors"])]
110
+
111
+ if parameters["time"] == "interval of observation":
112
+ # filter each observation with observation interval start/stop
113
+
114
+ # keep events between observation interval start time and observation interval stop/end
115
+ df_interval = df[
116
+ (
117
+ ((df["Start (s)"] >= df["Observation interval start"]) & (df["Start (s)"] <= df["Observation interval stop"]))
118
+ | ((df["Stop (s)"] >= df["Observation interval start"]) & (df["Stop (s)"] <= df["Observation interval stop"]))
119
+ )
120
+ | ((df["Start (s)"] < df["Observation interval start"]) & (df["Stop (s)"] > df["Observation interval stop"]))
121
+ ]
122
+
123
+ df_interval.loc[df["Start (s)"] < df["Observation interval start"], "Start (s)"] = df["Observation interval start"]
124
+ df_interval.loc[df["Stop (s)"] > df["Observation interval stop"], "Stop (s)"] = df["Observation interval stop"]
125
+
126
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
127
+
128
+ df = df_interval
129
+
130
+ else:
131
+ # filter selected time interval
132
+ if parameters["start time"] is not None and parameters["end time"] is not None:
133
+ MIN_TIME = parameters["start time"]
134
+ MAX_TIME = parameters["end time"]
135
+
136
+ # keep events between start time and end_time
137
+ df_interval = df[
138
+ (
139
+ ((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
140
+ | ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
141
+ )
142
+ | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
143
+ ]
144
+
145
+ df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
146
+ df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
147
+
148
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
149
+
150
+ df = df_interval
151
+
152
+ print("filtered")
153
+ print("=" * 50)
154
+
155
+ print(f"{df=}")
156
+
157
+ return df
158
+
159
+
160
+ def run_plugin(self, plugin_name):
161
+ """
162
+ run plugin
163
+ """
164
+
165
+ if not self.project:
166
+ QMessageBox.warning(
167
+ self,
168
+ cfg.programName,
169
+ "No observations found. Open a project first",
170
+ QMessageBox.Ok | QMessageBox.Default,
171
+ QMessageBox.NoButton,
172
+ )
173
+ return
174
+
175
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
176
+
177
+ if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
178
+ QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
179
+ return
180
+
181
+ plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
182
+
183
+ logging.debug(f"{plugin_path=}")
184
+
185
+ if not Path(plugin_path).is_file():
186
+ QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
187
+ return
188
+
189
+ logging.debug(f"run plugin from {plugin_path}")
190
+
191
+ module_name = Path(plugin_path).stem
192
+
193
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
194
+ plugin_module = importlib.util.module_from_spec(spec)
195
+
196
+ logging.debug(f"{plugin_module=}")
197
+
198
+ spec.loader.exec_module(plugin_module)
199
+
200
+ logging.info(
201
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
202
+ )
203
+
204
+ selected_observations, parameters = self.obs_param()
205
+ if not selected_observations:
206
+ return
207
+
208
+ df = project_functions.project2dataframe(self.pj, selected_observations)
209
+
210
+ """
211
+ logging.debug("dataframe info")
212
+ logging.debug(f"{df.info()}")
213
+ logging.debug(f"{df.head()}")
214
+ """
215
+
216
+ # filter the dataframe with parameters
217
+ filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
218
+
219
+ plugin_results = plugin_module.run(filtered_df)
220
+ # test if plugin_tests is a tuple: if not transform to tuple
221
+ if not isinstance(plugin_results, tuple):
222
+ plugin_results = tuple([plugin_results])
223
+
224
+ self.plugin_visu: list = []
225
+ for result in plugin_results:
226
+ if isinstance(result, str):
227
+ self.plugin_visu.append(dialog.Results_dialog())
228
+ self.plugin_visu[-1].setWindowTitle(plugin_name)
229
+ self.plugin_visu[-1].ptText.clear()
230
+ self.plugin_visu[-1].ptText.appendPlainText(result)
231
+ self.plugin_visu[-1].show()
232
+ elif isinstance(result, pd.DataFrame):
233
+ self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_module.__version__} ({plugin_module.__version_date__})", result))
234
+ self.plugin_visu[-1].show()
235
+ else:
236
+ # result is not str nor dataframe
237
+ QMessageBox.critical(
238
+ None,
239
+ cfg.programName,
240
+ (
241
+ f"Plugin returns an unknown object type: {type(result)}\n\n"
242
+ "Plugins must return str and/or Pandas Dataframes.\n"
243
+ "Check the plugin code."
244
+ ),
245
+ QMessageBox.Ok | QMessageBox.Default,
246
+ QMessageBox.NoButton,
247
+ )