celldetective 1.5.0b2__py3-none-any.whl → 1.5.0b4__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.
celldetective/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.5.0b2"
1
+ __version__ = "1.5.0b4"
@@ -325,14 +325,18 @@ class AppInitWindow(CelldetectiveMainWindow):
325
325
  def reload_previous_experiments(self):
326
326
 
327
327
  self.recent_file_acts = []
328
- if os.path.exists(os.sep.join([self.soft_path, "celldetective", "recent.txt"])):
329
- recent_exps = open(
330
- os.sep.join([self.soft_path, "celldetective", "recent.txt"]), "r"
331
- )
332
- recent_exps = recent_exps.readlines()
333
- recent_exps = [r.strip() for r in recent_exps]
328
+ recent_path = os.sep.join([self.soft_path, "celldetective", "recent.txt"])
329
+ if os.path.exists(recent_path):
330
+ with open(recent_path, "r") as f:
331
+ recent_exps = [r.strip() for r in f.readlines()]
334
332
  recent_exps.reverse()
335
- recent_exps = list(dict.fromkeys(recent_exps))
333
+ recent_exps = list(dict.fromkeys(recent_exps))[:10]
334
+
335
+ # Auto-clean the file as well
336
+ with open(recent_path, "w") as f:
337
+ for r in reversed(recent_exps):
338
+ f.write(r + "\n")
339
+
336
340
  self.recent_file_acts = [QAction(r, self) for r in recent_exps]
337
341
  for r in self.recent_file_acts:
338
342
  r.triggered.connect(
@@ -525,10 +529,23 @@ class AppInitWindow(CelldetectiveMainWindow):
525
529
  logger.info(f"Number of positions per well:")
526
530
  pretty_table(number_pos)
527
531
 
528
- with open(
529
- os.sep.join([self.soft_path, "celldetective", "recent.txt"]), "a+"
530
- ) as f:
531
- f.write(self.exp_dir + "\n")
532
+ recent_path = os.sep.join(
533
+ [self.soft_path, "celldetective", "recent.txt"]
534
+ )
535
+ recent_exps = []
536
+ if os.path.exists(recent_path):
537
+ with open(recent_path, "r") as f:
538
+ recent_exps = [r.strip() for r in f.readlines()]
539
+
540
+ recent_exps.append(self.exp_dir)
541
+ # Deduplicate (keep latest)
542
+ recent_exps = list(dict.fromkeys(reversed(recent_exps)))
543
+ recent_exps.reverse() # Back to original order (latest at end)
544
+ recent_exps = recent_exps[-10:] # Keep only last 10
545
+
546
+ with open(recent_path, "w") as f:
547
+ for r in recent_exps:
548
+ f.write(r + "\n")
532
549
 
533
550
  threading.Thread(
534
551
  target=log_position_stats, args=(wells,), daemon=True
@@ -120,6 +120,8 @@ class QCheckableComboBox(QComboBox):
120
120
  actions = self.toolMenu.actions()
121
121
 
122
122
  item = self.model().itemFromIndex(index)
123
+ if item is None:
124
+ return
123
125
  if item.checkState() == Qt.Checked:
124
126
  item.setCheckState(Qt.Unchecked)
125
127
  actions[idx].setChecked(False)
@@ -305,7 +305,11 @@ class BaseAnnotator(CelldetectiveMainWindow, Styles):
305
305
  ]
306
306
  # self.log_btns = [QPushButton() for i in range(self.n_signals)]
307
307
 
308
- signals = list(self.df_tracks.columns)
308
+ signals = [
309
+ c
310
+ for c in self.df_tracks.columns
311
+ if pd.api.types.is_numeric_dtype(self.df_tracks[c])
312
+ ]
309
313
 
310
314
  to_remove = [
311
315
  "FRAME",
@@ -357,7 +361,10 @@ class BaseAnnotator(CelldetectiveMainWindow, Styles):
357
361
 
358
362
  for i in range(len(self.signal_choice_cb)):
359
363
  self.signal_choice_cb[i].addItems(["--"] + signals)
360
- self.signal_choice_cb[i].setCurrentIndex(i + 1)
364
+ if i + 1 < self.signal_choice_cb[i].count():
365
+ self.signal_choice_cb[i].setCurrentIndex(i + 1)
366
+ else:
367
+ self.signal_choice_cb[i].setCurrentIndex(0)
361
368
  self.signal_choice_cb[i].currentIndexChanged.connect(self.plot_signals)
362
369
 
363
370
  def on_scatter_pick(self, event):
@@ -58,14 +58,15 @@ logger = logging.getLogger(__name__)
58
58
 
59
59
 
60
60
  class BackgroundLoader(QThread):
61
- def run(self):
62
- logger.info("Loading background packages...")
63
- try:
64
- from celldetective.gui.viewers.base_viewer import StackVisualizer
65
- self.StackVisualizer = StackVisualizer
66
- except Exception:
67
- logger.error("Background packages not loaded...")
68
- logger.info("Background packages loaded...")
61
+ def run(self):
62
+ logger.info("Loading background packages...")
63
+ try:
64
+ from celldetective.gui.viewers.base_viewer import StackVisualizer
65
+
66
+ self.StackVisualizer = StackVisualizer
67
+ except Exception:
68
+ logger.error("Background packages not loaded...")
69
+ logger.info("Background packages loaded...")
69
70
 
70
71
 
71
72
  class ControlPanel(CelldetectiveMainWindow):
@@ -179,9 +180,11 @@ class ControlPanel(CelldetectiveMainWindow):
179
180
 
180
181
  name = self.exp_dir.split(os.sep)[-2]
181
182
  experiment_label = QLabel(f"Experiment:")
182
- experiment_label.setStyleSheet("""
183
+ experiment_label.setStyleSheet(
184
+ """
183
185
  font-weight: bold;
184
- """)
186
+ """
187
+ )
185
188
 
186
189
  self.folder_exp_btn = QPushButton()
187
190
  self.folder_exp_btn.setIcon(icon(MDI6.folder, color="black"))
@@ -553,16 +556,18 @@ class ControlPanel(CelldetectiveMainWindow):
553
556
  for p in self.ProcessPopulations:
554
557
  p.check_seg_btn.setEnabled(False)
555
558
  p.check_tracking_result_btn.setEnabled(False)
556
- p.view_tab_btn.setEnabled(True)
557
- p.signal_analysis_action.setEnabled(True)
559
+ p.view_tab_btn.setEnabled(self.position_list.isAnySelected())
560
+ p.signal_analysis_action.setEnabled(self.position_list.isAnySelected())
558
561
  p.check_seg_btn.setEnabled(False)
559
562
  p.check_tracking_result_btn.setEnabled(False)
560
- p.check_measurements_btn.setEnabled(False)
561
- p.check_signals_btn.setEnabled(False)
563
+ p.check_measurements_btn.setEnabled(self.position_list.isAnySelected())
564
+ p.check_signals_btn.setEnabled(self.position_list.isAnySelected())
562
565
  p.delete_tracks_btn.hide()
563
566
 
564
- self.NeighPanel.view_tab_btn.setEnabled(True)
565
- self.NeighPanel.check_signals_btn.setEnabled(False)
567
+ self.NeighPanel.view_tab_btn.setEnabled(self.position_list.isAnySelected())
568
+ self.NeighPanel.check_signals_btn.setEnabled(
569
+ self.position_list.isAnySelected()
570
+ )
566
571
  self.view_stack_btn.setEnabled(False)
567
572
 
568
573
  elif self.well_list.isMultipleSelection():
@@ -822,10 +822,15 @@ def color_from_state(state, recently_modified=False):
822
822
  color_map = {}
823
823
  for value in unique_values:
824
824
 
825
- if np.isnan(value):
826
- value = "nan"
827
- color_map[value] = "k"
828
- elif value == 0:
825
+ try:
826
+ if np.isnan(value):
827
+ value = "nan"
828
+ color_map[value] = "k"
829
+ continue
830
+ except TypeError:
831
+ pass
832
+
833
+ if value == 0:
829
834
  color_map[value] = "tab:blue"
830
835
  elif value == 1:
831
836
  color_map[value] = "tab:red"
@@ -834,7 +839,11 @@ def color_from_state(state, recently_modified=False):
834
839
  else:
835
840
  import matplotlib.pyplot as plt
836
841
 
837
- color_map[value] = plt.cm.tab20(value / 20.0)
842
+ if isinstance(value, (int, float, np.number)):
843
+ idx = value
844
+ else:
845
+ idx = hash(str(value)) % 20
846
+ color_map[value] = plt.cm.tab20(idx / 20.0)
838
847
 
839
848
  return color_map
840
849
 
@@ -101,6 +101,7 @@ class MeasureAnnotator(BaseAnnotator):
101
101
 
102
102
  def __init__(self, *args, **kwargs):
103
103
 
104
+ self.status_name = "group"
104
105
  super().__init__(read_config=False, *args, **kwargs)
105
106
 
106
107
  self.setWindowTitle("Static annotator")
@@ -115,17 +116,21 @@ class MeasureAnnotator(BaseAnnotator):
115
116
 
116
117
  self.current_frame = 0
117
118
  self.show_fliers = False
118
- self.status_name = "group"
119
119
 
120
120
  if self.proceed:
121
121
 
122
122
  from celldetective.utils.image_loaders import fix_missing_labels
123
+ from celldetective.tracking import write_first_detection_class
123
124
 
124
125
  # Ensure labels match stack length
125
126
  if self.len_movie > 0:
126
127
  temp_labels = locate_labels(self.pos, population=self.mode)
127
128
  if temp_labels is None or len(temp_labels) < self.len_movie:
128
- fix_missing_labels(self.pos, population=self.mode)
129
+ fix_missing_labels(
130
+ self.pos,
131
+ population=self.mode,
132
+ prefix=self.parent_window.movie_prefix,
133
+ )
129
134
  self.labels = locate_labels(self.pos, population=self.mode)
130
135
  elif len(temp_labels) > self.len_movie:
131
136
  self.labels = temp_labels[: self.len_movie]
@@ -174,13 +179,62 @@ class MeasureAnnotator(BaseAnnotator):
174
179
  cols = np.array(self.df_tracks.columns)
175
180
  self.class_cols = np.array(
176
181
  [
177
- c.startswith("group") or c.startswith("class")
182
+ c.startswith("group")
183
+ or c.startswith("class")
184
+ or c.startswith("status")
178
185
  for c in list(self.df_tracks.columns)
179
186
  ]
180
187
  )
181
188
  self.class_cols = list(cols[self.class_cols])
182
189
 
183
- to_remove = ["class_id", "group_color", "class_color"]
190
+ to_remove = [
191
+ "class_id",
192
+ "group_color",
193
+ "class_color",
194
+ "group_id",
195
+ "status_color",
196
+ "status_id",
197
+ ]
198
+ for col in to_remove:
199
+ try:
200
+ self.class_cols.remove(col)
201
+ except:
202
+ pass
203
+
204
+ # Generate missing status columns from class columns
205
+ for c in self.class_cols:
206
+ if c.startswith("class_"):
207
+ status_col = c.replace("class_", "status_")
208
+ if status_col not in self.df_tracks.columns:
209
+ if (
210
+ status_col == "status_firstdetection"
211
+ or c == "class_firstdetection"
212
+ ):
213
+ try:
214
+ from celldetective.tracking import (
215
+ write_first_detection_class,
216
+ )
217
+
218
+ self.df_tracks = write_first_detection_class(
219
+ self.df_tracks
220
+ )
221
+ except Exception as e:
222
+ logger.error(
223
+ f"Could not generate status_firstdetection: {e}"
224
+ )
225
+ self.df_tracks[status_col] = self.df_tracks[c]
226
+ else:
227
+ self.df_tracks[status_col] = self.df_tracks[c]
228
+
229
+ # Re-evaluate class_cols after generation
230
+ cols = np.array(self.df_tracks.columns)
231
+ self.class_cols = np.array(
232
+ [
233
+ c.startswith("group") or c.startswith("status")
234
+ for c in list(self.df_tracks.columns)
235
+ ]
236
+ )
237
+ self.class_cols = list(cols[self.class_cols])
184
238
  for col in to_remove:
185
239
  try:
186
240
  self.class_cols.remove(col)
@@ -188,7 +242,8 @@ class MeasureAnnotator(BaseAnnotator):
188
242
  pass
189
243
 
190
244
  if len(self.class_cols) > 0:
191
- self.status_name = self.class_cols[0]
245
+ if self.status_name not in self.class_cols:
246
+ self.status_name = self.class_cols[0]
192
247
  else:
193
248
  self.status_name = "group"
194
249
 
@@ -335,7 +390,13 @@ class MeasureAnnotator(BaseAnnotator):
335
390
  cols = np.array(self.df_tracks.columns)
336
391
  self.class_cols = np.array(
337
392
  [
338
- c.startswith("group") or c.startswith("status")
393
+ c.startswith("group")
394
+ or c.startswith("status")
395
+ or (
396
+ c.startswith("class")
397
+ and not c.endswith("_id")
398
+ and not c.endswith("_color")
399
+ )
339
400
  for c in list(self.df_tracks.columns)
340
401
  ]
341
402
  )
@@ -347,14 +408,23 @@ class MeasureAnnotator(BaseAnnotator):
347
408
  "class_id",
348
409
  "class_color",
349
410
  "status_color",
411
+ "status_id",
350
412
  ]
351
413
  for col in to_remove:
352
- try:
414
+ while col in self.class_cols:
353
415
  self.class_cols.remove(col)
354
- except Exception:
355
- pass
416
+
417
+ # Filter to keep only group_* and status_* as requested by user, but allow 'group' if it exists
418
+ final_cols = []
419
+ for c in self.class_cols:
420
+ if c == "group" or c.startswith("group_") or c.startswith("status_"):
421
+ final_cols.append(c)
422
+
423
+ self.class_cols = final_cols
356
424
 
357
425
  self.class_choice_cb.addItems(self.class_cols)
426
+ if self.status_name in self.class_cols:
427
+ self.class_choice_cb.setCurrentText(self.status_name)
358
428
  self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
359
429
 
360
430
  def populate_window(self):
@@ -678,13 +748,30 @@ class MeasureAnnotator(BaseAnnotator):
678
748
 
679
749
  try:
680
750
  cell_selected = f"cell: {self.track_of_interest}\n"
681
- if "TRACK_ID" in self.df_tracks.columns:
682
- cell_status = f"phenotype: {self.df_tracks.loc[(self.df_tracks['FRAME']==self.current_frame)&(self.df_tracks['TRACK_ID'] == self.track_of_interest), self.status_name].to_numpy()[0]}\n"
751
+ if self.status_name in self.df_tracks.columns:
752
+ if "TRACK_ID" in self.df_tracks.columns:
753
+ val = self.df_tracks.loc[
754
+ (self.df_tracks["FRAME"] == self.current_frame)
755
+ & (self.df_tracks["TRACK_ID"] == self.track_of_interest),
756
+ self.status_name,
757
+ ].to_numpy()
758
+ if len(val) > 0:
759
+ cell_status = f"phenotype: {val[0]}\n"
760
+ else:
761
+ cell_status = "phenotype: N/A\n"
762
+ else:
763
+ val = self.df_tracks.loc[
764
+ self.df_tracks["ID"] == self.track_of_interest, self.status_name
765
+ ].to_numpy()
766
+ if len(val) > 0:
767
+ cell_status = f"phenotype: {val[0]}\n"
768
+ else:
769
+ cell_status = "phenotype: N/A\n"
683
770
  else:
684
- cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
771
+ cell_status = f"phenotype: N/A (col '{self.status_name}' missing)\n"
685
772
  self.cell_info.setText(cell_selected + cell_status)
686
773
  except Exception as e:
687
- logger.error(e)
774
+ logger.error(f"Error in give_cell_information: {e}")
688
775
 
689
776
  def create_new_event_class(self):
690
777
 
@@ -772,8 +859,11 @@ class MeasureAnnotator(BaseAnnotator):
772
859
 
773
860
  def assign_color_state(self, state):
774
861
 
775
- if np.isnan(state):
776
- state = "nan"
862
+ try:
863
+ if np.isnan(state):
864
+ state = "nan"
865
+ except TypeError:
866
+ pass
777
867
  return self.state_color_map[state]
778
868
 
779
869
  def on_scatter_pick(self, event):
@@ -860,9 +950,9 @@ class MeasureAnnotator(BaseAnnotator):
860
950
  ].to_numpy()
861
951
  )
862
952
  self.colors.append(
863
- self.df_tracks.loc[
864
- self.df_tracks["FRAME"] == t, ["group_color"]
865
- ].to_numpy()
953
+ self.df_tracks.loc[self.df_tracks["FRAME"] == t, ["group_color"]]
954
+ .to_numpy()
955
+ .copy()
866
956
  )
867
957
  if "TRACK_ID" in self.df_tracks.columns:
868
958
  self.tracks.append(
@@ -945,6 +1035,12 @@ class MeasureAnnotator(BaseAnnotator):
945
1035
  self.changed_class()
946
1036
 
947
1037
  def modify(self):
1038
+ if self.status_name not in self.df_tracks.columns:
1039
+ logger.warning(
1040
+ f"Column '{self.status_name}' not found in df_tracks. Skipping modify."
1041
+ )
1042
+ return
1043
+
948
1044
  all_states = self.df_tracks.loc[:, self.status_name].tolist()
949
1045
  all_states = np.array(all_states)
950
1046
  self.state_color_map = color_from_state(all_states, recently_modified=False)
@@ -263,8 +263,7 @@ class ConfigMeasurementsPlot(CelldetectiveWidget):
263
263
  def ask_for_feature(self):
264
264
 
265
265
  cols = np.array(list(self.df.columns))
266
- is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
267
- feats = cols[is_number(self.df.dtypes)]
266
+ feats = [c for c in cols if pd.api.types.is_numeric_dtype(self.df[c])]
268
267
 
269
268
  self.feature_choice_widget = CelldetectiveWidget()
270
269
  self.feature_choice_widget.setWindowTitle("Select numeric feature")
@@ -286,8 +285,7 @@ class ConfigMeasurementsPlot(CelldetectiveWidget):
286
285
  def ask_for_features(self):
287
286
 
288
287
  cols = np.array(list(self.df.columns))
289
- is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
290
- feats = cols[is_number(self.df.dtypes)]
288
+ feats = [c for c in cols if pd.api.types.is_numeric_dtype(self.df[c])]
291
289
 
292
290
  self.feature_choice_widget = CelldetectiveWidget()
293
291
  self.feature_choice_widget.setWindowTitle("Select numeric feature")
@@ -21,6 +21,7 @@ from celldetective.utils.data_cleaning import extract_cols_from_table_list
21
21
  from celldetective.utils.parsing import _extract_labels_from_config
22
22
  from celldetective.utils.data_loaders import load_experiment_tables
23
23
  from celldetective.signals import mean_signal
24
+ import pandas as pd
24
25
  import numpy as np
25
26
  import os
26
27
  import matplotlib.pyplot as plt
@@ -377,8 +378,7 @@ class ConfigSignalPlot(CelldetectiveWidget):
377
378
  def ask_for_feature(self):
378
379
 
379
380
  cols = np.array(list(self.df.columns))
380
- is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
381
- feats = cols[is_number(self.df.dtypes)]
381
+ feats = [c for c in cols if pd.api.types.is_numeric_dtype(self.df[c])]
382
382
 
383
383
  self.feature_choice_widget = CelldetectiveWidget()
384
384
  self.feature_choice_widget.setWindowTitle("Select numeric feature")
@@ -400,8 +400,7 @@ class ConfigSignalPlot(CelldetectiveWidget):
400
400
  def ask_for_features(self):
401
401
 
402
402
  cols = np.array(list(self.df.columns))
403
- is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
404
- feats = cols[is_number(self.df.dtypes)]
403
+ feats = [c for c in cols if pd.api.types.is_numeric_dtype(self.df[c])]
405
404
 
406
405
  self.feature_choice_widget = CelldetectiveWidget()
407
406
  self.feature_choice_widget.setWindowTitle("Select numeric feature")