boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/write_event.py ADDED
@@ -0,0 +1,538 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+
7
+ This program is free software; you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation; either version 2 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program; if not, write to the Free Software
19
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ MA 02110-1301, USA.
21
+
22
+ """
23
+
24
+ import bisect
25
+ import logging
26
+ from decimal import Decimal as dec
27
+ import re
28
+ import pathlib as pl
29
+
30
+ from . import config as cfg
31
+ from . import dialog
32
+ from . import utilities as util
33
+ from . import select_modifiers
34
+ from . import event_operations
35
+
36
+
37
+ def write_event(self, event: dict, mem_time: dec) -> int:
38
+ """
39
+ add event from pressed key to observation
40
+ offset is added to event time
41
+ ask for modifiers if configured
42
+ load events in tableview
43
+ scroll to active event
44
+
45
+ Args:
46
+ event (dict): event parameters
47
+ memTime (Decimal): time
48
+
49
+ """
50
+
51
+ logging.debug(f"write event - event: {event} memtime: {mem_time}")
52
+
53
+ if event is None:
54
+ return 1
55
+
56
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
57
+ # live observation finished (end of time interval reached)
58
+ if not self.liveObservationStarted and mem_time.is_nan():
59
+ _ = dialog.MessageDialog(
60
+ cfg.programName,
61
+ (
62
+ "The live observation is finished.<br>"
63
+ "The observation interval is "
64
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
65
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
66
+ ),
67
+ (cfg.OK,),
68
+ )
69
+ return 1
70
+
71
+ if mem_time < self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]:
72
+ _ = dialog.MessageDialog(
73
+ cfg.programName,
74
+ (
75
+ "The live observation has not began.<br>"
76
+ "The observation interval is "
77
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
78
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
79
+ ),
80
+ (cfg.OK,),
81
+ )
82
+ return 1
83
+
84
+ editing_event = "row" in event
85
+
86
+ # add time offset if not from editing
87
+ if not editing_event:
88
+ # add offset
89
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
90
+ mem_time += dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET]).quantize(dec(".001"))
91
+
92
+ # add media creation time
93
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
94
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
95
+ media_file_name = self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"]
96
+
97
+ logging.debug(f"{media_file_name=}")
98
+
99
+ media_file_name_posix = pl.Path(media_file_name).as_posix()
100
+
101
+ logging.debug(f"{media_file_name_posix=}")
102
+
103
+ # add media creation date/time
104
+
105
+ mem_time += dec(
106
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_file_name_posix]
107
+ )
108
+
109
+ # check if time > 2**31 - 1 (2147483647)
110
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.VIEWER_MEDIA, cfg.LIVE, cfg.VIEWER_LIVE):
111
+ if (mem_time < -2147483647) or (mem_time > 2147483647):
112
+ _ = dialog.MessageDialog(
113
+ cfg.programName,
114
+ (f"The timestamp must be between -2147483647 and 2147483647.<br>The current timestamp is {mem_time}"),
115
+ (cfg.OK,),
116
+ )
117
+ return 1
118
+
119
+ # remove key code from modifiers
120
+ subject = event.get(cfg.SUBJECT, self.currentSubject)
121
+ comment = event.get(cfg.COMMENT, "")
122
+
123
+ if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
124
+ image_idx = event.get(cfg.IMAGE_INDEX, "")
125
+ image_path = event.get(cfg.IMAGE_PATH, "")
126
+ # check if pictures dir is relative
127
+ if str(pl.Path(image_path).parent) not in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
128
+ image_path = str(pl.Path(image_path).relative_to(pl.Path(self.projectFileName).parent))
129
+
130
+ if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA):
131
+ frame_idx = event.get(cfg.FRAME_INDEX, cfg.NA)
132
+
133
+ # check if a same event is already in events list (time, subject, code)
134
+
135
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
136
+ # adding event
137
+ if (not editing_event) and self.checkSameEvent(
138
+ self.observationId,
139
+ mem_time,
140
+ subject,
141
+ event[cfg.BEHAVIOR_CODE],
142
+ ):
143
+ _ = dialog.MessageDialog(cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,))
144
+ return 1
145
+
146
+ # modifying event and time was changed
147
+ if editing_event and mem_time != self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][0]:
148
+ if self.checkSameEvent(
149
+ self.observationId,
150
+ mem_time,
151
+ subject,
152
+ event[cfg.BEHAVIOR_CODE],
153
+ ):
154
+ _ = dialog.MessageDialog(
155
+ cfg.programName,
156
+ "The same event already exists (same time, behavior code and subject).",
157
+ [cfg.OK],
158
+ )
159
+ return 1
160
+
161
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
162
+ # adding event
163
+ if (not editing_event) and self.checkSameEvent(
164
+ self.observationId,
165
+ image_idx,
166
+ subject,
167
+ event[cfg.BEHAVIOR_CODE],
168
+ ):
169
+ _ = dialog.MessageDialog(
170
+ cfg.programName,
171
+ "The same event already exists (same image index, behavior code and subject).",
172
+ [cfg.OK],
173
+ )
174
+ return 1
175
+
176
+ # modifying event and time was changed
177
+ if (
178
+ editing_event
179
+ and image_idx
180
+ != self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]]
181
+ ):
182
+ if self.checkSameEvent(
183
+ self.observationId,
184
+ image_idx,
185
+ subject,
186
+ event[cfg.BEHAVIOR_CODE],
187
+ ):
188
+ _ = dialog.MessageDialog(
189
+ cfg.programName,
190
+ "The same event already exists (same image index, behavior code and subject).",
191
+ (cfg.OK,),
192
+ )
193
+ return 1
194
+
195
+ # extract State events
196
+ state_behaviors_codes = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
197
+
198
+ # index of current subject
199
+ # subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
200
+
201
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
202
+ position = mem_time
203
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
204
+ position = dec(image_idx) # decimal to pass to util.get_current_states_modifiers_by_subject
205
+
206
+ current_states: dict = util.get_current_states_modifiers_by_subject(
207
+ state_behaviors_codes,
208
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
209
+ dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
210
+ position,
211
+ include_modifiers=False,
212
+ )
213
+
214
+ # check if ask modifiers at stop is enabled
215
+ flag_ask_at_stop = False
216
+ if event["type"] == cfg.STATE_EVENT:
217
+ for idx in event[cfg.MODIFIERS]:
218
+ if event[cfg.MODIFIERS][idx].get("ask at stop", False):
219
+ flag_ask_at_stop = True
220
+ break
221
+
222
+ flag_ask_modifier = False
223
+ if flag_ask_at_stop:
224
+ # TODO: check if new event is a STOP one
225
+
226
+ idx_subject: str = ""
227
+ for idx in current_states:
228
+ if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
229
+ idx_subject = idx
230
+ break
231
+
232
+ if event[cfg.BEHAVIOR_CODE] in current_states[idx_subject]:
233
+ flag_ask_modifier = True
234
+ else:
235
+ flag_ask_modifier = True
236
+
237
+ if flag_ask_modifier:
238
+ if "from map" in event: # modifiers only for behaviors without coding map
239
+ modifier_str = event["from map"]
240
+ else:
241
+ # check if event has modifiers
242
+ modifier_str: str = ""
243
+
244
+ if event[cfg.MODIFIERS]:
245
+ selected_modifiers: dict = {}
246
+ modifiers_external_data: dict = {}
247
+ # check if modifiers are from external data
248
+ for idx in event[cfg.MODIFIERS]:
249
+ if event[cfg.MODIFIERS][idx]["type"] == cfg.EXTERNAL_DATA_MODIFIER:
250
+ if editing_event:
251
+ original_modifiers_list = event.get("original_modifiers", "").split("|")
252
+ modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
253
+ modifiers_external_data[idx]["selected"] = original_modifiers_list[int(idx)]
254
+
255
+ else: # no edit
256
+ for idx2 in self.plot_data:
257
+ if self.plot_data[idx2].y_label.upper() == event[cfg.MODIFIERS][idx]["name"].upper():
258
+ modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
259
+ modifiers_external_data[idx]["selected"] = self.plot_data[idx2].lb_value.text()
260
+
261
+ # check if modifiers are in single, multiple or numeric
262
+ if [x for x in event[cfg.MODIFIERS] if event[cfg.MODIFIERS][x]["type"] != cfg.EXTERNAL_DATA_MODIFIER]:
263
+ # pause media
264
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
265
+ if self.playerType == cfg.MEDIA:
266
+ if self.dw_player[0].player.pause:
267
+ memState = cfg.PAUSED
268
+ elif self.dw_player[0].player.time_pos is not None:
269
+ memState = cfg.PLAYING
270
+ else:
271
+ memState = cfg.STOPPED
272
+ if memState == cfg.PLAYING:
273
+ self.pause_video()
274
+
275
+ # check if editing (original_modifiers key)
276
+ currentModifiers = event.get("original_modifiers", "")
277
+
278
+ modifiers_selector = select_modifiers.ModifiersList(
279
+ event[cfg.BEHAVIOR_CODE], eval(str(event[cfg.MODIFIERS])), currentModifiers
280
+ )
281
+
282
+ r = modifiers_selector.exec_()
283
+ if r:
284
+ selected_modifiers = modifiers_selector.get_modifiers()
285
+
286
+ # restart media
287
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
288
+ if self.playerType == cfg.MEDIA:
289
+ if memState == cfg.PLAYING:
290
+ self.play_video()
291
+ if not r: # cancel button pressed
292
+ return
293
+
294
+ all_modifiers = {**selected_modifiers, **modifiers_external_data}
295
+
296
+ modifier_str: str = ""
297
+ for idx in util.sorted_keys(all_modifiers):
298
+ if modifier_str:
299
+ modifier_str += "|"
300
+ if all_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
301
+ modifier_str += ",".join(all_modifiers[idx].get("selected", ""))
302
+ if all_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
303
+ modifier_str += all_modifiers[idx].get("selected", "NA")
304
+
305
+ modifier_str = re.sub(r" \(.*\)", "", modifier_str)
306
+ else: # do not ask modifier
307
+ modifier_str = ""
308
+
309
+ # update current state
310
+ # TODO: verify event["subject"] / self.currentSubject
311
+
312
+ # print(f"{current_states=}")
313
+
314
+ # logging.debug(f"self.currentSubject {self.currentSubject}")
315
+ # logging.debug(f"current_states {current_states}")
316
+
317
+ # fill the undo list
318
+ event_operations.fill_events_undo_list(self, "Undo last event edition" if editing_event else "Undo last event insertion")
319
+
320
+ logging.debug("save list of events for undo operation")
321
+
322
+ if not editing_event:
323
+ if self.currentSubject:
324
+ csj: list = [] # list of current state for the current subject
325
+ for idx in current_states:
326
+ if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
327
+ csj = current_states[idx]
328
+ break
329
+
330
+ else: # no focal subject
331
+ try:
332
+ csj = current_states[""]
333
+ except Exception:
334
+ csj = []
335
+
336
+ logging.debug(f"csj {csj}")
337
+
338
+ # print(f"{csj=}")
339
+
340
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
341
+ check_index = cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
342
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
343
+ check_index = cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]
344
+
345
+ cm: dict = {} # modifiers for current behaviors
346
+ for cs in csj:
347
+ for ev in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
348
+ if ev[check_index] > position:
349
+ break
350
+
351
+ if ev[cfg.EVENT_SUBJECT_FIELD_IDX] == self.currentSubject:
352
+ if ev[cfg.EVENT_BEHAVIOR_FIELD_IDX] == cs:
353
+ cm[cs] = ev[cfg.EVENT_MODIFIER_FIELD_IDX]
354
+
355
+ if flag_ask_at_stop:
356
+ # set modifier to START behavior
357
+ if event[cfg.BEHAVIOR_CODE] in csj:
358
+ mem_idx = -1
359
+ for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
360
+ # print(f"{e=}")
361
+ if e[0] >= mem_time:
362
+ break
363
+ # same behavior, same subject and modifier(s) not set
364
+ if e[2] == event[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
365
+ mem_idx = idx
366
+ if mem_idx != -1:
367
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = modifier_str
368
+ csj.remove(event[cfg.BEHAVIOR_CODE])
369
+
370
+ for cs in csj:
371
+ # close state if same state without modifier
372
+ if (
373
+ self.close_the_same_current_event
374
+ and (event[cfg.BEHAVIOR_CODE] == cs)
375
+ and modifier_str.replace("None", "").replace("|", "") == ""
376
+ ):
377
+ modifier_str = cm[cs]
378
+ continue
379
+
380
+ if (event[cfg.EXCLUDED] and cs in event[cfg.EXCLUDED].split(",")) or (
381
+ event[cfg.BEHAVIOR_CODE] == cs and cm[cs] != modifier_str
382
+ ):
383
+ # check if behavior to stop is a 'ask modifier at stop'
384
+ behavior_to_stop = [
385
+ self.pj[cfg.ETHOGRAM][x]
386
+ for x in self.pj[cfg.ETHOGRAM]
387
+ if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == cs
388
+ and self.pj[cfg.ETHOGRAM][x]["type"] in (cfg.STATE_EVENT, cfg.STATE_EVENT_WITH_CODING_MAP)
389
+ ]
390
+ if behavior_to_stop:
391
+ behavior_to_stop = behavior_to_stop[0]
392
+
393
+ flag_behavior_ask_at_stop = False
394
+ for idx in behavior_to_stop[cfg.MODIFIERS]:
395
+ if behavior_to_stop[cfg.MODIFIERS][idx].get("ask at stop", False):
396
+ flag_behavior_ask_at_stop = True
397
+ break
398
+ if flag_behavior_ask_at_stop:
399
+ modifiers_selector = select_modifiers.ModifiersList(
400
+ behavior_to_stop[cfg.BEHAVIOR_CODE],
401
+ eval(str(behavior_to_stop[cfg.MODIFIERS])),
402
+ currentModifier="",
403
+ )
404
+
405
+ r = modifiers_selector.exec_()
406
+ if r:
407
+ selected_modifiers = modifiers_selector.get_modifiers()
408
+
409
+ behavior_to_stop_modifier_str: str = ""
410
+ for idx in util.sorted_keys(selected_modifiers):
411
+ if behavior_to_stop_modifier_str:
412
+ behavior_to_stop_modifier_str += "|"
413
+ if selected_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
414
+ behavior_to_stop_modifier_str += ",".join(selected_modifiers[idx].get("selected", ""))
415
+ if selected_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
416
+ behavior_to_stop_modifier_str += selected_modifiers[idx].get("selected", "NA")
417
+
418
+ # set the start modifier
419
+ mem_idx = -1
420
+ for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
421
+ if e[0] >= mem_time - dec("0.001"):
422
+ break
423
+ # same behavior, same subject and modifier(s) not set
424
+ if e[2] == behavior_to_stop[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
425
+ mem_idx = idx
426
+ if mem_idx != -1:
427
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = behavior_to_stop_modifier_str
428
+
429
+ else:
430
+ behavior_to_stop_modifier_str = cm[cs]
431
+
432
+ # add excluded state event to observations (= STOP them)
433
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE):
434
+ bisect.insort(
435
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
436
+ [mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, ""],
437
+ )
438
+
439
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
440
+ bisect.insort(
441
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
442
+ [mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, "", cfg.NA],
443
+ )
444
+
445
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
446
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
447
+ [mem_time, self.currentSubject, cs, behavior_to_stop_modifier_str, "", image_idx, image_path]
448
+ )
449
+
450
+ # order by image index ASC
451
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
452
+ key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
453
+ )
454
+
455
+ # add event to pj
456
+ if editing_event: # modifying event
457
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
458
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
459
+ mem_time,
460
+ subject,
461
+ event[cfg.BEHAVIOR_CODE],
462
+ modifier_str,
463
+ comment,
464
+ frame_idx,
465
+ ]
466
+ # order events list using time, subject, behavior
467
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
468
+
469
+ elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
470
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
471
+ mem_time,
472
+ subject,
473
+ event[cfg.BEHAVIOR_CODE],
474
+ modifier_str,
475
+ comment,
476
+ ]
477
+ # order events list using time, subject, behavior
478
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
479
+
480
+ elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
481
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
482
+ mem_time,
483
+ subject,
484
+ event[cfg.BEHAVIOR_CODE],
485
+ modifier_str,
486
+ comment,
487
+ image_idx,
488
+ image_path,
489
+ ]
490
+ # order by image index ASC
491
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
492
+ key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
493
+ )
494
+
495
+ else: # add event
496
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
497
+ bisect.insort(
498
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
499
+ [mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment, frame_idx],
500
+ )
501
+ elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
502
+ bisect.insort(
503
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
504
+ [mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment],
505
+ )
506
+
507
+ elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
508
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
509
+ [mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment, image_idx, image_path]
510
+ )
511
+ # order by image index ASC
512
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
513
+ key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
514
+ )
515
+
516
+ # reload all events in tw
517
+ self.load_tw_events(self.observationId)
518
+
519
+ self.project_changed()
520
+
521
+ self.get_events_current_row()
522
+
523
+ # index of current subject selected by observer
524
+ subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
525
+
526
+ self.currentStates = util.get_current_states_modifiers_by_subject(
527
+ self.state_behaviors_codes,
528
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
529
+ dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
530
+ self.getLaps(),
531
+ include_modifiers=True,
532
+ )
533
+
534
+ self.lbCurrentStates.setText(f"Observed behaviors: {', '.join(self.currentStates[subject_idx])}")
535
+ # show current states in subjects table
536
+ self.show_current_states_in_subjects_table()
537
+
538
+ return 0
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: boris-behav-obs
3
+ Version: 9.7.6
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/markdown
20
+ License-File: LICENSE.TXT
21
+ Requires-Dist: exifread==3.5.1
22
+ Requires-Dist: numpy==2.3.2
23
+ Requires-Dist: matplotlib==3.10.5
24
+ Requires-Dist: pandas==2.3.2
25
+ Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]==3.8.0
26
+ Requires-Dist: pyreadr==0.5.3
27
+ Requires-Dist: pyside6==6.10
28
+ Requires-Dist: hachoir==3.3.0
29
+ Requires-Dist: scipy==1.16.1
30
+ Requires-Dist: scikit-learn==1.7.1
31
+ Provides-Extra: dev
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Provides-Extra: r
36
+ Requires-Dist: rpy2>=3.6.1; extra == "r"
37
+ Dynamic: license-file
38
+
39
+ BORIS (Behavioral Observation Research Interactive Software)
40
+ ===============================================================
41
+
42
+
43
+ ![BORIS logo](https://github.com/olivierfriard/BORIS/blob/master/boris/icons/logo_boris.png?raw=true)
44
+
45
+ BORIS is an easy-to-use event logging software for video/audio coding or live observations.
46
+
47
+ BORIS is a free and open-source software available for GNU/Linux, Windows and macOS.
48
+
49
+ It provides also some analysis tools like time budget and some plotting functions.
50
+
51
+ <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
52
+
53
+
54
+ The BORIS paper has more than 2423 citations in peer-reviewed scientific publications.
55
+
56
+
57
+
58
+
59
+ See the official [BORIS web site](https://www.boris.unito.it).
60
+
61
+ <a href="https://www.boris.unito.it" target="_blank"><img alt="Website" src="https://img.shields.io/website?url=https%3A%2F%2Fwww.boris.unito.it"></a>
62
+ <a href="https://www.boris.unito.it/user_guide/" target="_blank"><img alt="User guide" src="https://img.shields.io/badge/Documentation-orange"></a>
63
+ [![Python web site](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org)
64
+ ![Python versions](https://img.shields.io/pypi/pyversions/boris-behav-obs)
65
+ ![BORIS license](https://img.shields.io/pypi/l/boris-behav-obs)
66
+ [![PyPI version](https://img.shields.io/pypi/v/boris-behav-obs.svg)](https://pypi.org/project/boris-behav-obs/)
67
+
68
+ [![Number of downloads](https://static.pepy.tech/personalized-badge/boris-behav-obs?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/boris-behav-obs)
69
+ ![commit-activity](https://img.shields.io/github/commit-activity/m/olivierfriard/BORIS)
70
+ ![GitHub last commit](https://img.shields.io/github/last-commit/olivierfriard/BORIS)
71
+
72
+ ![BORIS scopus citations badge](https://penelope.unito.it/friard/boris_scopus_citations.svg)
73
+
74
+
75
+ ![GitHub Repo stars](https://img.shields.io/github/stars/olivierfriard/BORIS?style=flat&label=Stars)
76
+ [![Please Star](https://img.shields.io/badge/⭐-Star%20this%20repo-blue?style=flat-square)](https://github.com/olivierfriard/BORIS/stargazers)
77
+
78
+ # Documentation
79
+
80
+
81
+
82
+ The [user guide](https://www.boris.unito.it/user_guide/) provides a good starting point for learning how to use BORIS.
83
+
84
+ Some [video tutorials](https://www.boris.unito.it/video_tutorials/) are available.
85
+
86
+
87
+
88
+
89
+
90
+ # Bug reports and feature requests
91
+
92
+
93
+ To search for bugs, report them or request a feature, please use the [GitHub issues tracker](https://github.com/olivierfriard/BORIS/issues)
94
+
95
+
96
+
97
+
98
+
99
+ # Citing BORIS
100
+
101
+
102
+ Please acknowledge and cite the use of this software and its authors when
103
+ results are used in publications or published elsewhere. You can use the
104
+ following BibTex entry
105
+
106
+ ```
107
+ @article {MEE3:MEE312584,
108
+ author = {Friard, Olivier and Gamba, Marco},
109
+ title = {BORIS: a free, versatile open-source event-logging software for video/audio coding and live observations},
110
+ journal = {Methods in Ecology and Evolution},
111
+ issn = {2041-210X},
112
+ url = {http://dx.doi.org/10.1111/2041-210X.12584},
113
+ doi = {10.1111/2041-210X.12584},
114
+ pages = {1324--1330},
115
+ year = {2016},
116
+ }
117
+ ```
118
+
119
+ You can also send us a nice postcard! See the [user testimonials](https://www.boris.unito.it/postcards).
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+ # Licence
129
+
130
+
131
+ This program is distributed in the hope that it will be useful,
132
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
133
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
134
+ GNU General Public License for more details.
135
+
136
+
137
+ Distributed with a [GPL v.3 license](LICENSE.TXT).
138
+
139
+ Copyright (C) 2012-2025 Olivier Friard