celldetective 1.3.9.post5__py3-none-any.whl → 1.4.1__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 (94) hide show
  1. celldetective/__init__.py +0 -3
  2. celldetective/_version.py +1 -1
  3. celldetective/events.py +2 -4
  4. celldetective/exceptions.py +11 -0
  5. celldetective/extra_properties.py +132 -0
  6. celldetective/filters.py +7 -1
  7. celldetective/gui/InitWindow.py +37 -46
  8. celldetective/gui/__init__.py +3 -9
  9. celldetective/gui/about.py +19 -15
  10. celldetective/gui/analyze_block.py +34 -19
  11. celldetective/gui/base_annotator.py +786 -0
  12. celldetective/gui/base_components.py +23 -0
  13. celldetective/gui/classifier_widget.py +86 -94
  14. celldetective/gui/configure_new_exp.py +163 -46
  15. celldetective/gui/control_panel.py +76 -146
  16. celldetective/gui/{signal_annotator.py → event_annotator.py} +533 -1438
  17. celldetective/gui/generic_signal_plot.py +11 -13
  18. celldetective/gui/gui_utils.py +54 -23
  19. celldetective/gui/help/neighborhood.json +2 -2
  20. celldetective/gui/json_readers.py +5 -4
  21. celldetective/gui/layouts.py +265 -31
  22. celldetective/gui/{signal_annotator2.py → pair_event_annotator.py} +433 -635
  23. celldetective/gui/plot_measurements.py +21 -17
  24. celldetective/gui/plot_signals_ui.py +125 -72
  25. celldetective/gui/process_block.py +283 -188
  26. celldetective/gui/processes/compute_neighborhood.py +594 -0
  27. celldetective/gui/processes/downloader.py +37 -34
  28. celldetective/gui/processes/measure_cells.py +19 -8
  29. celldetective/gui/processes/segment_cells.py +47 -11
  30. celldetective/gui/processes/track_cells.py +18 -13
  31. celldetective/gui/seg_model_loader.py +21 -62
  32. celldetective/gui/settings/__init__.py +7 -0
  33. celldetective/gui/settings/_settings_base.py +70 -0
  34. celldetective/gui/{retrain_signal_model_options.py → settings/_settings_event_model_training.py} +54 -109
  35. celldetective/gui/{measurement_options.py → settings/_settings_measurements.py} +54 -92
  36. celldetective/gui/{neighborhood_options.py → settings/_settings_neighborhood.py} +10 -13
  37. celldetective/gui/settings/_settings_segmentation.py +49 -0
  38. celldetective/gui/{retrain_segmentation_model_options.py → settings/_settings_segmentation_model_training.py} +38 -92
  39. celldetective/gui/{signal_annotator_options.py → settings/_settings_signal_annotator.py} +78 -103
  40. celldetective/gui/{btrack_options.py → settings/_settings_tracking.py} +85 -116
  41. celldetective/gui/styles.py +2 -1
  42. celldetective/gui/survival_ui.py +49 -95
  43. celldetective/gui/tableUI.py +53 -25
  44. celldetective/gui/table_ops/__init__.py +0 -0
  45. celldetective/gui/table_ops/merge_groups.py +118 -0
  46. celldetective/gui/thresholds_gui.py +617 -1221
  47. celldetective/gui/viewers.py +107 -42
  48. celldetective/gui/workers.py +8 -4
  49. celldetective/io.py +137 -57
  50. celldetective/links/zenodo.json +145 -144
  51. celldetective/measure.py +94 -53
  52. celldetective/neighborhood.py +342 -268
  53. celldetective/preprocessing.py +56 -35
  54. celldetective/regionprops/_regionprops.py +16 -5
  55. celldetective/relative_measurements.py +50 -29
  56. celldetective/scripts/analyze_signals.py +4 -1
  57. celldetective/scripts/measure_cells.py +5 -5
  58. celldetective/scripts/measure_relative.py +20 -12
  59. celldetective/scripts/segment_cells.py +4 -10
  60. celldetective/scripts/segment_cells_thresholds.py +3 -3
  61. celldetective/scripts/track_cells.py +10 -8
  62. celldetective/scripts/train_segmentation_model.py +18 -6
  63. celldetective/signals.py +29 -14
  64. celldetective/tracking.py +14 -3
  65. celldetective/utils.py +91 -62
  66. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/METADATA +24 -16
  67. celldetective-1.4.1.dist-info/RECORD +123 -0
  68. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/WHEEL +1 -1
  69. tests/gui/__init__.py +0 -0
  70. tests/gui/test_new_project.py +228 -0
  71. tests/gui/test_project.py +99 -0
  72. tests/test_preprocessing.py +2 -2
  73. celldetective/models/segmentation_effectors/ricm_bf_all_last/config_input.json +0 -79
  74. celldetective/models/segmentation_effectors/ricm_bf_all_last/ricm_bf_all_last +0 -0
  75. celldetective/models/segmentation_effectors/ricm_bf_all_last/training_instructions.json +0 -37
  76. celldetective/models/segmentation_effectors/test-transfer/config_input.json +0 -39
  77. celldetective/models/segmentation_effectors/test-transfer/test-transfer +0 -0
  78. celldetective/models/signal_detection/NucCond/classification_loss.png +0 -0
  79. celldetective/models/signal_detection/NucCond/classifier.h5 +0 -0
  80. celldetective/models/signal_detection/NucCond/config_input.json +0 -1
  81. celldetective/models/signal_detection/NucCond/log_classifier.csv +0 -126
  82. celldetective/models/signal_detection/NucCond/log_regressor.csv +0 -282
  83. celldetective/models/signal_detection/NucCond/regression_loss.png +0 -0
  84. celldetective/models/signal_detection/NucCond/regressor.h5 +0 -0
  85. celldetective/models/signal_detection/NucCond/scores.npy +0 -0
  86. celldetective/models/signal_detection/NucCond/test_confusion_matrix.png +0 -0
  87. celldetective/models/signal_detection/NucCond/test_regression.png +0 -0
  88. celldetective/models/signal_detection/NucCond/validation_confusion_matrix.png +0 -0
  89. celldetective/models/signal_detection/NucCond/validation_regression.png +0 -0
  90. celldetective-1.3.9.post5.dist-info/RECORD +0 -129
  91. tests/test_qt.py +0 -103
  92. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/entry_points.txt +0 -0
  93. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info/licenses}/LICENSE +0 -0
  94. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
- from PyQt5.QtWidgets import QMessageBox, QComboBox, QLineEdit, QVBoxLayout, QWidget, QLabel, QHBoxLayout, QPushButton
1
+ from PyQt5.QtWidgets import QComboBox, QLineEdit, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
2
2
  from PyQt5.QtCore import Qt
3
3
  from PyQt5.QtGui import QDoubleValidator
4
- from celldetective.gui.gui_utils import center_window
4
+ from celldetective.gui.gui_utils import center_window, generic_message
5
5
  from superqt import QColormapComboBox
6
6
  from celldetective.gui.generic_signal_plot import SurvivalPlotWidget
7
7
  from celldetective.utils import get_software_location, _extract_labels_from_config, extract_cols_from_table_list
@@ -11,14 +11,14 @@ import os
11
11
  import matplotlib.pyplot as plt
12
12
  plt.rcParams['svg.fonttype'] = 'none'
13
13
  from glob import glob
14
- from celldetective.gui import Styles
14
+ from celldetective.gui import Styles, CelldetectiveWidget
15
15
  from matplotlib import colormaps
16
16
  from celldetective.events import compute_survival
17
17
  from celldetective.relative_measurements import expand_pair_table
18
18
  import matplotlib.cm
19
19
  from celldetective.neighborhood import extract_neighborhood_in_pair_table
20
20
 
21
- class ConfigSurvival(QWidget, Styles):
21
+ class ConfigSurvival(CelldetectiveWidget):
22
22
 
23
23
  """
24
24
  UI to set survival instructions.
@@ -30,7 +30,6 @@ class ConfigSurvival(QWidget, Styles):
30
30
  super().__init__()
31
31
  self.parent_window = parent_window
32
32
  self.setWindowTitle("Configure survival")
33
- self.setWindowIcon(self.celldetective_icon)
34
33
 
35
34
  self.exp_dir = self.parent_window.exp_dir
36
35
  self.soft_path = get_software_location()
@@ -53,8 +52,6 @@ class ConfigSurvival(QWidget, Styles):
53
52
  self.populate_widget()
54
53
  if self.auto_close:
55
54
  self.close()
56
-
57
- self.setAttribute(Qt.WA_DeleteOnClose)
58
55
 
59
56
  def interpret_pos_location(self):
60
57
 
@@ -91,51 +88,27 @@ class ConfigSurvival(QWidget, Styles):
91
88
 
92
89
 
93
90
  pops = []
94
- for population in ['effectors','targets','pairs']:
91
+ self.cols_per_pop = {}
92
+ for population in self.parent_window.parent_window.populations:
95
93
  tables = glob(self.exp_dir+os.sep.join(['W*','*','output','tables',f'trajectories_{population}.csv']))
96
94
  if len(tables)>0:
97
95
  pops.append(population)
98
-
99
- tables_targets = glob(self.exp_dir+os.sep.join(['W*','*','output','tables',f'trajectories_targets.csv']))
100
- self.cols_targets = extract_cols_from_table_list(tables_targets)
101
- tables_effectors = glob(self.exp_dir+os.sep.join(['W*','*','output','tables',f'trajectories_effectors.csv']))
102
- self.cols_effectors = extract_cols_from_table_list(tables_effectors)
103
-
104
- # Smart reading of existing neighborhoods (without loading tables in memory)
105
- if 'pairs' in pops and not 'targets' in pops:
106
- # must be effector-effector
107
- effector_neighs = [c[16:] for c in self.cols_effectors if c.startswith('inclusive_count_neighborhood')]
108
- if len(effector_neighs)>0:
109
- pops.pop(pops.index('pairs'))
110
- pops.append('effectors-effectors')
111
- elif 'pairs' in pops and not 'effectors' in pops:
112
- # must be target-target
113
- target_neighs = [c for c in self.cols_targets if c.startswith('inclusive_count_neighborhood')]
114
- if len(target_neighs)>0:
115
- pops.pop(pops.index('pairs'))
116
- pops.append('targets-targets')
117
- elif 'pairs' in pops:
118
- # either effector-target or target-effector
119
- target_neighs_cross = [c for c in self.cols_targets if c.startswith('inclusive_count_neighborhood') and '_2_' in c]
120
- if len(target_neighs_cross)>0:
121
- pops.append('targets-effectors')
122
- effector_neighs_cross = [c for c in self.cols_effectors if c.startswith('inclusive_count_neighborhood') and '_2_' in c]
123
- if len(effector_neighs_cross)>0:
124
- pops.append('effectors-targets')
125
- target_neighs = [c for c in self.cols_targets if c.startswith('inclusive_count_neighborhood') and 'self' in c]
126
- if len(target_neighs)>0:
127
- pops.append('targets-targets')
128
- effector_neighs = [c for c in self.cols_effectors if c.startswith('inclusive_count_neighborhood') and 'self' in c]
129
- if len(effector_neighs)>0:
130
- pops.append('effectors-effectors')
131
- pops.pop(pops.index('pairs'))
132
- else:
133
- pass
134
-
96
+ cols = extract_cols_from_table_list(tables)
97
+
98
+ # check for neighbor pairs
99
+ neigh_cols = [c for c in cols if c.startswith('inclusive_count_neighborhood')]
100
+ neigh_pairs = [c.split('_(')[-1].split(')_')[0].split('-') for c in neigh_cols]
101
+ neigh_pairs = ['-'.join(c) for c in neigh_pairs]
102
+ for k in range(len(neigh_pairs)):
103
+ if "_self_" in neigh_pairs[k]:
104
+ neigh_pairs[k] = '-'.join([population, population])
105
+ pops.extend(neigh_pairs)
106
+
107
+ self.cols_per_pop.update({population: cols})
135
108
 
136
109
  labels = [QLabel('population: '), QLabel('time of\nreference: '), QLabel('time of\ninterest: '), QLabel('cmap: ')] #QLabel('class: '),
137
110
  self.cb_options = [pops, ['0'], [], []] #['class'],
138
- self.cbs = [QComboBox() for i in range(len(labels))]
111
+ self.cbs = [QComboBox() for _ in range(len(labels))]
139
112
 
140
113
  self.cbs[-1] = QColormapComboBox()
141
114
  self.cbs[0].currentIndexChanged.connect(self.set_classes_and_times)
@@ -155,12 +128,8 @@ class ConfigSurvival(QWidget, Styles):
155
128
  if hasattr(matplotlib.cm, str(cm).lower()):
156
129
  try:
157
130
  self.cbs[-1].addColormap(cm.lower())
158
- except:
131
+ except Exception as _:
159
132
  pass
160
- #try:
161
- # self.cbs[-1].addColormap(cm)
162
- # except:
163
- # pass
164
133
 
165
134
  main_layout.addLayout(choice_layout)
166
135
 
@@ -213,18 +182,16 @@ class ConfigSurvival(QWidget, Styles):
213
182
 
214
183
  self.population = 'pairs'
215
184
  tables_pairs = glob(self.exp_dir+os.sep.join(['W*','*','output','tables',f'trajectories_pairs.csv']))
185
+ if not tables_pairs:
186
+ print('No pair table found... please compute the pair measurements...')
187
+ return None
216
188
  self.cols_pairs = extract_cols_from_table_list(tables_pairs)
217
189
 
218
190
  self.population_reference = pop_split[0]
219
191
  self.population_neigh = pop_split[1]
220
- if self.population_reference=='targets':
221
- cols_ref = self.cols_targets
222
- else:
223
- cols_ref = self.cols_effectors
224
- if self.population_neigh=='targets':
225
- cols_neigh = self.cols_targets
226
- else:
227
- cols_neigh = self.cols_effectors
192
+
193
+ cols_ref = self.cols_per_pop[self.population_reference]
194
+ cols_neigh = self.cols_per_pop[self.population_neigh]
228
195
 
229
196
  time_cols_ref = np.array([s.startswith('t_') or s=='t0' for s in cols_ref])
230
197
  if len(time_cols_ref)>0:
@@ -237,9 +204,11 @@ class ConfigSurvival(QWidget, Styles):
237
204
  time_cols_neigh = ['neighbor_'+t for t in time_cols_neigh]
238
205
 
239
206
  if self.population_reference!=self.population_neigh:
240
- self.neighborhood_keys = [c[16:] for c in cols_ref if c.startswith('inclusive_count_neighborhood') and '_2_' in c]
207
+ self.neighborhood_keys = [c[16:] for c in cols_ref if c.startswith('inclusive_count_neighborhood') and str(self.population_neigh) in c]
241
208
  else:
242
- self.neighborhood_keys = [c[16:] for c in cols_ref if c.startswith('inclusive_count_neighborhood') and 'self' in c]
209
+ self.neighborhood_keys = [c[16:] for c in cols_ref if c.startswith('inclusive_count_neighborhood') and str(self.population_neigh) not in c]
210
+
211
+ print(f"{self.neighborhood_keys=}")
243
212
 
244
213
  time_idx = np.array([s.startswith('t_') or s.startswith('t0') for s in self.cols_pairs])
245
214
  time_cols_pairs = list(self.cols_pairs[time_idx])
@@ -247,10 +216,7 @@ class ConfigSurvival(QWidget, Styles):
247
216
  time_columns = time_cols_ref + time_cols_neigh + time_cols_pairs
248
217
 
249
218
  else:
250
- if self.population=='targets':
251
- self.all_columns = self.cols_targets
252
- else:
253
- self.all_columns = self.cols_effectors
219
+ self.all_columns = self.cols_per_pop[self.population]
254
220
  time_idx = np.array([s.startswith('t_') or s=='t0' for s in self.all_columns])
255
221
 
256
222
  try:
@@ -274,6 +240,8 @@ class ConfigSurvival(QWidget, Styles):
274
240
  self.time_of_interest = self.cbs[2].currentText()
275
241
  if self.time_of_interest=="t0":
276
242
  self.class_of_interest = "class"
243
+ elif self.time_of_interest.startswith('t0'):
244
+ self.class_of_interest = self.time_of_interest.replace('t0_','class_')
277
245
  else:
278
246
  self.class_of_interest = self.time_of_interest.replace('t_','class_')
279
247
 
@@ -290,29 +258,19 @@ class ConfigSurvival(QWidget, Styles):
290
258
  print(e, ' The query is misunderstood and will not be applied...')
291
259
 
292
260
  self.interpret_pos_location()
261
+
293
262
  if self.class_of_interest in list(self.df.columns) and self.cbs[2].currentText() in list(self.df.columns):
294
263
  self.compute_survival_functions()
295
264
  else:
296
- msgBox = QMessageBox()
297
- msgBox.setIcon(QMessageBox.Warning)
298
- msgBox.setText("The class and/or event time of interest is not found in the dataframe...")
299
- msgBox.setWindowTitle("Warning")
300
- msgBox.setStandardButtons(QMessageBox.Ok)
301
- returnValue = msgBox.exec()
302
- if returnValue == QMessageBox.Ok:
303
- return None
265
+ generic_message("The class and/or event time of interest is not found in the dataframe...")
266
+ return None
267
+
304
268
  if 'survival_fit' in list(self.df_pos_info.columns):
305
269
  self.plot_window = SurvivalPlotWidget(parent_window=self, df=self.df, df_pos_info = self.df_pos_info, df_well_info = self.df_well_info, title='plot survivals')
306
270
  self.plot_window.show()
307
271
  else:
308
- msgBox = QMessageBox()
309
- msgBox.setIcon(QMessageBox.Warning)
310
- msgBox.setText("No survival function was successfully computed...\nCheck your parameter choice.")
311
- msgBox.setWindowTitle("Warning")
312
- msgBox.setStandardButtons(QMessageBox.Ok)
313
- returnValue = msgBox.exec()
314
- if returnValue == QMessageBox.Ok:
315
- return None
272
+ generic_message("No survival function was successfully computed...\nCheck your parameter choice.")
273
+ return None
316
274
 
317
275
  def load_available_tables_local(self):
318
276
 
@@ -327,32 +285,26 @@ class ConfigSurvival(QWidget, Styles):
327
285
  self.df, self.df_pos_info = load_experiment_tables(self.exp_dir, well_option=self.well_option, position_option=self.position_option, population=self.population, return_pos_info=True)
328
286
 
329
287
  if self.df is None:
330
- msgBox = QMessageBox()
331
- msgBox.setIcon(QMessageBox.Warning)
332
- msgBox.setText("No table could be found.. Abort.")
333
- msgBox.setWindowTitle("Warning")
334
- msgBox.setStandardButtons(QMessageBox.Ok)
335
- returnValue = msgBox.exec()
336
- if returnValue == QMessageBox.Ok:
337
- return None
288
+ generic_message("No table could be found.. Abort.")
289
+ return None
338
290
  else:
339
291
  self.df_well_info = self.df_pos_info.loc[:,['well_path', 'well_index', 'well_name', 'well_number', 'well_alias']].drop_duplicates()
340
- #print(f"{self.df_well_info=}")
341
292
 
342
293
  if self.population=='pairs':
343
294
  self.df = expand_pair_table(self.df)
344
295
  self.df = extract_neighborhood_in_pair_table(self.df, reference_population=self.population_reference, neighbor_population=self.population_neigh, neighborhood_key=self.neighborhood_keys[0], contact_only=True)
345
296
 
346
-
347
297
  def compute_survival_functions(self):
348
298
 
349
299
  cut_observation_time = None
350
300
  try:
351
- cut_observation_time = float(self.query_time_cut.text().replace(',','.')) / self.FrameToMin
352
- if not 0<cut_observation_time<=(self.df['FRAME'].max()):
353
- print('Invalid cut time (larger than movie length)... Not applied.')
354
- cut_observation_time = None
301
+ if self.query_time_cut.text()!='':
302
+ cut_observation_time = float(self.query_time_cut.text().replace(',','.')) / self.FrameToMin
303
+ if not 0<cut_observation_time<=(self.df['FRAME'].max()):
304
+ print('Invalid cut time (larger than movie length)... Not applied.')
305
+ cut_observation_time = None
355
306
  except Exception as e:
307
+ print(f"{e=}")
356
308
  pass
357
309
 
358
310
  pairs = False
@@ -361,7 +313,9 @@ class ConfigSurvival(QWidget, Styles):
361
313
 
362
314
  # Per position survival
363
315
  for block,movie_group in self.df.groupby(['well','position']):
316
+ print(f"{block=}")
364
317
  ks = compute_survival(movie_group, self.class_of_interest, self.cbs[2].currentText(), t_reference=self.cbs[1].currentText(), FrameToMin=self.FrameToMin, cut_observation_time=cut_observation_time, pairs=pairs)
318
+ print(f"{ks=}")
365
319
  if ks is not None:
366
320
  self.df_pos_info.loc[self.df_pos_info['pos_path']==block[1],'survival_fit'] = ks
367
321
 
@@ -1,8 +1,11 @@
1
- from PyQt5.QtWidgets import QRadioButton, QButtonGroup, QMainWindow, QTableView, QAction, QMenu,QFileDialog, QLineEdit, QHBoxLayout, QWidget, QPushButton, QVBoxLayout, QComboBox, QLabel, QCheckBox, QMessageBox
1
+ from PyQt5.QtWidgets import QRadioButton, QButtonGroup, QTableView, QAction, QMenu,QFileDialog, QLineEdit, QHBoxLayout, QPushButton, QVBoxLayout, QComboBox, QLabel, QCheckBox, QMessageBox
2
2
  from PyQt5.QtCore import Qt
3
3
  from PyQt5.QtGui import QBrush, QColor, QDoubleValidator
4
4
  import pandas as pd
5
5
  import matplotlib.pyplot as plt
6
+
7
+ from celldetective.gui.table_ops.merge_groups import MergeGroupWidget
8
+
6
9
  plt.rcParams['svg.fonttype'] = 'none'
7
10
  from celldetective.gui.gui_utils import FigureCanvas, center_window, QHSeperationLine, GenericOpColWidget, PandasModel
8
11
  from celldetective.utils import differentiate_per_track, collapse_trajectories_by_status, test_2samp_generic, safe_log
@@ -12,7 +15,7 @@ import numpy as np
12
15
  import seaborn as sns
13
16
  import matplotlib.cm as mcm
14
17
  import os
15
- from celldetective.gui import Styles
18
+ from celldetective.gui import CelldetectiveWidget, CelldetectiveMainWindow
16
19
  from superqt import QColormapComboBox, QLabeledSlider, QSearchableComboBox
17
20
  from superqt.fonticon import icon
18
21
  from fonticon_mdi6 import MDI6
@@ -22,12 +25,13 @@ from matplotlib import colormaps
22
25
  import matplotlib.cm
23
26
 
24
27
 
25
- class QueryWidget(QWidget):
28
+ class QueryWidget(CelldetectiveWidget):
26
29
 
27
30
  def __init__(self, parent_window):
28
31
 
29
32
  super().__init__()
30
33
  self.parent_window = parent_window
34
+
31
35
  self.setWindowTitle("Filter table")
32
36
  # Create the QComboBox and add some items
33
37
 
@@ -39,7 +43,6 @@ class QueryWidget(QWidget):
39
43
  self.submit_btn = QPushButton('submit')
40
44
  self.submit_btn.clicked.connect(self.filter_table)
41
45
  layout.addWidget(self.submit_btn, 30)
42
- self.setAttribute(Qt.WA_DeleteOnClose)
43
46
  center_window(self)
44
47
 
45
48
  def filter_table(self):
@@ -54,7 +57,7 @@ class QueryWidget(QWidget):
54
57
  return None
55
58
 
56
59
 
57
- class MergeOneHotWidget(QWidget, Styles):
60
+ class MergeOneHotWidget(CelldetectiveWidget):
58
61
 
59
62
  def __init__(self, parent_window, selected_columns=None):
60
63
 
@@ -139,7 +142,7 @@ class MergeOneHotWidget(QWidget, Styles):
139
142
  self.submit_btn.setEnabled(True)
140
143
 
141
144
 
142
- class DifferentiateColWidget(QWidget, Styles):
145
+ class DifferentiateColWidget(CelldetectiveWidget):
143
146
 
144
147
  def __init__(self, parent_window, column=None):
145
148
 
@@ -217,7 +220,7 @@ class DifferentiateColWidget(QWidget, Styles):
217
220
 
218
221
 
219
222
 
220
- class OperationOnColsWidget(QWidget, Styles):
223
+ class OperationOnColsWidget(CelldetectiveWidget):
221
224
 
222
225
  def __init__(self, parent_window, column1=None, column2=None, operation='divide'):
223
226
 
@@ -394,7 +397,7 @@ class LogColWidget(GenericOpColWidget):
394
397
  self.parent_window.data['log10('+self.measurements_cb.currentText()+')'] = safe_log(self.parent_window.data[self.measurements_cb.currentText()].values)
395
398
 
396
399
 
397
- class RenameColWidget(QWidget):
400
+ class RenameColWidget(CelldetectiveWidget):
398
401
 
399
402
  def __init__(self, parent_window, column=None):
400
403
 
@@ -429,11 +432,11 @@ class RenameColWidget(QWidget):
429
432
  self.parent_window.table_view.setModel(self.parent_window.model)
430
433
  self.close()
431
434
 
432
- class PivotTableUI(QWidget):
435
+ class PivotTableUI(CelldetectiveWidget):
433
436
 
434
437
  def __init__(self, data, title="", mode=None, *args, **kwargs):
435
438
 
436
- QWidget.__init__(self, *args, **kwargs)
439
+ CelldetectiveWidget.__init__(self, *args, **kwargs)
437
440
 
438
441
  self.data = data
439
442
  self.title = title
@@ -535,11 +538,11 @@ class PivotTableUI(QWidget):
535
538
  """
536
539
  self.information_label.setText(html_caption)
537
540
 
538
- class TableUI(QMainWindow, Styles):
541
+ class TableUI(CelldetectiveMainWindow):
539
542
 
540
543
  def __init__(self, data, title, population='targets',plot_mode="plot_track_signals", save_inplace_option=False, collapse_tracks_option=True, *args, **kwargs):
541
544
 
542
- QMainWindow.__init__(self, *args, **kwargs)
545
+ CelldetectiveMainWindow.__init__(self, *args, **kwargs)
543
546
 
544
547
  self.setWindowTitle(title)
545
548
  self.setGeometry(100,100,1000,400)
@@ -574,7 +577,6 @@ class TableUI(QMainWindow, Styles):
574
577
  self.model = PandasModel(data)
575
578
  self.table_view.setModel(self.model)
576
579
  self.table_view.resizeColumnsToContents()
577
- self.setAttribute(Qt.WA_DeleteOnClose)
578
580
 
579
581
  def resizeEvent(self, event):
580
582
 
@@ -651,11 +653,17 @@ class TableUI(QMainWindow, Styles):
651
653
  self.calibrate_action.triggered.connect(self.calibrate_selected_feature)
652
654
  self.calibrate_action.setShortcut("Ctrl+C")
653
655
  self.mathMenu.addAction(self.calibrate_action)
656
+
657
+ self.merge_classification_action = QAction('&Merge states...', self)
658
+ self.merge_classification_action.triggered.connect(self.merge_classification_features)
659
+ self.mathMenu.addAction(self.merge_classification_action)
654
660
 
655
661
  self.derivative_action = QAction('&Differentiate...', self)
656
662
  self.derivative_action.triggered.connect(self.differenciate_selected_feature)
657
663
  self.derivative_action.setShortcut("Ctrl+D")
658
664
  self.mathMenu.addAction(self.derivative_action)
665
+ if not self.tracks:
666
+ self.derivative_action.setEnabled(False)
659
667
 
660
668
  self.abs_action = QAction('&Absolute value...', self)
661
669
  self.abs_action.triggered.connect(self.take_abs_of_selected_feature)
@@ -696,7 +704,7 @@ class TableUI(QMainWindow, Styles):
696
704
 
697
705
  def collapse_pairs_in_neigh(self):
698
706
 
699
- self.selectNeighWidget = QWidget()
707
+ self.selectNeighWidget = CelldetectiveWidget()
700
708
  self.selectNeighWidget.setMinimumWidth(480)
701
709
  self.selectNeighWidget.setWindowTitle('Set neighborhood of interest')
702
710
 
@@ -757,14 +765,20 @@ class TableUI(QMainWindow, Styles):
757
765
  ref_pop = self.reference_pop_cb.currentText()
758
766
  neighborhood = self.neigh_cb.currentText()
759
767
  status_neigh = 'status_'+neighborhood
768
+
760
769
  if 'self' in neighborhood:
761
770
  neighbor_pop = ref_pop
762
- elif ref_pop=='targets':
763
- neighbor_pop = 'effectors'
764
- elif ref_pop=='effectors':
765
- neighbor_pop = "targets"
766
771
 
767
- data = extract_neighborhood_in_pair_table(self.data, neighborhood_key=neighborhood, contact_only=self.contact_only_check.isChecked())
772
+ neigh_col = neighborhood.replace('status_','')
773
+ if '_(' in neigh_col and ')_' in neigh_col:
774
+ neighbor_pop = neigh_col.split('_(')[-1].split(')_')[0].split('-')[-1]
775
+ else:
776
+ if ref_pop=='targets':
777
+ neighbor_pop = 'effectors'
778
+ if ref_pop=='effectors':
779
+ neighbor_pop = "targets"
780
+
781
+ data = extract_neighborhood_in_pair_table(self.data, neighborhood_key=neighborhood, contact_only=self.contact_only_check.isChecked(), reference_population=ref_pop)
768
782
 
769
783
  if self.groupby_pair_rb.isChecked():
770
784
  self.groupby_cols = ['position', 'REFERENCE_ID', 'NEIGHBOR_ID']
@@ -774,6 +788,7 @@ class TableUI(QMainWindow, Styles):
774
788
  self.current_data = data
775
789
  skip_projection = False
776
790
  if 'reference_tracked' in list(self.current_data.columns):
791
+ print(f"{self.current_data['reference_tracked']=} {(self.current_data['reference_tracked']==False)=} {np.all(self.current_data['reference_tracked']==False)=}")
777
792
  if np.all(self.current_data['reference_tracked'].astype(bool)==False):
778
793
  # reference not tracked
779
794
  if self.groupby_reference_rb.isChecked():
@@ -987,6 +1002,22 @@ class TableUI(QMainWindow, Styles):
987
1002
 
988
1003
  self.LogWidget = LogColWidget(self, selected_col)
989
1004
  self.LogWidget.show()
1005
+
1006
+ def merge_classification_features(self):
1007
+
1008
+ x = self.table_view.selectedIndexes()
1009
+ col_idx = np.unique(np.array([l.column() for l in x]))
1010
+
1011
+ col_selection = []
1012
+ if isinstance(col_idx, (list, np.ndarray)):
1013
+ cols = np.array(list(self.data.columns))
1014
+ if len(col_idx) > 0:
1015
+ selected_cols = cols[col_idx]
1016
+ col_selection.extend(selected_cols)
1017
+
1018
+ self.merge_classification_widget = MergeGroupWidget(self, columns = col_selection)
1019
+ self.merge_classification_widget.show()
1020
+
990
1021
 
991
1022
  def calibrate_selected_feature(self):
992
1023
 
@@ -1030,14 +1061,11 @@ class TableUI(QMainWindow, Styles):
1030
1061
 
1031
1062
  x = self.table_view.selectedIndexes()
1032
1063
  col_idx = np.unique(np.array([l.column() for l in x]))
1064
+ selected_cols = None
1033
1065
  if isinstance(col_idx, (list, np.ndarray)):
1034
1066
  cols = np.array(list(self.data.columns))
1035
1067
  if len(col_idx)>0:
1036
1068
  selected_col = str(cols[col_idx[0]])
1037
- else:
1038
- selected_col = None
1039
- else:
1040
- selected_col = None
1041
1069
 
1042
1070
  self.mergewidget = MergeOneHotWidget(self, selected_columns=selected_cols)
1043
1071
  self.mergewidget.show()
@@ -1091,7 +1119,7 @@ class TableUI(QMainWindow, Styles):
1091
1119
 
1092
1120
  self.current_data = self.data
1093
1121
 
1094
- self.projectionWidget = QWidget()
1122
+ self.projectionWidget = CelldetectiveWidget()
1095
1123
  self.projectionWidget.setMinimumWidth(500)
1096
1124
  self.projectionWidget.setWindowTitle('Set projection mode')
1097
1125
 
@@ -1191,7 +1219,7 @@ class TableUI(QMainWindow, Styles):
1191
1219
 
1192
1220
  def set_1D_plot_params(self):
1193
1221
 
1194
- self.plot1Dparams = QWidget()
1222
+ self.plot1Dparams = CelldetectiveWidget()
1195
1223
  self.plot1Dparams.setWindowTitle('Set 1D plot parameters')
1196
1224
 
1197
1225
  layout = QVBoxLayout()
File without changes
@@ -0,0 +1,118 @@
1
+ from typing import List
2
+
3
+ import numpy as np
4
+ from PyQt5.QtCore import QSize, Qt
5
+ from PyQt5.QtWidgets import QComboBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout
6
+ from fonticon_mdi6 import MDI6
7
+ from superqt.fonticon import icon
8
+
9
+ from celldetective.gui import CelldetectiveWidget
10
+ from celldetective.gui.gui_utils import PandasModel, center_window
11
+
12
+
13
+ class MergeGroupWidget(CelldetectiveWidget):
14
+ def __init__(self, parent_window, columns: List[str] = [], n_cols_init: int = 3):
15
+
16
+ super().__init__()
17
+ self.parent_window = parent_window
18
+
19
+ self.setWindowTitle("Merge classifications")
20
+ self.group_cols = [c for c in list(self.parent_window.data.columns) if c.startswith("group_") or c.startswith("status_")]
21
+ self.group_cols.insert(0, "--")
22
+ if len(columns) > n_cols_init:
23
+ n_cols_init = len(columns)
24
+
25
+ center_window(self)
26
+
27
+ layout = QVBoxLayout(self)
28
+ layout.setContentsMargins(30, 10, 30, 30)
29
+
30
+ label = QLabel(
31
+ "Merge several binary or multi-label classification features into a multi-label classification feature where each state is one of the possible combinations.\n")
32
+
33
+ label.setWordWrap(True) # enables automatic line breaking
34
+ label.setTextInteractionFlags(Qt.TextSelectableByMouse) # optional, to allow copy
35
+ label.setStyleSheet("color: gray;") # optional style
36
+
37
+ layout.addWidget(label)
38
+
39
+
40
+ self.name_le = QLineEdit("group_multilabel")
41
+ name_layout = QHBoxLayout()
42
+ name_layout.addWidget(QLabel("name: "), 25)
43
+ name_layout.addWidget(self.name_le, 75)
44
+ layout.addLayout(name_layout)
45
+
46
+ self.cbs_layout = QVBoxLayout()
47
+ self.cbs_layout.setContentsMargins(0,10,0,0)
48
+
49
+ self.cbs = []
50
+ for i in range(n_cols_init):
51
+ cb_i = QComboBox()
52
+ cb_i.addItems(self.group_cols)
53
+ if i < len(columns):
54
+ selection = columns[i]
55
+ idx = cb_i.findText(selection)
56
+ if idx>=0:
57
+ cb_i.setCurrentIndex(idx)
58
+ else:
59
+ cb_i.setCurrentIndex(0)
60
+ self.cbs.append(cb_i)
61
+
62
+ col_layout = QHBoxLayout()
63
+ col_layout.addWidget(QLabel(f'state {i}: '), 25)
64
+ col_layout.addWidget(cb_i, 75)
65
+ self.cbs_layout.addLayout(col_layout)
66
+
67
+ layout.addLayout(self.cbs_layout)
68
+
69
+ self.add_feature_btn = QPushButton()
70
+ self.add_feature_btn.setIcon(icon(MDI6.plus, color=self.help_color))
71
+ self.add_feature_btn.setIconSize(QSize(20, 20))
72
+ self.add_feature_btn.clicked.connect(self.add_col)
73
+ self.add_feature_btn.setStyleSheet(self.button_select_all)
74
+ layout.addWidget(self.add_feature_btn, alignment=Qt.AlignRight)
75
+
76
+ self.submit_btn = QPushButton('Compute')
77
+ self.submit_btn.setStyleSheet(self.button_style_sheet)
78
+ self.submit_btn.clicked.connect(self.compute)
79
+ layout.addWidget(self.submit_btn, 30)
80
+
81
+ self.setAttribute(Qt.WA_DeleteOnClose)
82
+
83
+ def add_col(self):
84
+ cb_i = QComboBox()
85
+ cb_i.addItems(self.group_cols)
86
+ self.cbs.append(cb_i)
87
+
88
+ col_layout = QHBoxLayout()
89
+ col_layout.addWidget(QLabel(f'state {len(self.cbs)}: '), 25)
90
+ col_layout.addWidget(cb_i, 75)
91
+
92
+ self.cbs_layout.addLayout(col_layout)
93
+
94
+ def compute(self):
95
+
96
+ cols_to_merge = [cb_i.currentText() for cb_i in self.cbs if cb_i.currentText() != "--"]
97
+ name = self.name_le.text()
98
+ if " " in name:
99
+ name.replace(" ","_")
100
+ if name == '':
101
+ name = "multilabel"
102
+ if not name.startswith("group_"):
103
+ name = "group_" + name
104
+
105
+ if len(cols_to_merge) > 1:
106
+ print("Computing a multi-label classification from the classification feature sources...")
107
+ bases = [int(self.parent_window.data[c].max()) + 1 for c in cols_to_merge]
108
+ multipliers = np.concatenate(([1], np.cumprod(bases[:-1])))
109
+ self.parent_window.data[name] = (self.parent_window.data[cols_to_merge] * multipliers).sum(axis=1)
110
+ self.parent_window.data.loc[self.parent_window.data[cols_to_merge].isna().any(axis=1), name] = np.nan
111
+
112
+ self.parent_window.model = PandasModel(self.parent_window.data)
113
+ self.parent_window.table_view.setModel(self.parent_window.model)
114
+ self.close()
115
+ elif len(cols_to_merge) == 1:
116
+ print("Only one classification feature was selected, nothing to merge...")
117
+ else:
118
+ print("No classification feature was selected...")