boris-behav-obs 9.2__tar.gz → 9.2.3__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 (123) hide show
  1. boris_behav_obs-9.2.3/PKG-INFO +33 -0
  2. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/advanced_event_filtering.py +5 -3
  3. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/behavior_binary_table.py +5 -5
  4. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/connections.py +3 -1
  5. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/cooccurence.py +4 -2
  6. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/core.py +35 -31
  7. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/export_events.py +8 -6
  8. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/export_observation.py +4 -1
  9. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/gui_utilities.py +19 -11
  10. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/observation_operations.py +16 -9
  11. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/player_dock_widget.py +19 -4
  12. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plot_events.py +1 -1
  13. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plugins.py +31 -4
  14. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/project_functions.py +6 -14
  15. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/synthetic_time_budget.py +11 -7
  16. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/time_budget_functions.py +1 -1
  17. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/time_budget_widget.py +6 -4
  18. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/utilities.py +0 -2
  19. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/version.py +2 -2
  20. boris_behav_obs-9.2.3/boris_behav_obs.egg-info/PKG-INFO +33 -0
  21. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris_behav_obs.egg-info/SOURCES.txt +2 -1
  22. boris_behav_obs-9.2.3/boris_behav_obs.egg-info/entry_points.txt +2 -0
  23. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris_behav_obs.egg-info/requires.txt +0 -1
  24. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/pyproject.toml +12 -13
  25. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_utilities.py +7 -0
  26. boris_behav_obs-9.2.3/tests/test_utilities2.py +124 -0
  27. boris_behav_obs-9.2/PKG-INFO +0 -711
  28. boris_behav_obs-9.2/boris_behav_obs.egg-info/PKG-INFO +0 -711
  29. boris_behav_obs-9.2/boris_behav_obs.egg-info/entry_points.txt +0 -2
  30. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/LICENSE.TXT +0 -0
  31. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/MANIFEST.in +0 -0
  32. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/README.TXT +0 -0
  33. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/README.md +0 -0
  34. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/__init__.py +0 -0
  35. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/__main__.py +0 -0
  36. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/about.py +0 -0
  37. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/add_modifier.py +0 -0
  38. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/add_modifier_ui.py +0 -0
  39. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/analysis_plugins/__init__.py +0 -0
  40. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/analysis_plugins/number_of_occurences.py +0 -0
  41. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  42. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/analysis_plugins/time_budget.py +0 -0
  43. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/behav_coding_map_creator.py +0 -0
  44. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/behaviors_coding_map.py +0 -0
  45. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/boris_cli.py +0 -0
  46. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/cmd_arguments.py +0 -0
  47. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/coding_pad.py +0 -0
  48. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/config.py +0 -0
  49. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/config_file.py +0 -0
  50. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/converters.py +0 -0
  51. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/converters_ui.py +0 -0
  52. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/core_qrc.py +0 -0
  53. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/core_ui.py +0 -0
  54. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/db_functions.py +0 -0
  55. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/dev.py +0 -0
  56. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/dialog.py +0 -0
  57. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/duration_widget.py +0 -0
  58. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/edit_event.py +0 -0
  59. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/edit_event_ui.py +0 -0
  60. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/event_operations.py +0 -0
  61. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/events_cursor.py +0 -0
  62. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/events_snapshots.py +0 -0
  63. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/exclusion_matrix.py +0 -0
  64. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/external_processes.py +0 -0
  65. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/geometric_measurement.py +0 -0
  66. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/image_overlay.py +0 -0
  67. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/import_observations.py +0 -0
  68. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/irr.py +0 -0
  69. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/latency.py +0 -0
  70. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/map_creator.py +0 -0
  71. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/measurement_widget.py +0 -0
  72. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/media_file.py +0 -0
  73. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/menu_options.py +0 -0
  74. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/modifiers_coding_map.py +0 -0
  75. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/mpv-1.0.3.py +0 -0
  76. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/mpv.py +0 -0
  77. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/mpv2.py +0 -0
  78. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/observation.py +0 -0
  79. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/observation_ui.py +0 -0
  80. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/observations_list.py +0 -0
  81. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/otx_parser.py +0 -0
  82. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/param_panel.py +0 -0
  83. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/param_panel_ui.py +0 -0
  84. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plot_data_module.py +0 -0
  85. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plot_events_rt.py +0 -0
  86. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plot_spectrogram_rt.py +0 -0
  87. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/plot_waveform_rt.py +0 -0
  88. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/__init__.py +0 -0
  89. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/const.py +0 -0
  90. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/dict.py +0 -0
  91. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/func.py +0 -0
  92. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/interval.py +0 -0
  93. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/portion/io.py +0 -0
  94. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/preferences.py +0 -0
  95. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/preferences_ui.py +0 -0
  96. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/project.py +0 -0
  97. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/project_import_export.py +0 -0
  98. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/project_ui.py +0 -0
  99. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/qrc_boris.py +0 -0
  100. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/qrc_boris5.py +0 -0
  101. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/select_modifiers.py +0 -0
  102. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/select_observations.py +0 -0
  103. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/select_subj_behav.py +0 -0
  104. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/state_events.py +0 -0
  105. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/subjects_pad.py +0 -0
  106. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/transitions.py +0 -0
  107. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/video_equalizer.py +0 -0
  108. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/video_equalizer_ui.py +0 -0
  109. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/video_operations.py +0 -0
  110. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/view_df.py +0 -0
  111. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/view_df_ui.py +0 -0
  112. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris/write_event.py +0 -0
  113. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  114. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/boris_behav_obs.egg-info/top_level.txt +0 -0
  115. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/setup.cfg +0 -0
  116. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_db_functions.py +0 -0
  117. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_export_observation.py +0 -0
  118. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_irr.py +0 -0
  119. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_observation_gui.py +0 -0
  120. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_otx_parser.py +0 -0
  121. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_preferences_gui.py +0 -0
  122. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_project_functions.py +0 -0
  123. {boris_behav_obs-9.2 → boris_behav_obs-9.2.3}/tests/test_time_budget.py +0 -0
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: boris-behav-obs
3
+ Version: 9.2.3
4
+ Summary: BORIS - Behavioral Observation Research Interactive Software
5
+ Author-email: Olivier Friard <olivier.friard@unito.it>
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, http://www.boris.unito.it
8
+ Project-URL: Documentation, https://boris.readthedocs.io/en/latest/
9
+ Project-URL: Change_log, https://github.com/olivierfriard/BORIS/wiki/BORIS-change-log-v.8
10
+ Project-URL: Source_code, https://github.com/olivierfriard/BORIS
11
+ Project-URL: Issues, https://github.com/olivierfriard/BORIS/issues
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/x-rst
20
+ License-File: LICENSE.TXT
21
+ Requires-Dist: exifread>=3.0.0
22
+ Requires-Dist: numpy>=1.26.4
23
+ Requires-Dist: matplotlib>=3.3.3
24
+ Requires-Dist: pandas>=2.2.2
25
+ Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
26
+ Requires-Dist: pyreadr
27
+ Requires-Dist: pyside6==6.8.0.2
28
+ Requires-Dist: hachoir>=3.3.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-cov; extra == "dev"
33
+ Dynamic: license-file
@@ -359,8 +359,10 @@ def event_filtering(self):
359
359
  selected_observations,
360
360
  start_coding=start_coding,
361
361
  end_coding=end_coding,
362
- start_interval=start_interval,
363
- end_interval=end_interval,
362
+ # start_interval=start_interval,
363
+ # end_interval=end_interval,
364
+ start_interval=None,
365
+ end_interval=None,
364
366
  maxTime=max_media_duration_all_obs,
365
367
  show_include_modifiers=False,
366
368
  show_exclude_non_coded_behaviors=False,
@@ -368,7 +370,7 @@ def event_filtering(self):
368
370
  n_observations=len(selected_observations),
369
371
  )
370
372
 
371
- if parameters == {}:
373
+ if not parameters:
372
374
  return
373
375
 
374
376
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
@@ -80,12 +80,12 @@ def create_behavior_binary_table(pj: dict, selected_observations: list, paramete
80
80
  end_time = dec(max_obs_length)
81
81
 
82
82
  if parameters_obs["time"] == cfg.TIME_OBS_INTERVAL:
83
- obs_interval = pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
83
+ obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
84
84
  offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
85
85
  start_time = dec(obs_interval[0]) + offset
86
86
  # Use max observation length for end time if no interval is defined (=0)
87
87
  max_obs_length, _ = observation_operations.observation_length(pj, [obs_id])
88
- end_time = dec(obs_interval[1]) + offset if obs_interval[1] != 0 else dec(max_obs_length)
88
+ end_time = dec(obs_interval[1]) + offset if obs_interval[1] not in (0, None) else dec(max_obs_length)
89
89
 
90
90
  if obs_id not in results_df:
91
91
  results_df[obs_id] = {}
@@ -166,8 +166,8 @@ def behavior_binary_table(self):
166
166
  None,
167
167
  cfg.programName,
168
168
  (
169
- "Depending of the length of your observations "
170
- "the execution of this function may be very long.<br>"
169
+ "Depending on the length of yours observations "
170
+ "the execution of this function may take a long time.<br>"
171
171
  "The program interface may freeze, be patient. <br>"
172
172
  ),
173
173
  )
@@ -224,7 +224,7 @@ def behavior_binary_table(self):
224
224
  by_category=False,
225
225
  n_observations=len(selected_observations),
226
226
  )
227
- if parameters == {}:
227
+ if not parameters:
228
228
  return
229
229
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
230
230
  QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
@@ -271,7 +271,9 @@ def connections(self):
271
271
  self.actionViewBehavior.triggered.connect(self.view_behavior)
272
272
  self.twEthogram.addAction(self.actionViewBehavior)
273
273
 
274
- self.actionFilterBehaviors.triggered.connect(lambda: self.filter_behaviors(table=cfg.ETHOGRAM))
274
+ self.actionFilterBehaviors.triggered.connect(
275
+ lambda: self.filter_behaviors(table=cfg.ETHOGRAM, behavior_type=cfg.STATE_EVENT_TYPES + cfg.POINT_EVENT_TYPES)
276
+ )
275
277
  self.twEthogram.addAction(self.actionFilterBehaviors)
276
278
 
277
279
  self.actionShowAllBehaviors.triggered.connect(self.show_all_behaviors)
@@ -106,8 +106,10 @@ def get_cooccurence(self):
106
106
  selected_observations,
107
107
  start_coding=start_coding,
108
108
  end_coding=end_coding,
109
- start_interval=start_interval,
110
- end_interval=end_interval,
109
+ # start_interval=start_interval,
110
+ # end_interval=end_interval,
111
+ start_interval=None,
112
+ end_interval=None,
111
113
  maxTime=max_media_duration_all_obs,
112
114
  n_observations=len(selected_observations),
113
115
  show_include_modifiers=False,
@@ -794,10 +794,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
794
794
 
795
795
  def filter_behaviors(
796
796
  self,
797
- title="Select the behaviors to show in the ethogram table",
798
- text="Behaviors to show in ethogram list",
799
- table=cfg.ETHOGRAM,
800
- behavior_type=cfg.STATE_EVENT_TYPES,
797
+ title: str = "Select the behaviors to show in the ethogram table",
798
+ text: str = "Behaviors to show in ethogram list",
799
+ table: str = cfg.ETHOGRAM,
800
+ behavior_type: list = cfg.STATE_EVENT_TYPES,
801
801
  ) -> Tuple[bool, list]:
802
802
  """
803
803
  allow user to:
@@ -809,6 +809,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
809
809
  title (str): title of dialog box
810
810
  text (str): text of dialog box
811
811
  table (str): table where behaviors will be filtered
812
+ behavior_type
812
813
 
813
814
  Returns:
814
815
  (None if table = ETHOGRAM)
@@ -858,21 +859,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
858
859
  categories = ["###no category###"]
859
860
 
860
861
  for category in categories:
861
- if category != "###no category###":
862
- if category == "":
863
- paramPanelWindow.item = QListWidgetItem("No category")
864
- paramPanelWindow.item.setData(34, "No category")
865
- else:
866
- paramPanelWindow.item = QListWidgetItem(category)
867
- paramPanelWindow.item.setData(34, category)
862
+ if category == "###no category###":
863
+ continue
864
+ if category == "":
865
+ paramPanelWindow.item = QListWidgetItem("No category")
866
+ paramPanelWindow.item.setData(34, "No category")
867
+ else:
868
+ paramPanelWindow.item = QListWidgetItem(category)
869
+ paramPanelWindow.item.setData(34, category)
868
870
 
869
- font = QFont()
870
- font.setBold(True)
871
- paramPanelWindow.item.setFont(font)
872
- paramPanelWindow.item.setData(33, "category")
873
- paramPanelWindow.item.setData(35, False)
871
+ font = QFont()
872
+ font.setBold(True)
873
+ paramPanelWindow.item.setFont(font)
874
+ paramPanelWindow.item.setData(33, "category")
875
+ paramPanelWindow.item.setData(35, False)
874
876
 
875
- paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
877
+ paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
876
878
 
877
879
  # check if behavior type must be shown
878
880
  for behavior in [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]:
@@ -3631,6 +3633,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3631
3633
  else:
3632
3634
  current_time = self.getLaps()
3633
3635
 
3636
+ # check if observation time interval
3637
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]:
3638
+ # check if current time outside of interval
3639
+ if current_time >= self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1]:
3640
+ self.beep("beep")
3641
+ self.liveTimer.stop()
3642
+ self.liveObservationStarted = False
3643
+ # set current time to end of observation interval
3644
+ current_time = dec(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
3645
+ self.pb_live_obs.setText("Live observation finished")
3646
+
3634
3647
  self.lb_current_media_time.setText(util.convertTime(self.timeFormat, current_time))
3635
3648
 
3636
3649
  # extract State events
@@ -3660,14 +3673,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3660
3673
  self.liveTimer.stop()
3661
3674
  self.pb_live_obs.setText("Live observation stopped (scan sampling)")
3662
3675
 
3663
- # observation time interval
3664
- if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]:
3665
- if current_time >= self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]:
3666
- self.beep("beep")
3667
- self.liveTimer.stop()
3668
- self.liveObservationStarted = False
3669
- self.pb_live_obs.setText("Live observation finished")
3670
-
3671
3676
  def start_live_observation(self):
3672
3677
  """
3673
3678
  activate the live observation mode
@@ -3769,8 +3774,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3769
3774
  selected_observations,
3770
3775
  start_coding=start_coding,
3771
3776
  end_coding=end_coding,
3772
- start_interval=start_interval,
3773
- end_interval=end_interval,
3777
+ # start_interval=start_interval,
3778
+ # end_interval=end_interval,
3779
+ start_interval=None,
3780
+ end_interval=None,
3774
3781
  maxTime=max_media_duration_all_obs,
3775
3782
  show_include_modifiers=False,
3776
3783
  show_exclude_non_coded_behaviors=False,
@@ -5747,10 +5754,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5747
5754
  if self.playerType == cfg.MEDIA:
5748
5755
  self.pause_video()
5749
5756
 
5750
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
5751
- self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]))
5752
- else:
5753
- self.seek_mediaplayer(0)
5757
+ self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]))
5754
5758
 
5755
5759
  self.update_visualizations()
5756
5760
 
@@ -76,7 +76,7 @@ def export_events_as_behavioral_sequences(self, separated_subjects=False, timed=
76
76
  else:
77
77
  max_media_duration_all_obs = None
78
78
  start_coding, end_coding = dec("NaN"), dec("NaN")
79
- start_interval, end_interval = dec("NaN"), dec("NaN")
79
+ start_interval, end_interval = None, None
80
80
 
81
81
  parameters = select_subj_behav.choose_obs_subj_behav_category(
82
82
  self,
@@ -169,7 +169,7 @@ def export_tabular_events(self, mode: str = "tabular") -> None:
169
169
  else:
170
170
  max_media_duration_all_obs = None
171
171
  start_coding, end_coding = dec("NaN"), dec("NaN")
172
- start_interval, end_interval = dec("NaN"), dec("NaN")
172
+ start_interval, end_interval = None, None
173
173
 
174
174
  parameters = select_subj_behav.choose_obs_subj_behav_category(
175
175
  self,
@@ -373,7 +373,7 @@ def export_aggregated_events(self):
373
373
  else:
374
374
  max_media_duration_all_obs = None
375
375
  start_coding, end_coding = dec("NaN"), dec("NaN")
376
- start_interval, end_interval = dec("NaN"), dec("NaN")
376
+ start_interval, end_interval = None, None
377
377
 
378
378
  parameters = select_subj_behav.choose_obs_subj_behav_category(
379
379
  self,
@@ -681,8 +681,10 @@ def export_events_as_textgrid(self) -> None:
681
681
  selected_observations,
682
682
  start_coding=start_coding,
683
683
  end_coding=end_coding,
684
- start_interval=start_interval,
685
- end_interval=end_interval,
684
+ # start_interval=start_interval,
685
+ # end_interval=end_interval,
686
+ start_interval=None,
687
+ end_interval=None,
686
688
  show_include_modifiers=False,
687
689
  show_exclude_non_coded_behaviors=False,
688
690
  maxTime=max_obs_length,
@@ -786,7 +788,7 @@ def export_events_as_textgrid(self) -> None:
786
788
 
787
789
  if parameters["time"] == cfg.TIME_OBS_INTERVAL:
788
790
  max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
789
- obs_interval = self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
791
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
790
792
  offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
791
793
  min_time = float(obs_interval[0]) + offset
792
794
  # Use max media duration for max time if no interval is defined (=0)
@@ -121,7 +121,10 @@ def export_events_jwatcher(
121
121
  except Exception:
122
122
  # coded behavior not defined in ethogram
123
123
  continue
124
- if [ethogram[k][cfg.TYPE] for k in ethogram if ethogram[k][cfg.BEHAVIOR_CODE] == behav_code] == [cfg.STATE_EVENT]:
124
+ if [ethogram[k][cfg.TYPE] for k in ethogram if ethogram[k][cfg.BEHAVIOR_CODE] == behav_code] in [
125
+ [cfg.STATE_EVENT],
126
+ [cfg.STATE_EVENT_WITH_CODING_MAP],
127
+ ]:
125
128
  if behav_code in mem_number_of_state_events:
126
129
  mem_number_of_state_events[behav_code] += 1
127
130
  else:
@@ -31,32 +31,40 @@ def save_geometry(widget: QWidget, widget_name: str):
31
31
  save window geometry in ini file
32
32
  """
33
33
 
34
- try:
35
- ini_file_path = pl.Path.home() / pl.Path(".boris")
36
- if ini_file_path.is_file():
34
+ ini_file_path = pl.Path.home() / pl.Path(".boris")
35
+ if ini_file_path.is_file():
36
+ try:
37
37
  settings = QSettings(str(ini_file_path), QSettings.IniFormat)
38
38
  settings.setValue(f"{widget_name} geometry", widget.saveGeometry())
39
- except Exception:
40
- logging.warning(f"error during saving {widget_name} geometry")
39
+ except Exception:
40
+ logging.warning(f"error during saving {widget_name} geometry")
41
41
 
42
42
 
43
- def restore_geometry(widget: QWidget, widget_name: str, default_geometry):
43
+ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
44
44
  """
45
45
  restore window geometry in ini file
46
46
  """
47
+ def default_resize(widget, default_width_height):
48
+ if default_width_height != (0, 0):
49
+ try:
50
+ widget.resize(default_width_height[0], default_width_height[1])
51
+ except Exception:
52
+ logging.warning("Error during restoring default")
53
+
47
54
 
55
+ logging.debug(f'restore geometry function for {widget_name}')
48
56
  try:
49
57
  ini_file_path = pl.Path.home() / pl.Path(".boris")
50
58
  if ini_file_path.is_file():
51
59
  settings = QSettings(str(ini_file_path), QSettings.IniFormat)
60
+ print(settings.value(f"{widget_name} geometry"))
52
61
  widget.restoreGeometry(settings.value(f"{widget_name} geometry"))
62
+ logging.debug(f'geometry restored for {widget_name} {settings.value(f"{widget_name} geometry")}')
63
+ else:
64
+ default_resize(widget, default_width_height)
53
65
  except Exception:
54
66
  logging.warning(f"error during restoring {widget_name} geometry")
55
- if default_geometry != (0, 0):
56
- try:
57
- widget.resize(default_geometry[0], default_geometry[1])
58
- except Exception:
59
- logging.warning("Error during restoring default")
67
+ default_resize(widget, default_width_height)
60
68
 
61
69
 
62
70
  def set_icons(self, theme_mode: str) -> None:
@@ -359,14 +359,20 @@ def time_intervals_range(observations: dict, observations_list: list) -> Tuple[O
359
359
  decimal.Decimal: time of latest end interval
360
360
 
361
361
  """
362
- start_interval_list = []
363
- end_interval_list = []
362
+ start_interval_list: list = []
363
+ end_interval_list: list = []
364
364
  for obs_id in observations_list:
365
365
  observation = observations[obs_id]
366
366
  offset = observation[cfg.TIME_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)
367
+ # check if observation interval is defined
368
+ if (
369
+ not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0]
370
+ and not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1]
371
+ ):
372
+ return None, None
373
+
374
+ start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
375
+ end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
370
376
 
371
377
  if not start_interval_list:
372
378
  earliest_start_interval = None
@@ -872,8 +878,8 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
872
878
  observationWindow.cb_observation_time_interval.setText(
873
879
  (
874
880
  "Limit observation to a time interval: "
875
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][0]} - "
876
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][1]}"
881
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
882
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
877
883
  )
878
884
  )
879
885
 
@@ -1950,8 +1956,9 @@ def initialize_new_media_observation(self) -> bool:
1950
1956
  )
1951
1957
 
1952
1958
  # position media
1953
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
1954
- self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]), player=i)
1959
+ self.seek_mediaplayer(
1960
+ int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]), player=i
1961
+ )
1955
1962
 
1956
1963
  # restore video zoom level
1957
1964
  if cfg.ZOOM_LEVEL in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
@@ -23,7 +23,18 @@ This file is part of BORIS.
23
23
  import sys
24
24
  import logging
25
25
  import functools
26
- from PySide6.QtWidgets import QLabel, QDockWidget, QWidget, QHBoxLayout, QVBoxLayout, QSlider, QSizePolicy, QStackedWidget, QToolButton
26
+ from PySide6.QtWidgets import (
27
+ QApplication,
28
+ QLabel,
29
+ QDockWidget,
30
+ QWidget,
31
+ QHBoxLayout,
32
+ QVBoxLayout,
33
+ QSlider,
34
+ QSizePolicy,
35
+ QStackedWidget,
36
+ QToolButton,
37
+ )
27
38
  from PySide6.QtCore import Signal, QEvent, Qt
28
39
  from PySide6.QtGui import QIcon, QAction
29
40
 
@@ -141,7 +152,10 @@ class DW_player(QDockWidget):
141
152
  self.mute_button.setFocusPolicy(Qt.NoFocus)
142
153
  self.mute_button.setAutoRaise(True)
143
154
  self.mute_action = QAction()
144
- self.mute_action.setIcon(QIcon(":/volume_xmark"))
155
+
156
+ theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
157
+
158
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
145
159
  self.mute_action.triggered.connect(self.mute_action_triggered)
146
160
  self.mute_button.setDefaultAction(self.mute_action)
147
161
 
@@ -180,10 +194,11 @@ class DW_player(QDockWidget):
180
194
  """
181
195
  emit signal when mute action is triggered
182
196
  """
197
+ theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
183
198
  if self.player.mute:
184
- self.mute_action.setIcon(QIcon(":/volume_xmark"))
199
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
185
200
  else:
186
- self.mute_action.setIcon(QIcon(":/volume_off"))
201
+ self.mute_action.setIcon(QIcon(f":/volume_off_{theme_mode}"))
187
202
  self.mute_action_triggered_signal.emit(self.id_)
188
203
 
189
204
  def keyPressEvent(self, event):
@@ -441,7 +441,7 @@ def create_events_plot(
441
441
  max_time = float(obs_length)
442
442
 
443
443
  if interval == cfg.TIME_OBS_INTERVAL:
444
- obs_interval = self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
444
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
445
445
  offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
446
446
  min_time = float(obs_interval[0]) + offset
447
447
  # Use max media duration for max time if no interval is defined (=0)
@@ -65,24 +65,44 @@ def load_plugins(self):
65
65
  """
66
66
  load selected plugins in analysis menu
67
67
  """
68
+
69
+ def msg():
70
+ QMessageBox.warning(
71
+ self,
72
+ cfg.programName,
73
+ f"A plugin with the same name is already loaded ({self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name]}).\n\nThe plugin from {file_} is not loaded.",
74
+ QMessageBox.Ok | QMessageBox.Default,
75
+ QMessageBox.NoButton,
76
+ )
77
+
68
78
  self.menu_plugins.clear()
69
79
  self.config_param[cfg.ANALYSIS_PLUGINS] = {}
70
80
 
71
81
  # load BORIS plugins
72
- for file_ in (Path(__file__).parent / "analysis_plugins").glob("*.py"):
82
+ for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
73
83
  if file_.name == "__init__.py":
74
84
  continue
75
85
  plugin_name = get_plugin_name(file_)
76
86
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
87
+ # check if plugin with same name already loaded
88
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
89
+ msg()
90
+ continue
91
+
77
92
  self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
78
93
 
79
94
  # load personal plugins
80
95
  if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
81
- for file_ in Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py"):
96
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
82
97
  if file_.name == "__init__.py":
83
98
  continue
84
99
  plugin_name = get_plugin_name(file_)
85
100
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
101
+ # check if plugin with same name already loaded
102
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
103
+ msg()
104
+ continue
105
+
86
106
  self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
87
107
 
88
108
  logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
@@ -108,7 +128,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
108
128
  # filter selected behaviors
109
129
  df = df[df["Behavior"].isin(parameters["selected behaviors"])]
110
130
 
111
- if parameters["time"] == "interval of observation":
131
+ if parameters["time"] == cfg.TIME_OBS_INTERVAL:
112
132
  # filter each observation with observation interval start/stop
113
133
 
114
134
  # keep events between observation interval start time and observation interval stop/end
@@ -142,6 +162,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
142
162
  | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
143
163
  ]
144
164
 
165
+ # cut state events to interval
145
166
  df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
146
167
  df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
147
168
 
@@ -152,7 +173,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
152
173
  print("filtered")
153
174
  print("=" * 50)
154
175
 
155
- print(f"{df=}")
176
+ # print(f"{df=}")
156
177
 
157
178
  return df
158
179
 
@@ -205,8 +226,12 @@ def run_plugin(self, plugin_name):
205
226
  if not selected_observations:
206
227
  return
207
228
 
229
+ logging.info("preparing dtaaframe for plugin")
230
+
208
231
  df = project_functions.project2dataframe(self.pj, selected_observations)
209
232
 
233
+ logging.info("done")
234
+
210
235
  """
211
236
  logging.debug("dataframe info")
212
237
  logging.debug(f"{df.info()}")
@@ -214,7 +239,9 @@ def run_plugin(self, plugin_name):
214
239
  """
215
240
 
216
241
  # filter the dataframe with parameters
242
+ logging.info("filtering dataframe for plugin")
217
243
  filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
244
+ logging.info("done")
218
245
 
219
246
  plugin_results = plugin_module.run(filtered_df)
220
247
  # test if plugin_tests is a tuple: if not transform to tuple
@@ -1899,20 +1899,12 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1899
1899
  if idx_event in stop_event_idx:
1900
1900
  continue
1901
1901
  data["Observation id"].append(obs_id)
1902
- data["Observation date"].append(pj["observations"][obs_id]["date"])
1903
- data["Description"].append(" ".join(pj["observations"][obs_id]["description"].splitlines()))
1904
- data["Observation type"].append(pj["observations"][obs_id]["type"])
1905
-
1906
- data["Observation interval start"].append(
1907
- pj["observations"][obs_id]["observation time interval"][0]
1908
- if pj["observations"][obs_id]["observation time interval"][0]
1909
- else None
1910
- )
1911
- data["Observation interval stop"].append(
1912
- pj["observations"][obs_id]["observation time interval"][1]
1913
- if pj["observations"][obs_id]["observation time interval"][1]
1914
- else None
1915
- )
1902
+ data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
1903
+ data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
1904
+ data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
1905
+
1906
+ data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
1907
+ data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
1916
1908
 
1917
1909
  # data["Source"].append("")
1918
1910
  # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
@@ -71,8 +71,10 @@ def synthetic_time_budget(self) -> None:
71
71
  selected_observations,
72
72
  start_coding=start_coding,
73
73
  end_coding=end_coding,
74
- start_interval=start_interval,
75
- end_interval=end_interval,
74
+ # start_interval=start_interval,
75
+ # end_interval=end_interval,
76
+ start_interval=None,
77
+ end_interval=None,
76
78
  maxTime=max_media_duration_all_obs,
77
79
  show_exclude_non_coded_behaviors=False,
78
80
  by_category=False,
@@ -93,7 +95,7 @@ def synthetic_time_budget(self) -> None:
93
95
  title="Select behaviors to exclude from the total time",
94
96
  text="The duration of the selected behaviors will be subtracted from the total time",
95
97
  table="",
96
- behavior_type=[cfg.STATE_EVENT],
98
+ behavior_type=cfg.STATE_EVENT_TYPES,
97
99
  )
98
100
  if cancel_pressed:
99
101
  return
@@ -201,8 +203,10 @@ def synthetic_binned_time_budget(self) -> None:
201
203
  selected_observations,
202
204
  start_coding=start_coding,
203
205
  end_coding=end_coding,
204
- start_interval=start_interval,
205
- end_interval=end_interval,
206
+ # start_interval=start_interval,
207
+ # end_interval=end_interval,
208
+ start_interval=None,
209
+ end_interval=None,
206
210
  maxTime=max_media_duration_all_obs,
207
211
  show_exclude_non_coded_behaviors=False,
208
212
  by_category=False,
@@ -221,9 +225,9 @@ def synthetic_binned_time_budget(self) -> None:
221
225
  # ask for excluding behaviors durations from total time
222
226
  cancel_pressed, synth_tb_param[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
223
227
  title="Select behaviors to exclude",
224
- text=("The duration of the selected behaviors will " "be subtracted from the total time"),
228
+ text=("The duration of the selected behaviors will be subtracted from the total time"),
225
229
  table="",
226
- behavior_type=[cfg.STATE_EVENT],
230
+ behavior_type=cfg.STATE_EVENT_TYPES,
227
231
  )
228
232
  if cancel_pressed:
229
233
  return
@@ -260,7 +260,7 @@ def synthetic_time_budget_bin(pj: dict, selected_observations: list, parameters_
260
260
  max_time = dec(end_time)
261
261
 
262
262
  if time_interval == cfg.TIME_OBS_INTERVAL:
263
- obs_interval = pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
263
+ obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
264
264
  offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
265
265
  min_time = dec(obs_interval[0]) + offset
266
266
  # Use max media duration for max time if no interval is defined (=0)