boris-behav-obs 9.3.3__py2.py3-none-any.whl → 9.3.5__py2.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.
boris/event_operations.py CHANGED
@@ -70,7 +70,7 @@ def add_event(self):
70
70
 
71
71
  editWindow = DlgEditEvent(
72
72
  observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
73
- time_value=0,
73
+ time_value=dec("NaN"),
74
74
  image_idx=0,
75
75
  current_time=current_time,
76
76
  time_format=self.timeFormat,
@@ -80,8 +80,6 @@ def add_event(self):
80
80
 
81
81
  sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
82
82
 
83
- print(f"{sortedSubjects=}")
84
-
85
83
  editWindow.cobSubject.addItems(sortedSubjects)
86
84
  if self.currentSubject:
87
85
  editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
@@ -173,8 +171,13 @@ def add_event(self):
173
171
  time_ = dec("NaN")
174
172
  if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
175
173
  if self.playerType != cfg.VIEWER_IMAGES:
176
- if self.extract_exif_DateTimeOriginal(self.images_list[new_index]) != -1:
177
- time_ = self.extract_exif_DateTimeOriginal(self.images_list[new_index]) - self.image_time_ref
174
+ exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[new_index])
175
+ if exif_date_time != -1:
176
+ time_ = exif_date_time
177
+
178
+ # check if first value must be substracted
179
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.SUBSTRACT_FIRST_EXIF_DATE, True):
180
+ time_ -= self.image_time_ref
178
181
 
179
182
  elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
180
183
  time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
@@ -591,6 +594,12 @@ def edit_event(self):
591
594
  else:
592
595
  current_value = self.getLaps()
593
596
 
597
+ # get exif date time
598
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
599
+ exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
600
+ else:
601
+ exif_date_time = None
602
+
594
603
  edit_window = DlgEditEvent(
595
604
  observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
596
605
  time_value=time_value,
@@ -598,12 +607,15 @@ def edit_event(self):
598
607
  current_time=current_value,
599
608
  time_format=self.timeFormat,
600
609
  show_set_current_time=True,
610
+ exif_date_time=exif_date_time,
601
611
  )
602
612
  edit_window.setWindowTitle("Edit event")
603
613
 
604
614
  # time
605
615
  if time_value.is_nan():
606
616
  edit_window.cb_set_time_na.setChecked(True)
617
+
618
+ # remove visibility of 'set current time' widget if VIEWER mode
607
619
  if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
608
620
  edit_window.pb_set_to_current_time.setVisible(False)
609
621
 
@@ -637,20 +649,16 @@ def edit_event(self):
637
649
  sortedCodes.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX])
638
650
  )
639
651
  else:
640
- logging.warning(
641
- (
642
- f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
643
- "does not exist longer in the ethogram"
644
- )
652
+ msg: str = (
653
+ f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
654
+ "does not exist longer in the ethogram"
645
655
  )
656
+ logging.warning(msg)
657
+
646
658
  QMessageBox.warning(
647
659
  self,
648
660
  cfg.programName,
649
- (
650
- "The behaviour "
651
- f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> "
652
- "does not exist more in the ethogram"
653
- ),
661
+ msg,
654
662
  )
655
663
  edit_window.cobCode.setCurrentIndex(0)
656
664
 
@@ -658,28 +666,32 @@ def edit_event(self):
658
666
  f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
659
667
  )
660
668
 
661
- # frame index
662
- frame_idx = read_event_field(
663
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
664
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
665
- cfg.FRAME_INDEX,
666
- )
667
- edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
668
- if frame_idx in (cfg.NA, None):
669
- edit_window.cb_set_frame_idx_na.setChecked(True)
669
+ # # frame index
670
+ # frame_idx = read_event_field(
671
+ # self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
672
+ # self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
673
+ # cfg.FRAME_INDEX,
674
+ # )
675
+ # edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
676
+ # if frame_idx in (cfg.NA, None):
677
+ # edit_window.cb_set_frame_idx_na.setChecked(True)
670
678
 
671
679
  # comment
672
680
  edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
673
681
 
674
682
  flag_ok = False # for looping until event is OK or Cancel pressed
675
683
  while True:
676
- if edit_window.exec_(): # button OK
684
+ if edit_window.exec(): # button OK
677
685
  self.project_changed()
678
686
 
687
+ new_time = edit_window.time_widget.get_time()
688
+
689
+ if edit_window.cb_set_time_na.isChecked():
690
+ new_time = dec("NaN")
691
+
679
692
  # MEDIA / LIVE
680
693
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
681
- new_time = edit_window.time_widget.get_time()
682
- if new_time is None:
694
+ if new_time.is_nan():
683
695
  QMessageBox.warning(
684
696
  self,
685
697
  cfg.programName,
@@ -707,19 +719,17 @@ def edit_event(self):
707
719
  event[cfg.FRAME_INDEX] = frame_idx
708
720
  self.seek_mediaplayer(mem_time)
709
721
 
710
- """
711
- if not edit_window.sb_frame_idx.value() or edit_window.cb_set_frame_idx_na.isChecked():
712
- event[cfg.FRAME_INDEX] = cfg.NA
713
- else:
714
- if self.playerType == cfg.MEDIA:
715
- mem_time = self.getLaps()
716
- if not self.seek_mediaplayer(new_time):
717
- frame_idx = self.get_frame_index()
718
- event[cfg.FRAME_INDEX] = frame_idx
719
- self.seek_mediaplayer(mem_time)
720
-
721
- # event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
722
- """
722
+ # if not edit_window.sb_frame_idx.value() or edit_window.cb_set_frame_idx_na.isChecked():
723
+ # event[cfg.FRAME_INDEX] = cfg.NA
724
+ # else:
725
+ # if self.playerType == cfg.MEDIA:
726
+ # mem_time = self.getLaps()
727
+ # if not self.seek_mediaplayer(new_time):
728
+ # frame_idx = self.get_frame_index()
729
+ # event[cfg.FRAME_INDEX] = frame_idx
730
+ # self.seek_mediaplayer(mem_time)
731
+ #
732
+ # # event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
723
733
 
724
734
  event["row"] = pj_event_idx
725
735
  event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
@@ -747,10 +757,7 @@ def edit_event(self):
747
757
  for key in self.pj[cfg.ETHOGRAM]:
748
758
  if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
749
759
  event = self.full_event(key)
750
- if edit_window.time_value == cfg.NA or (edit_window.cb_set_time_na.isChecked()):
751
- event[cfg.TIME] = dec("NaN")
752
- else:
753
- event[cfg.TIME] = edit_window.time_widget.get_time()
760
+ event[cfg.TIME] = new_time
754
761
 
755
762
  event[cfg.SUBJECT] = (
756
763
  "" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
@@ -808,7 +815,7 @@ def edit_time_selected_events(self):
808
815
  return
809
816
 
810
817
  d = w.time_widget.get_time()
811
- if d is None or not d:
818
+ if d.is_nan() or not d:
812
819
  return
813
820
 
814
821
  if ":" in util.smart_time_format(abs(d)):
@@ -882,8 +889,6 @@ def copy_selected_events(self):
882
889
  else:
883
890
  copied_events.append("\t".join([str(x) for x in event]))
884
891
 
885
- print(f"{copied_events=}")
886
-
887
892
  cb = QApplication.clipboard()
888
893
  cb.clear(mode=QClipboard.Mode.Clipboard)
889
894
  cb.setText("\n".join(copied_events), mode=QClipboard.Mode.Clipboard)
boris/observation.py CHANGED
@@ -24,10 +24,9 @@ import logging
24
24
  import os
25
25
  import pandas as pd
26
26
  import pathlib as pl
27
- import datetime as dt
28
27
 
29
28
  from PySide6.QtCore import Qt
30
- from PySide6.QtGui import QColor, QTextCursor, QAction
29
+ from PySide6.QtGui import QColor
31
30
  from PySide6.QtWidgets import (
32
31
  QDialog,
33
32
  QVBoxLayout,
@@ -1024,7 +1024,21 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
1024
1024
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DIRECTORIES_LIST] = [
1025
1025
  observationWindow.lw_images_directory.item(i).text() for i in range(observationWindow.lw_images_directory.count())
1026
1026
  ]
1027
+
1028
+ # check if exif data must be used
1027
1029
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE] = observationWindow.rb_use_exif.isChecked()
1030
+
1031
+ # ask if the value of the exif date time of the first picture must be substracted
1032
+ # TODO: improve this
1033
+ if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE]:
1034
+ response = dialog.MessageDialog(
1035
+ cfg.programName,
1036
+ "You choose to use the EXIF metadata. Do you want to substract the date time value of the first picture?",
1037
+ (cfg.YES, cfg.NO),
1038
+ )
1039
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SUBSTRACT_FIRST_EXIF_DATE] = response == cfg.YES
1040
+
1041
+ # check if time lapse
1028
1042
  if observationWindow.rb_time_lapse.isChecked():
1029
1043
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = observationWindow.sb_time_lapse.value()
1030
1044
  else:
@@ -1086,7 +1100,6 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
1086
1100
  return "Observation not launched"
1087
1101
 
1088
1102
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
1089
- # QMessageBox.critical(self, cfg.programName, "Observation from images directory is not yet implemented")
1090
1103
  initialize_new_images_observation(self)
1091
1104
 
1092
1105
  self.load_tw_events(self.observationId)
@@ -1100,7 +1113,7 @@ def close_observation(self):
1100
1113
 
1101
1114
  logging.info(f"Close observation (player type: {self.playerType})")
1102
1115
 
1103
- # check observation events
1116
+ # check observation state events
1104
1117
 
1105
1118
  flag_ok, msg = project_functions.check_state_events_obs(
1106
1119
  self.observationId,
@@ -1111,16 +1124,22 @@ def close_observation(self):
1111
1124
 
1112
1125
  if not flag_ok:
1113
1126
  out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
1114
- results = dialog.Results_dialog()
1127
+ results = dialog.Results_dialog_exit_code()
1115
1128
  results.setWindowTitle(f"{cfg.programName} - Check selected observations")
1116
1129
  results.ptText.setReadOnly(True)
1117
1130
  results.ptText.appendHtml(out)
1118
- results.pbSave.setVisible(False)
1119
- results.pbCancel.setText("Close observation")
1120
- results.pbCancel.setVisible(True)
1121
- results.pbOK.setText("Fix unpaired state events")
1122
1131
 
1123
- if results.exec_(): # fix events
1132
+ results.pb1.setText("Close observation")
1133
+ results.pb2.setText("Return to observation")
1134
+ if self.playerType == cfg.IMAGES:
1135
+ results.pb3.setVisible(False)
1136
+ else:
1137
+ results.pb3.setText("Fix unpaired state events")
1138
+
1139
+ r = results.exec()
1140
+ if r == 2: # Return to observation
1141
+ return
1142
+ if r == 3: # Fix unpaired state events
1124
1143
  state_events.fix_unpaired_events(self, silent_mode=True)
1125
1144
 
1126
1145
  self.saved_state = self.saveState()
@@ -1280,7 +1299,7 @@ def check_creation_date(self) -> Tuple[int, dict]:
1280
1299
  def init_mpv(self):
1281
1300
  """Start mpv process and embed it in the PySide6 application."""
1282
1301
 
1283
- print("start MPV process")
1302
+ logging.debug("function: init_mpv")
1284
1303
 
1285
1304
  """
1286
1305
  print(f"{self.winId()=}")
@@ -1307,7 +1326,7 @@ def init_mpv(self):
1307
1326
  def send_command(command):
1308
1327
  """Send a JSON command to the mpv IPC server."""
1309
1328
 
1310
- print(f"send commnand {command}")
1329
+ logging.debug("function: send commnand {command}")
1311
1330
  # print(f"{self.mpv_process=}")
1312
1331
 
1313
1332
  try:
@@ -2407,8 +2426,11 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2407
2426
 
2408
2427
  cumul_media_durations: list = [dec(0)]
2409
2428
  for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2410
- media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2411
- cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2429
+ try:
2430
+ media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2431
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2432
+ except KeyError:
2433
+ return None
2412
2434
 
2413
2435
  cumul_media_durations.remove(dec(0))
2414
2436
 
boris/project.py CHANGED
@@ -150,12 +150,14 @@ class BehavioralCategories(QDialog):
150
150
  sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
151
151
  )
152
152
 
153
- if behavioral_categories_in_ethogram.difference(set(behavioral_categories)):
153
+ if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
154
+ set(behavioral_categories)
155
+ ) != {""}:
154
156
  if (
155
157
  dialog.MessageDialog(
156
158
  cfg.programName,
157
159
  (
158
- "They are behavioral categories that are present in ethogram but not defined.<br>"
160
+ "There are behavioral categories that are present in ethogram but not defined.<br>"
159
161
  f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
160
162
  "<br>"
161
163
  "Do you want to add them in the behavioral categories list?"
@@ -210,7 +212,6 @@ class BehavioralCategories(QDialog):
210
212
  color = col_diag.currentColor()
211
213
  if color.name() == "#000000": # black -> delete color
212
214
  self.lw.item(row, 1).setText("")
213
- # self.lw.item(row, 1).setBackground(QColor(230, 230, 230))
214
215
  self.lw.item(row, 1).setBackground(self.not_editable_column_color())
215
216
  elif color.isValid():
216
217
  self.lw.item(row, 1).setText(color.name())
@@ -820,18 +821,18 @@ class projectDialog(QDialog, Ui_dlgProject):
820
821
  column (int): column double-clicked
821
822
  """
822
823
 
823
- # check if double click on excluded column
824
+ # excluded column
824
825
  if column == cfg.behavioursFields[cfg.EXCLUDED]:
825
826
  self.exclusion_matrix()
826
827
 
827
- # check if double click on 'coding map' column
828
+ # coding map
828
829
  if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
829
830
  if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
830
831
  self.behavior_type_changed(row)
831
832
  else:
832
833
  QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
833
834
 
834
- # check if double click on behavior type
835
+ # behavior type
835
836
  if column == cfg.behavioursFields["type"]:
836
837
  self.behavior_type_doubleclicked(row)
837
838
 
@@ -843,7 +844,7 @@ class projectDialog(QDialog, Ui_dlgProject):
843
844
  if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
844
845
  self.category_doubleclicked(row)
845
846
 
846
- # check if double click on modifiers
847
+ # modifiers
847
848
  if column == cfg.behavioursFields[cfg.MODIFIERS]:
848
849
  # check if behavior has coding map
849
850
  if (
@@ -897,16 +898,17 @@ class projectDialog(QDialog, Ui_dlgProject):
897
898
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
898
899
  current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
899
900
  if current_color.isValid():
901
+ print(f"{current_color=}")
900
902
  col_diag.setCurrentColor(current_color)
901
903
 
902
- if col_diag.exec_():
904
+ if col_diag.exec():
903
905
  color = col_diag.currentColor()
904
906
  if color.name() == "#000000": # black -> delete color
905
907
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
906
908
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
907
909
  elif color.isValid():
910
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
908
911
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
909
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(color)
910
912
 
911
913
  def category_doubleclicked(self, row):
912
914
  """
@@ -356,7 +356,7 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
356
356
  use check_state_events_obs function
357
357
  """
358
358
 
359
- logging.info("Check state events")
359
+ logging.info("Check state events function")
360
360
 
361
361
  out = ""
362
362
  not_paired_obs_list = []
@@ -404,7 +404,7 @@ def check_project_integrity(
404
404
  * check if media file are available (optional)
405
405
  * check if media length available
406
406
  * check independent variables
407
- * check if coded subjects are defines
407
+ * check if coded subjects are defined
408
408
 
409
409
  Args:
410
410
  pj (dict): BORIS project
@@ -474,6 +474,7 @@ def check_project_integrity(
474
474
  if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
475
475
  out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
476
476
 
477
+ """
477
478
  # check if media length available
478
479
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
479
480
  for nplayer in cfg.ALL_PLAYERS:
@@ -484,6 +485,7 @@ def check_project_integrity(
484
485
  except KeyError:
485
486
  out += "<br><br>" if out else ""
486
487
  out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
488
+ """
487
489
 
488
490
  out += "<br><br>" if out else ""
489
491
  out += out_events
@@ -530,25 +532,46 @@ def check_project_integrity(
530
532
  ]
531
533
  )
532
534
 
533
- out += "<br><br>" if out else ""
535
+ tmp_out: str = ""
534
536
  for obs_id in pj[cfg.OBSERVATIONS]:
535
537
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
536
538
  continue
537
539
  for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
538
540
  if var_label in defined_set_var_label:
539
541
  if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
540
- out += (
542
+ tmp_out += (
541
543
  f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
542
544
  f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
543
545
  )
546
+ if tmp_out:
547
+ out += "<br><br>" if out else ""
548
+ out += tmp_out
544
549
 
545
550
  # check if coded subjects are defined in the subjects list
551
+ tmp_out: str = ""
546
552
  subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
547
553
  coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
548
554
 
549
555
  for subject in coded_subjects:
550
556
  if subject and subject not in subjects_list:
551
- out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
557
+ tmp_out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
558
+ if tmp_out:
559
+ out += "<br><br>" if out else ""
560
+ out += tmp_out
561
+
562
+ # check if media file have info in media_info section of project
563
+ tmp_out: str = ""
564
+ for obs_id in pj[cfg.OBSERVATIONS]:
565
+ for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
566
+ for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
567
+ for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
568
+ if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]:
569
+ tmp_out += f"Observation <b>{obs_id}</b>:<br>"
570
+ tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
571
+ if tmp_out:
572
+ tmp_out += "<br>You should repick the media file to fix this issue."
573
+ out += "<br><br>" if out else ""
574
+ out += tmp_out
552
575
 
553
576
  return out
554
577
 
@@ -1168,13 +1191,8 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1168
1191
  """
1169
1192
  if not observation[cfg.EVENTS]:
1170
1193
  return (dec("0.0"), dec("0.0"))
1171
- if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1172
- """
1173
- print("=" * 120)
1174
- print(observation[cfg.EVENTS])
1175
- print("=" * 120)
1176
- """
1177
1194
 
1195
+ if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1178
1196
  event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
1179
1197
 
1180
1198
  return (
@@ -1183,8 +1201,12 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1183
1201
  )
1184
1202
  if observation[cfg.TYPE] == cfg.IMAGES:
1185
1203
  events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
1186
-
1187
- return (dec(min(events)), dec(max(events)))
1204
+ # test if indexes contain NA
1205
+ try:
1206
+ dec(min(events))
1207
+ return (dec(min(events)), dec(max(events)))
1208
+ except Exception:
1209
+ return (dec("NaN"), dec("NaN"))
1188
1210
 
1189
1211
 
1190
1212
  def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
@@ -1273,7 +1295,7 @@ def open_project_json(project_file_name: str) -> tuple:
1273
1295
  str: message
1274
1296
  """
1275
1297
 
1276
- logging.debug(f"open project: {project_file_name}")
1298
+ logging.debug(f"open_project_json function: {project_file_name}")
1277
1299
 
1278
1300
  projectChanged: bool = False
1279
1301
  msg: str = ""
@@ -1612,6 +1634,8 @@ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -
1612
1634
  list: list of events with state events fixed
1613
1635
  """
1614
1636
 
1637
+ logging.debug("fix_unpaired_state_events2 function")
1638
+
1615
1639
  closing_events_to_add: list = []
1616
1640
  subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
1617
1641
  ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
@@ -1960,6 +1984,6 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1960
1984
 
1961
1985
  pd.DataFrame(data).info()
1962
1986
 
1963
- print(pd.DataFrame(data))
1987
+ # print(pd.DataFrame(data))
1964
1988
 
1965
1989
  return pd.DataFrame(data)
@@ -21,7 +21,9 @@ Copyright 2012-2025 Olivier Friard
21
21
 
22
22
  """
23
23
 
24
+ import logging
24
25
  from typing import Tuple
26
+
25
27
  from PySide6.QtCore import Qt
26
28
  from PySide6.QtWidgets import QAbstractItemView
27
29
 
@@ -60,7 +62,10 @@ def select_observations2(self, mode: str, windows_title: str = "") -> Tuple[str,
60
62
  data: list = []
61
63
  not_paired: list = []
62
64
 
65
+ # check if observations changed
63
66
  if hash(str(self.pj[cfg.OBSERVATIONS])) != self.mem_hash_obs:
67
+ logging.debug("observations changed")
68
+
64
69
  for obs in sorted(list(pj[cfg.OBSERVATIONS].keys())):
65
70
  date = pj[cfg.OBSERVATIONS][obs]["date"].replace("T", " ")
66
71
  descr = util.eol2space(pj[cfg.OBSERVATIONS][obs][cfg.DESCRIPTION])
@@ -124,11 +129,12 @@ def select_observations2(self, mode: str, windows_title: str = "") -> Tuple[str,
124
129
  data, header=fields_list + indep_var_header, column_type=column_type, not_paired=not_paired
125
130
  )
126
131
  self.data = data
132
+ self.not_paired = not_paired
127
133
  self.mem_hash_obs = hash(str(self.pj[cfg.OBSERVATIONS]))
128
134
 
129
135
  else:
130
136
  obsList = observations_list.observationsList_widget(
131
- self.data, header=fields_list + indep_var_header, column_type=column_type, not_paired=not_paired
137
+ self.data, header=fields_list + indep_var_header, column_type=column_type, not_paired=self.not_paired
132
138
  )
133
139
 
134
140
  if windows_title:
boris/state_events.py CHANGED
@@ -109,7 +109,7 @@ def fix_unpaired_events(self, silent_mode: bool = False):
109
109
  return
110
110
 
111
111
  fix_at_time = w.time_widget.get_time()
112
- if fix_at_time is None:
112
+ if fix_at_time.is_nan():
113
113
  QMessageBox.warning(
114
114
  self,
115
115
  cfg.programName,
boris/utilities.py CHANGED
@@ -129,7 +129,7 @@ def extract_video_creation_date(file_path: str) -> int | None:
129
129
  return None
130
130
 
131
131
 
132
- def extract_date_time_from_file_name(file_path: str) -> int:
132
+ def extract_date_time_from_file_name(file_path: str) -> int | None:
133
133
  """
134
134
  extract YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HH:MM:SS from file name
135
135
  """
boris/version.py CHANGED
@@ -20,5 +20,5 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- __version__ = "9.3.3"
24
- __version_date__ = "2025-05-07"
23
+ __version__ = "9.3.5"
24
+ __version_date__ = "2025-05-12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.3.3
3
+ Version: 9.3.5
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License-Expression: GPL-3.0-only
@@ -24,7 +24,7 @@ Requires-Dist: matplotlib>=3.3.3
24
24
  Requires-Dist: pandas>=2.2.2
25
25
  Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
26
26
  Requires-Dist: pyreadr
27
- Requires-Dist: pyside6==6.8.0.2
27
+ Requires-Dist: pyside6==6.9
28
28
  Requires-Dist: hachoir>=3.3.0
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: ruff; extra == "dev"