celldetective 1.5.0b7__py3-none-any.whl → 1.5.0b9__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.
Files changed (33) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/event_detection_models.py +2463 -0
  3. celldetective/gui/base/channel_norm_generator.py +19 -3
  4. celldetective/gui/base/figure_canvas.py +1 -1
  5. celldetective/gui/base/list_widget.py +1 -1
  6. celldetective/gui/base_annotator.py +2 -5
  7. celldetective/gui/event_annotator.py +248 -138
  8. celldetective/gui/generic_signal_plot.py +14 -14
  9. celldetective/gui/gui_utils.py +27 -6
  10. celldetective/gui/pair_event_annotator.py +146 -20
  11. celldetective/gui/plot_signals_ui.py +32 -15
  12. celldetective/gui/process_block.py +2 -2
  13. celldetective/gui/seg_model_loader.py +4 -4
  14. celldetective/gui/settings/_settings_event_model_training.py +32 -14
  15. celldetective/gui/settings/_settings_segmentation_model_training.py +5 -5
  16. celldetective/gui/settings/_settings_signal_annotator.py +0 -19
  17. celldetective/gui/survival_ui.py +39 -11
  18. celldetective/gui/tableUI.py +69 -148
  19. celldetective/gui/thresholds_gui.py +45 -5
  20. celldetective/gui/viewers/base_viewer.py +17 -20
  21. celldetective/gui/viewers/spot_detection_viewer.py +136 -27
  22. celldetective/processes/train_signal_model.py +1 -1
  23. celldetective/scripts/train_signal_model.py +1 -1
  24. celldetective/signals.py +4 -2426
  25. celldetective/utils/event_detection/__init__.py +1 -1
  26. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/METADATA +1 -1
  27. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/RECORD +33 -31
  28. tests/gui/test_spot_detection_viewer.py +187 -0
  29. tests/test_signals.py +135 -116
  30. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/WHEEL +0 -0
  31. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/entry_points.txt +0 -0
  32. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/licenses/LICENSE +0 -0
  33. {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b9.dist-info}/top_level.txt +0 -0
@@ -417,8 +417,7 @@ class SettingsEventDetectionModelTraining(CelldetectiveSettingsPanel):
417
417
  self.signals = ["--"] + num_cols_pairs + num_cols_reference + num_cols_neighbor
418
418
 
419
419
  for cb in self.ch_norm.channel_cbs:
420
- cb.clear()
421
- cb.addItems(self.signals)
420
+ self.ch_norm.add_items_truncated(cb, self.signals)
422
421
 
423
422
  def fill_available_neighborhoods(self):
424
423
 
@@ -541,15 +540,36 @@ class SettingsEventDetectionModelTraining(CelldetectiveSettingsPanel):
541
540
  self.model_length_slider.setValue(int(signal_length))
542
541
  self.model_length_slider.setEnabled(False)
543
542
 
544
- for c, cb in zip(channels, self.ch_norm.channel_cbs):
545
- index = cb.findText(c)
543
+ try:
544
+ norm_perc = data["normalization_percentile"]
545
+ if isinstance(norm_perc, bool):
546
+ norm_perc = [norm_perc] * len(channels)
547
+ norm_val = data["normalization_values"]
548
+ if len(norm_val) == 2 and isinstance(norm_val[0], float):
549
+ norm_val = [norm_val] * len(channels)
550
+ norm_clip = data["normalization_clip"]
551
+ if isinstance(norm_clip, bool):
552
+ norm_clip = [norm_clip] * len(channels)
553
+ except Exception:
554
+ norm_perc = [True] * len(channels)
555
+ norm_val = [[0.1, 99.99]] * len(channels)
556
+ norm_clip = [False] * len(channels)
557
+
558
+ for k, (c, cb) in enumerate(zip(channels, self.ch_norm.channel_cbs)):
559
+ index = cb.findData(c)
546
560
  cb.setCurrentIndex(index)
547
561
 
548
- if len(channels) < len(self.ch_norm.channel_cbs):
549
- for k in range(len(self.ch_norm.channel_cbs) - len(channels)):
550
- self.ch_norm.channel_cbs[len(channels) + k].setCurrentIndex(0)
551
- self.ch_norm.channel_cbs[len(channels) + k].setEnabled(False)
552
- self.ch_norm.add_col_btn.setEnabled(False)
562
+ # Set normalization mode
563
+ if self.ch_norm.normalization_mode[k] != norm_perc[k]:
564
+ self.ch_norm.switch_normalization_mode(k)
565
+
566
+ # Set clipping mode
567
+ if self.ch_norm.clip_option[k] != norm_clip[k]:
568
+ self.ch_norm.switch_clipping_mode(k)
569
+
570
+ # Set normalization values
571
+ self.ch_norm.normalization_min_value_le[k].setText(str(norm_val[k][0]))
572
+ self.ch_norm.normalization_max_value_le[k].setText(str(norm_val[k][1]))
553
573
 
554
574
  def adjust_scroll_area(self):
555
575
  """
@@ -581,7 +601,7 @@ class SettingsEventDetectionModelTraining(CelldetectiveSettingsPanel):
581
601
 
582
602
  channels = []
583
603
  for i in range(len(self.ch_norm.channel_cbs)):
584
- channels.append(self.ch_norm.channel_cbs[i].currentText())
604
+ channels.append(self.ch_norm.channel_cbs[i].currentData())
585
605
 
586
606
  slots_to_keep = np.where(np.array(channels) != "--")[0]
587
607
  while "--" in channels:
@@ -750,10 +770,8 @@ class SettingsEventDetectionModelTraining(CelldetectiveSettingsPanel):
750
770
 
751
771
  # Assuming signal/event models directory. Verifying path would be better but this fits pattern.
752
772
  # If exact attribute unknown, use os.path.dirname logic or config methods.
753
- # Safe bet: self.parent_window.signal_models_dir based on refresh call.
754
- model_path = os.path.join(
755
- self.parent_window.signal_models_dir, self.modelname_le.text()
756
- )
773
+ # Safe bet: self.signal_models_dir based on init.
774
+ model_path = os.path.join(self.signal_models_dir, self.modelname_le.text())
757
775
  if os.path.exists(model_path):
758
776
  time.sleep(0.5)
759
777
  shutil.rmtree(model_path)
@@ -355,7 +355,7 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
355
355
  not current_name in models
356
356
  and not self.spatial_calib_le.text() == ""
357
357
  and not np.all(
358
- [cb.currentText() == "--" for cb in self.ch_norm.channel_cbs]
358
+ [cb.currentData() == "--" for cb in self.ch_norm.channel_cbs]
359
359
  )
360
360
  ):
361
361
  self.submit_btn.setEnabled(True)
@@ -370,7 +370,7 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
370
370
  self.submit_warning.setText(
371
371
  "Please provide a valid spatial calibration..."
372
372
  )
373
- elif np.all([cb.currentText() == "--" for cb in self.ch_norm.channel_cbs]):
373
+ elif np.all([cb.currentData() == "--" for cb in self.ch_norm.channel_cbs]):
374
374
  self.submit_warning.setText("Please provide valid channels...")
375
375
 
376
376
  def rescale_slider(self):
@@ -459,7 +459,7 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
459
459
 
460
460
  for k in range(len(self.diamWidget.cellpose_channel_cb)):
461
461
  ch = self.diamWidget.cellpose_channel_cb[k].currentText()
462
- idx = self.ch_norm.channel_cbs[k].findText(ch)
462
+ idx = self.ch_norm.channel_cbs[k].findData(ch)
463
463
  self.ch_norm.channel_cbs[k].setCurrentIndex(idx)
464
464
 
465
465
  self.diamWidget.close()
@@ -561,7 +561,7 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
561
561
  self.cellpose_model.setChecked(True)
562
562
 
563
563
  for c, cb in zip(channels, self.ch_norm.channel_cbs):
564
- index = cb.findText(c)
564
+ index = cb.findData(c)
565
565
  cb.setCurrentIndex(index)
566
566
 
567
567
  for i in range(len(channels)):
@@ -622,7 +622,7 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
622
622
 
623
623
  channels = []
624
624
  for i in range(len(self.ch_norm.channel_cbs)):
625
- channels.append(self.ch_norm.channel_cbs[i].currentText())
625
+ channels.append(self.ch_norm.channel_cbs[i].currentData())
626
626
 
627
627
  slots_to_keep = np.where(np.array(channels) != "--")[0]
628
628
  while "--" in channels:
@@ -100,11 +100,6 @@ class SettingsSignalAnnotator(CelldetectiveSettingsPanel):
100
100
  hbox_frac.addWidget(self.fraction_slider, 80)
101
101
  sub_layout.addLayout(hbox_frac)
102
102
 
103
- hbox_interval = QHBoxLayout()
104
- hbox_interval.addWidget(self._interval_lbl, 20)
105
- hbox_interval.addWidget(self.interval_slider, 80)
106
- sub_layout.addLayout(hbox_interval)
107
-
108
103
  self._layout.addLayout(sub_layout)
109
104
 
110
105
  self._layout.addWidget(self.submit_btn)
@@ -118,7 +113,6 @@ class SettingsSignalAnnotator(CelldetectiveSettingsPanel):
118
113
 
119
114
  self._modality_lbl = QLabel("Modality: ")
120
115
  self._fraction_lbl = QLabel("fraction: ")
121
- self._interval_lbl = QLabel("interval [ms]: ")
122
116
 
123
117
  self.gs_btn = QRadioButton("grayscale")
124
118
  self.gs_btn.setChecked(True)
@@ -173,14 +167,6 @@ class SettingsSignalAnnotator(CelldetectiveSettingsPanel):
173
167
  self.fraction_slider.setRange(0.1, 1)
174
168
  self.fraction_slider.setValue(0.25)
175
169
 
176
- self.interval_slider = QLabeledSlider()
177
- self.interval_slider.setSingleStep(1)
178
- self.interval_slider.setTickInterval(1)
179
- self.interval_slider.setSingleStep(1)
180
- self.interval_slider.setOrientation(Qt.Horizontal)
181
- self.interval_slider.setRange(1, 1000)
182
- self.interval_slider.setValue(1)
183
-
184
170
  def enable_channels(self):
185
171
  """
186
172
  Enable three channels when RGB mode is checked.
@@ -255,7 +241,6 @@ class SettingsSignalAnnotator(CelldetectiveSettingsPanel):
255
241
  "rgb_mode": self.rgb_btn.isChecked(),
256
242
  "percentile_mode": self.percentile_mode,
257
243
  "fraction": float(self.fraction_slider.value()),
258
- "interval": int(self.interval_slider.value()),
259
244
  "log": self.log_option,
260
245
  }
261
246
  max_i = 3 if self.rgb_btn.isChecked() else 1
@@ -322,10 +307,6 @@ class SettingsSignalAnnotator(CelldetectiveSettingsPanel):
322
307
  fraction = instructions["fraction"]
323
308
  self.fraction_slider.setValue(fraction)
324
309
 
325
- if "interval" in instructions:
326
- interval = instructions["interval"]
327
- self.interval_slider.setValue(interval)
328
-
329
310
  if "log" in instructions:
330
311
  self.log_option = not instructions["log"]
331
312
  self.switch_to_log()
@@ -19,11 +19,13 @@ from celldetective.utils.data_cleaning import extract_cols_from_table_list
19
19
  from celldetective.utils.parsing import _extract_labels_from_config
20
20
  from celldetective.utils.data_loaders import load_experiment_tables
21
21
  import numpy as np
22
+ import pandas as pd
22
23
  import os
23
24
  import matplotlib.pyplot as plt
24
25
 
25
26
  plt.rcParams["svg.fonttype"] = "none"
26
27
  from glob import glob
28
+ from celldetective import get_logger
27
29
  from celldetective.gui.base.styles import Styles
28
30
  from celldetective.gui.base.components import CelldetectiveWidget
29
31
  from matplotlib import colormaps
@@ -32,6 +34,8 @@ from celldetective.relative_measurements import expand_pair_table
32
34
  import matplotlib.cm
33
35
  from celldetective.neighborhood import extract_neighborhood_in_pair_table
34
36
 
37
+ logger = get_logger(__name__)
38
+
35
39
 
36
40
  class ConfigSurvival(CelldetectiveWidget):
37
41
  """
@@ -225,7 +229,9 @@ class ConfigSurvival(CelldetectiveWidget):
225
229
  )
226
230
  )
227
231
  if not tables_pairs:
228
- print("No pair table found... please compute the pair measurements...")
232
+ logger.warning(
233
+ "No pair table found. Please compute the pair measurements."
234
+ )
229
235
  return None
230
236
  self.cols_pairs = extract_cols_from_table_list(tables_pairs)
231
237
 
@@ -264,7 +270,7 @@ class ConfigSurvival(CelldetectiveWidget):
264
270
  and str(self.population_neigh) not in c
265
271
  ]
266
272
 
267
- print(f"{self.neighborhood_keys=}")
273
+ logger.debug(f"Neighborhood keys: {self.neighborhood_keys}")
268
274
 
269
275
  time_idx = np.array(
270
276
  [s.startswith("t_") or s.startswith("t0") for s in self.cols_pairs]
@@ -281,8 +287,8 @@ class ConfigSurvival(CelldetectiveWidget):
281
287
 
282
288
  try:
283
289
  time_columns = list(self.all_columns[time_idx])
284
- except:
285
- print("no column starts with t")
290
+ except (IndexError, KeyError):
291
+ logger.warning("No column starts with 't_' for time reference.")
286
292
  self.auto_close = True
287
293
  return None
288
294
 
@@ -295,8 +301,15 @@ class ConfigSurvival(CelldetectiveWidget):
295
301
 
296
302
  def process_survival(self):
297
303
 
304
+ # Validate that reference time and time of interest are different
305
+ time_of_reference = self.cbs[1].currentText()
306
+ time_of_interest = self.cbs[2].currentText()
307
+ if time_of_reference == time_of_interest:
308
+ generic_message("Reference time and time of interest must be different.")
309
+ return None
310
+
298
311
  self.FrameToMin = float(self.time_calibration_le.text().replace(",", "."))
299
- self.time_of_interest = self.cbs[2].currentText()
312
+ self.time_of_interest = time_of_interest
300
313
  if self.time_of_interest == "t0":
301
314
  self.class_of_interest = "class"
302
315
  elif self.time_of_interest.startswith("t0"):
@@ -312,8 +325,22 @@ class ConfigSurvival(CelldetectiveWidget):
312
325
  query_text = self.query_le.text()
313
326
  if query_text != "":
314
327
  self.df = self.df.query(query_text)
328
+ except pd.errors.UndefinedVariableError as e:
329
+ logger.warning(f"Query failed - undefined variable: {e}")
330
+ generic_message(
331
+ f"Query error: column not found.\n{e}\n\nPlease check your column names."
332
+ )
333
+ return None
334
+ except SyntaxError as e:
335
+ logger.warning(f"Query failed - syntax error: {e}")
336
+ generic_message(
337
+ f"Query syntax error: {e}\n\nCheck your query format (e.g., 'column > 100')."
338
+ )
339
+ return None
315
340
  except Exception as e:
316
- print(e, " The query is misunderstood and will not be applied...")
341
+ logger.warning(f"Query not applied: {e}")
342
+ generic_message(f"Query could not be applied: {e}")
343
+ return None
317
344
 
318
345
  self.interpret_pos_location()
319
346
 
@@ -391,11 +418,12 @@ class ConfigSurvival(CelldetectiveWidget):
391
418
  / self.FrameToMin
392
419
  )
393
420
  if not 0 < cut_observation_time <= (self.df["FRAME"].max()):
394
- print("Invalid cut time (larger than movie length)... Not applied.")
421
+ logger.warning(
422
+ "Invalid cut time (larger than movie length). Not applied."
423
+ )
395
424
  cut_observation_time = None
396
425
  except Exception as e:
397
- print(f"{e=}")
398
- pass
426
+ logger.debug(f"Cut time parsing error: {e}")
399
427
 
400
428
  pairs = False
401
429
  if self.neighborhood_keys is not None:
@@ -403,7 +431,7 @@ class ConfigSurvival(CelldetectiveWidget):
403
431
 
404
432
  # Per position survival
405
433
  for block, movie_group in self.df.groupby(["well", "position"]):
406
- print(f"{block=}")
434
+ logger.debug(f"Processing block: {block}")
407
435
  ks = compute_survival(
408
436
  movie_group,
409
437
  self.class_of_interest,
@@ -413,7 +441,7 @@ class ConfigSurvival(CelldetectiveWidget):
413
441
  cut_observation_time=cut_observation_time,
414
442
  pairs=pairs,
415
443
  )
416
- print(f"{ks=}")
444
+ logger.debug(f"Survival fit result: {ks}")
417
445
  if ks is not None:
418
446
  self.df_pos_info.loc[
419
447
  self.df_pos_info["pos_path"] == block[1], "survival_fit"
@@ -21,8 +21,13 @@ from celldetective.gui.gui_utils import (
21
21
  )
22
22
  from celldetective.gui.base.figure_canvas import FigureCanvas
23
23
  from celldetective.gui.base.utils import center_window
24
- from celldetective.gui.table_ops._maths import DifferentiateColWidget, OperationOnColsWidget, CalibrateColWidget, \
25
- AbsColWidget, LogColWidget
24
+ from celldetective.gui.table_ops._maths import (
25
+ DifferentiateColWidget,
26
+ OperationOnColsWidget,
27
+ CalibrateColWidget,
28
+ AbsColWidget,
29
+ LogColWidget,
30
+ )
26
31
  from celldetective.gui.table_ops._merge_one_hot import MergeOneHotWidget
27
32
  from celldetective.gui.table_ops._query_table import QueryWidget
28
33
  from celldetective.gui.table_ops._rename_col import RenameColWidget
@@ -55,7 +60,7 @@ class PivotTableUI(CelldetectiveWidget):
55
60
  self.mode = mode
56
61
 
57
62
  self.setWindowTitle(title)
58
- print("tab to show: ", self.data)
63
+ logger.debug(f"Pivot table to show: {self.data.shape}")
59
64
 
60
65
  self.table = QTableView(self)
61
66
 
@@ -219,9 +224,35 @@ class TableUI(CelldetectiveMainWindow):
219
224
 
220
225
  try:
221
226
  self.fig.tight_layout()
222
- except:
227
+ except AttributeError:
228
+ # fig not yet created
223
229
  pass
224
230
 
231
+ def _get_selected_columns(self, max_cols=None):
232
+ """
233
+ Get selected column names from the table view.
234
+
235
+ Parameters
236
+ ----------
237
+ max_cols : int, optional
238
+ Maximum number of columns to return. Returns all if None.
239
+
240
+ Returns
241
+ -------
242
+ list
243
+ List of selected column names.
244
+ """
245
+ x = self.table_view.selectedIndexes()
246
+ col_idx = np.unique(np.array([l.column() for l in x]))
247
+ cols = np.array(list(self.data.columns))
248
+ result = []
249
+ if len(col_idx) > 0:
250
+ for i in col_idx:
251
+ result.append(str(cols[i]))
252
+ if max_cols is not None:
253
+ return result[:max_cols]
254
+ return result
255
+
225
256
  def _create_actions(self):
226
257
 
227
258
  self.save_as = QAction("&Save as...", self)
@@ -540,8 +571,8 @@ class TableUI(CelldetectiveMainWindow):
540
571
  self.renameWidget.show()
541
572
 
542
573
  def save_as_csv_inplace_per_pos(self):
543
-
544
- print("Saving each table in its respective position folder...")
574
+ """Save each position's table in its respective folder."""
575
+ logger.info("Saving each table in its respective position folder...")
545
576
  for pos, pos_group in self.data.groupby(["position"]):
546
577
  invalid_cols = [
547
578
  c for c in list(pos_group.columns) if c.startswith("Unnamed")
@@ -555,26 +586,12 @@ class TableUI(CelldetectiveMainWindow):
555
586
  ),
556
587
  index=False,
557
588
  )
558
- print("Done...")
589
+ logger.info("Done saving tables.")
559
590
 
560
591
  def divide_signals(self):
561
-
562
- x = self.table_view.selectedIndexes()
563
- col_idx = np.unique(np.array([l.column() for l in x]))
564
- if isinstance(col_idx, (list, np.ndarray)):
565
- cols = np.array(list(self.data.columns))
566
- if len(col_idx) > 0:
567
- selected_col1 = str(cols[col_idx[0]])
568
- if len(col_idx) > 1:
569
- selected_col2 = str(cols[col_idx[1]])
570
- else:
571
- selected_col2 = None
572
- else:
573
- selected_col1 = None
574
- selected_col2 = None
575
- else:
576
- selected_col1 = None
577
- selected_col2 = None
592
+ selected = self._get_selected_columns(max_cols=2)
593
+ selected_col1 = selected[0] if len(selected) > 0 else None
594
+ selected_col2 = selected[1] if len(selected) > 1 else None
578
595
 
579
596
  self.divWidget = OperationOnColsWidget(
580
597
  self, column1=selected_col1, column2=selected_col2, operation="divide"
@@ -582,23 +599,9 @@ class TableUI(CelldetectiveMainWindow):
582
599
  self.divWidget.show()
583
600
 
584
601
  def multiply_signals(self):
585
-
586
- x = self.table_view.selectedIndexes()
587
- col_idx = np.unique(np.array([l.column() for l in x]))
588
- if isinstance(col_idx, (list, np.ndarray)):
589
- cols = np.array(list(self.data.columns))
590
- if len(col_idx) > 0:
591
- selected_col1 = str(cols[col_idx[0]])
592
- if len(col_idx) > 1:
593
- selected_col2 = str(cols[col_idx[1]])
594
- else:
595
- selected_col2 = None
596
- else:
597
- selected_col1 = None
598
- selected_col2 = None
599
- else:
600
- selected_col1 = None
601
- selected_col2 = None
602
+ selected = self._get_selected_columns(max_cols=2)
603
+ selected_col1 = selected[0] if len(selected) > 0 else None
604
+ selected_col2 = selected[1] if len(selected) > 1 else None
602
605
 
603
606
  self.mulWidget = OperationOnColsWidget(
604
607
  self, column1=selected_col1, column2=selected_col2, operation="multiply"
@@ -606,23 +609,9 @@ class TableUI(CelldetectiveMainWindow):
606
609
  self.mulWidget.show()
607
610
 
608
611
  def add_signals(self):
609
-
610
- x = self.table_view.selectedIndexes()
611
- col_idx = np.unique(np.array([l.column() for l in x]))
612
- if isinstance(col_idx, (list, np.ndarray)):
613
- cols = np.array(list(self.data.columns))
614
- if len(col_idx) > 0:
615
- selected_col1 = str(cols[col_idx[0]])
616
- if len(col_idx) > 1:
617
- selected_col2 = str(cols[col_idx[1]])
618
- else:
619
- selected_col2 = None
620
- else:
621
- selected_col1 = None
622
- selected_col2 = None
623
- else:
624
- selected_col1 = None
625
- selected_col2 = None
612
+ selected = self._get_selected_columns(max_cols=2)
613
+ selected_col1 = selected[0] if len(selected) > 0 else None
614
+ selected_col2 = selected[1] if len(selected) > 1 else None
626
615
 
627
616
  self.addiWidget = OperationOnColsWidget(
628
617
  self, column1=selected_col1, column2=selected_col2, operation="add"
@@ -630,23 +619,9 @@ class TableUI(CelldetectiveMainWindow):
630
619
  self.addiWidget.show()
631
620
 
632
621
  def subtract_signals(self):
633
-
634
- x = self.table_view.selectedIndexes()
635
- col_idx = np.unique(np.array([l.column() for l in x]))
636
- if isinstance(col_idx, (list, np.ndarray)):
637
- cols = np.array(list(self.data.columns))
638
- if len(col_idx) > 0:
639
- selected_col1 = str(cols[col_idx[0]])
640
- if len(col_idx) > 1:
641
- selected_col2 = str(cols[col_idx[1]])
642
- else:
643
- selected_col2 = None
644
- else:
645
- selected_col1 = None
646
- selected_col2 = None
647
- else:
648
- selected_col1 = None
649
- selected_col2 = None
622
+ selected = self._get_selected_columns(max_cols=2)
623
+ selected_col1 = selected[0] if len(selected) > 0 else None
624
+ selected_col2 = selected[1] if len(selected) > 1 else None
650
625
 
651
626
  self.subWidget = OperationOnColsWidget(
652
627
  self, column1=selected_col1, column2=selected_col2, operation="subtract"
@@ -654,56 +629,22 @@ class TableUI(CelldetectiveMainWindow):
654
629
  self.subWidget.show()
655
630
 
656
631
  def differenciate_selected_feature(self):
657
-
658
- # check only one col selected and assert is numerical
659
- # open widget to select window parameters, directionality
660
- # create new col
661
-
662
- x = self.table_view.selectedIndexes()
663
- col_idx = np.unique(np.array([l.column() for l in x]))
664
- if isinstance(col_idx, (list, np.ndarray)):
665
- cols = np.array(list(self.data.columns))
666
- if len(col_idx) > 0:
667
- selected_col = str(cols[col_idx[0]])
668
- else:
669
- selected_col = None
670
- else:
671
- selected_col = None
672
-
632
+ """Open widget to differentiate the selected column."""
633
+ selected = self._get_selected_columns(max_cols=1)
634
+ selected_col = selected[0] if selected else None
673
635
  self.diffWidget = DifferentiateColWidget(self, selected_col)
674
636
  self.diffWidget.show()
675
637
 
676
638
  def take_log_of_selected_feature(self):
677
-
678
- # check only one col selected and assert is numerical
679
- # open widget to select window parameters, directionality
680
- # create new col
681
-
682
- x = self.table_view.selectedIndexes()
683
- col_idx = np.unique(np.array([l.column() for l in x]))
684
- if isinstance(col_idx, (list, np.ndarray)):
685
- cols = np.array(list(self.data.columns))
686
- if len(col_idx) > 0:
687
- selected_col = str(cols[col_idx[0]])
688
- else:
689
- selected_col = None
690
- else:
691
- selected_col = None
692
-
639
+ """Open widget to take log of the selected column."""
640
+ selected = self._get_selected_columns(max_cols=1)
641
+ selected_col = selected[0] if selected else None
693
642
  self.LogWidget = LogColWidget(self, selected_col)
694
643
  self.LogWidget.show()
695
644
 
696
645
  def merge_classification_features(self):
697
-
698
- x = self.table_view.selectedIndexes()
699
- col_idx = np.unique(np.array([l.column() for l in x]))
700
-
701
- col_selection = []
702
- if isinstance(col_idx, (list, np.ndarray)):
703
- cols = np.array(list(self.data.columns))
704
- if len(col_idx) > 0:
705
- selected_cols = cols[col_idx]
706
- col_selection.extend(selected_cols)
646
+ """Open widget to merge selected classification columns."""
647
+ col_selection = self._get_selected_columns()
707
648
 
708
649
  # Lazy load MergeGroupWidget
709
650
  from celldetective.gui.table_ops._merge_groups import MergeGroupWidget
@@ -712,38 +653,16 @@ class TableUI(CelldetectiveMainWindow):
712
653
  self.merge_classification_widget.show()
713
654
 
714
655
  def calibrate_selected_feature(self):
715
-
716
- x = self.table_view.selectedIndexes()
717
- col_idx = np.unique(np.array([l.column() for l in x]))
718
- if isinstance(col_idx, (list, np.ndarray)):
719
- cols = np.array(list(self.data.columns))
720
- if len(col_idx) > 0:
721
- selected_col = str(cols[col_idx[0]])
722
- else:
723
- selected_col = None
724
- else:
725
- selected_col = None
726
-
656
+ """Open widget to calibrate the selected column."""
657
+ selected = self._get_selected_columns(max_cols=1)
658
+ selected_col = selected[0] if selected else None
727
659
  self.calWidget = CalibrateColWidget(self, selected_col)
728
660
  self.calWidget.show()
729
661
 
730
662
  def take_abs_of_selected_feature(self):
731
-
732
- # check only one col selected and assert is numerical
733
- # open widget to select window parameters, directionality
734
- # create new col
735
-
736
- x = self.table_view.selectedIndexes()
737
- col_idx = np.unique(np.array([l.column() for l in x]))
738
- if isinstance(col_idx, (list, np.ndarray)):
739
- cols = np.array(list(self.data.columns))
740
- if len(col_idx) > 0:
741
- selected_col = str(cols[col_idx[0]])
742
- else:
743
- selected_col = None
744
- else:
745
- selected_col = None
746
-
663
+ """Open widget to take absolute value of the selected column."""
664
+ selected = self._get_selected_columns(max_cols=1)
665
+ selected_col = selected[0] if selected else None
747
666
  self.absWidget = AbsColWidget(self, selected_col)
748
667
  self.absWidget.show()
749
668
 
@@ -991,7 +910,8 @@ class TableUI(CelldetectiveMainWindow):
991
910
  y = column_names[unique_cols]
992
911
  idx = self.y_cb.findText(y)
993
912
  self.y_cb.setCurrentIndex(idx)
994
- except:
913
+ except (IndexError, KeyError):
914
+ # No column selected or invalid selection
995
915
  pass
996
916
 
997
917
  hbox = QHBoxLayout()
@@ -1018,7 +938,8 @@ class TableUI(CelldetectiveMainWindow):
1018
938
  if hasattr(matplotlib.cm, str(cm).lower()):
1019
939
  try:
1020
940
  self.cmap_cb.addColormap(cm.lower())
1021
- except:
941
+ except Exception:
942
+ # Some colormaps may fail to add
1022
943
  pass
1023
944
 
1024
945
  hbox = QHBoxLayout()
@@ -1057,7 +978,7 @@ class TableUI(CelldetectiveMainWindow):
1057
978
  cmap(i / len(self.data[self.hue_variable].unique()))
1058
979
  for i in range(len(self.data[self.hue_variable].unique()))
1059
980
  ]
1060
- except:
981
+ except (KeyError, ZeroDivisionError):
1061
982
  colors = None
1062
983
 
1063
984
  if self.hue_cb.currentText() == "--":