celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__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 (151) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +304 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +30 -15
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +232 -13
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.2.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,21 @@
1
- from PyQt5.QtWidgets import QApplication,QWidget, QMessageBox, QHBoxLayout, QFileDialog, QVBoxLayout, QScrollArea, QCheckBox, QGridLayout, QLabel, QLineEdit, QPushButton
1
+ from PyQt5.QtWidgets import (
2
+ QApplication,
3
+ QWidget,
4
+ QMessageBox,
5
+ QHBoxLayout,
6
+ QFileDialog,
7
+ QVBoxLayout,
8
+ QScrollArea,
9
+ QCheckBox,
10
+ QGridLayout,
11
+ QLabel,
12
+ QLineEdit,
13
+ QPushButton,
14
+ )
2
15
  from PyQt5.QtGui import QIntValidator, QDoubleValidator
3
- from celldetective.gui.gui_utils import center_window, help_generic
4
- from celldetective.utils import get_software_location
16
+ from celldetective.gui.gui_utils import help_generic
17
+ from celldetective.gui.base.utils import center_window
18
+ from celldetective import get_software_location
5
19
  import json
6
20
 
7
21
  from superqt import QLabeledSlider
@@ -11,682 +25,774 @@ from fonticon_mdi6 import MDI6
11
25
  from configparser import ConfigParser
12
26
  import os
13
27
  from functools import partial
28
+ import logging
14
29
  import numpy as np
15
- from celldetective.gui import CelldetectiveMainWindow, CelldetectiveWidget
30
+ from celldetective.gui.base.components import (
31
+ CelldetectiveMainWindow,
32
+ CelldetectiveWidget,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
16
36
 
17
37
 
18
38
  class ConfigNewExperiment(CelldetectiveMainWindow):
19
39
 
20
- def __init__(self, parent_window=None):
21
-
22
- super().__init__()
23
- self.parent_window = parent_window
24
- self.setWindowTitle("New experiment")
25
- center_window(self)
26
- self.setFixedWidth(500)
27
- self.setMaximumHeight(int(0.8*self.parent_window.screen_height))
28
- self.onlyFloat = QDoubleValidator()
29
-
30
- self.newExpFolder = str(QFileDialog.getExistingDirectory(self, 'Select directory'))
31
- self.populate_widget()
32
-
33
- def populate_widget(self):
34
-
35
- # Create button widget and layout
36
- self.scroll_area = QScrollArea(self)
37
- button_widget = CelldetectiveWidget()
38
- self.grid = QGridLayout()
39
- button_widget.setLayout(self.grid)
40
-
41
- self.grid.setContentsMargins(30, 30, 30, 30)
42
- self.grid.addWidget(QLabel("Folder:"), 0, 0, 1, 3)
43
- self.supFolder = QLineEdit()
44
- self.supFolder.setAlignment(Qt.AlignLeft)
45
- self.supFolder.setEnabled(True)
46
- self.supFolder.setText(self.newExpFolder)
47
- self.grid.addWidget(self.supFolder, 1, 0, 1, 1)
48
-
49
- self.browse_button = QPushButton("Browse...")
50
- self.browse_button.clicked.connect(self.browse_experiment_folder)
51
- self.browse_button.setIcon(icon(MDI6.folder, color="white"))
52
- self.browse_button.setStyleSheet(self.button_style_sheet)
53
- #self.browse_button.setIcon(QIcon_from_svg(abs_path+f"/icons/browse.svg", color='white'))
54
- self.grid.addWidget(self.browse_button, 1, 1, 1, 1)
55
-
56
- self.grid.addWidget(QLabel("Experiment name:"), 2, 0, 1, 3)
57
-
58
- self.expName = QLineEdit()
59
- self.expName.setPlaceholderText('folder_name_for_the_experiment')
60
- self.expName.setAlignment(Qt.AlignLeft)
61
- self.expName.setEnabled(True)
62
- self.expName.setFixedWidth(400)
63
- self.expName.setText("Untitled_Experiment")
64
- self.grid.addWidget(self.expName, 3, 0, 1, 3)
65
-
66
- self.generate_movie_settings()
67
- self.grid.addLayout(self.ms_grid, 29, 0, 1, 3)
68
-
69
- self.generate_channel_params_box()
70
- self.grid.addLayout(self.channel_grid, 30, 0, 1, 3)
71
-
72
- self.generate_population_params_box()
73
- self.grid.addLayout(self.population_grid, 31, 0, 1, 3)
74
-
75
- self.validate_button = QPushButton("Submit")
76
- self.validate_button.clicked.connect(self.create_config)
77
- self.validate_button.setStyleSheet(self.button_style_sheet)
78
- #self.validate_button.setIcon(QIcon_from_svg(abs_path+f"/icons/process.svg", color='white'))
79
-
80
- self.grid.addWidget(self.validate_button, 32, 0, 1, 3, alignment = Qt.AlignBottom)
81
- button_widget.adjustSize()
82
-
83
- self.scroll_area.setAlignment(Qt.AlignCenter)
84
- self.scroll_area.setWidget(button_widget)
85
- self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
86
- self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
87
- self.scroll_area.setWidgetResizable(True)
88
- self.setCentralWidget(self.scroll_area)
89
- self.show()
90
-
91
- QApplication.processEvents()
92
- self.adjustScrollArea()
93
-
94
- def adjustScrollArea(self):
95
-
96
- """
97
- Auto-adjust scroll area to fill space
98
- (from https://stackoverflow.com/questions/66417576/make-qscrollarea-use-all-available-space-of-qmainwindow-height-axis)
99
- """
100
-
101
- step = 5
102
- while self.scroll_area.verticalScrollBar().isVisible() and self.height() < self.maximumHeight():
103
- self.resize(self.width(), self.height() + step)
104
-
105
- def generate_movie_settings(self):
106
-
107
- """
108
- Parameters related to the movie parameters
109
- """
110
-
111
- onlyInt = QIntValidator()
112
- onlyInt.setRange(0, 100000)
113
-
114
- self.ms_grid = QGridLayout()
115
- self.ms_grid.setContentsMargins(21,30,20,30)
116
-
117
- ms_lbl = QLabel("MOVIE SETTINGS")
118
- ms_lbl.setStyleSheet("""
119
- font-weight: bold;
120
- """)
121
- self.ms_grid.addWidget(ms_lbl, 0,0,1,3, alignment=Qt.AlignCenter)
122
-
123
- self.number_of_wells = QLabel("Number of wells:")
124
- self.ms_grid.addWidget(self.number_of_wells, 1, 0, 1, 3)
125
-
126
- self.help_btn = QPushButton()
127
- self.help_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
128
- self.help_btn.setIconSize(QSize(20, 20))
129
- self.help_btn.clicked.connect(self.help_structure)
130
- self.help_btn.setStyleSheet(self.button_select_all)
131
- self.help_btn.setToolTip("Help.")
132
- self.ms_grid.addWidget(self.help_btn, 1, 0, 1, 3, alignment=Qt.AlignRight)
133
-
134
- self.SliderWells = QLabeledSlider(Qt.Horizontal, self)
135
- self.SliderWells.setMinimum(1)
136
- self.SliderWells.setMaximum(512)
137
- self.ms_grid.addWidget(self.SliderWells, 2, 0, 1, 3, alignment=Qt.AlignTop)
138
-
139
- self.number_of_positions = QLabel("Number of positions per well:")
140
- self.ms_grid.addWidget(self.number_of_positions, 3, 0, 1, 3)
141
-
142
- self.SliderPos = QLabeledSlider(Qt.Horizontal, self)
143
- self.SliderPos.setMinimum(1)
144
- self.SliderPos.setMaximum(512)
145
-
146
- self.ms_grid.addWidget(self.SliderPos, 4, 0, 1, 3)
147
-
148
- self.ms_grid.addWidget(QLabel("Calibration from pixel to µm:"), 5, 0, 1, 3)
149
- self.PxToUm_field = QLineEdit()
150
- self.PxToUm_field.setValidator(self.onlyFloat)
151
- self.PxToUm_field.setPlaceholderText('1 px = XXX µm')
152
- self.PxToUm_field.setAlignment(Qt.AlignLeft)
153
- self.PxToUm_field.setEnabled(True)
154
- self.PxToUm_field.setFixedWidth(400)
155
- self.PxToUm_field.setText("1,0")
156
- self.ms_grid.addWidget(self.PxToUm_field, 6, 0, 1, 3)
157
-
158
- self.ms_grid.addWidget(QLabel("Calibration from frame to minutes:"), 7, 0, 1, 3)
159
- self.FrameToMin_field = QLineEdit()
160
- self.FrameToMin_field.setAlignment(Qt.AlignLeft)
161
- self.FrameToMin_field.setEnabled(True)
162
- self.FrameToMin_field.setFixedWidth(400)
163
- self.FrameToMin_field.setValidator(self.onlyFloat)
164
- self.FrameToMin_field.setPlaceholderText('1 frame = XXX min')
165
- self.FrameToMin_field.setText("1,0")
166
- self.ms_grid.addWidget(self.FrameToMin_field, 8, 0, 1, 3)
167
-
168
- self.movie_length = QLabel("Number of frames:")
169
- self.movie_length.setToolTip('Optional: depending on how the movies are encoded, the automatic extraction of the number of frames can be difficult.\nThe software will then rely on this value.')
170
- self.ms_grid.addWidget(self.movie_length,9, 0, 1, 3)
171
- self.MovieLengthSlider = QLabeledSlider(Qt.Horizontal, self)
172
- self.MovieLengthSlider.setMinimum(1)
173
- #self.MovieLengthSlider.setMaximum(128)
174
- self.ms_grid.addWidget(self.MovieLengthSlider, 10, 0, 1, 3)
175
-
176
- self.prefix_lbl = QLabel("Prefix for the movies:")
177
- self.prefix_lbl.setToolTip('The stack file name must start with this prefix to be properly loaded.')
178
- self.ms_grid.addWidget(self.prefix_lbl, 11, 0, 1, 3)
179
- self.movie_prefix_field = QLineEdit()
180
- self.movie_prefix_field.setAlignment(Qt.AlignLeft)
181
- self.movie_prefix_field.setEnabled(True)
182
- self.movie_prefix_field.setFixedWidth(400)
183
- self.movie_prefix_field.setText("")
184
- self.ms_grid.addWidget(self.movie_prefix_field, 12, 0, 1, 3)
185
-
186
- self.ms_grid.addWidget(QLabel("Image width:"), 13, 0, 1, 3)
187
- self.shape_x_field = QLineEdit()
188
- self.shape_x_field.setValidator(onlyInt)
189
- self.shape_x_field.setAlignment(Qt.AlignLeft)
190
- self.shape_x_field.setEnabled(True)
191
- self.shape_x_field.setFixedWidth(400)
192
- self.shape_x_field.setText("2048")
193
- self.ms_grid.addWidget(self.shape_x_field, 14, 0, 1, 3)
194
-
195
- self.ms_grid.addWidget(QLabel("Image height:"), 15, 0, 1, 3)
196
- self.shape_y_field = QLineEdit()
197
- self.shape_y_field.setValidator(onlyInt)
198
- self.shape_y_field.setAlignment(Qt.AlignLeft)
199
- self.shape_y_field.setEnabled(True)
200
- self.shape_y_field.setFixedWidth(400)
201
- self.shape_y_field.setText("2048")
202
- self.ms_grid.addWidget(self.shape_y_field, 16, 0, 1, 3)
203
-
204
- def help_structure(self):
205
-
206
- """
207
- Helper to choose an experiment structure.
208
- """
209
-
210
- dict_path = os.sep.join([get_software_location(),'celldetective','gui','help','exp-structure.json'])
211
-
212
- with open(dict_path) as f:
213
- d = json.load(f)
214
-
215
- suggestion = help_generic(d)
216
- if isinstance(suggestion, str):
217
- print(f"{suggestion=}")
218
- msgBox = QMessageBox()
219
- msgBox.setIcon(QMessageBox.Information)
220
- msgBox.setTextFormat(Qt.RichText)
221
- msgBox.setText(suggestion+"\nSee <a href='https://celldetective.readthedocs.io/en/latest/get-started.html#data-organization'>the docs</a> for more information.")
222
- msgBox.setWindowTitle("Info")
223
- msgBox.setStandardButtons(QMessageBox.Ok)
224
- returnValue = msgBox.exec()
225
- if returnValue == QMessageBox.Ok:
226
- return None
227
-
228
- def generate_channel_params_box(self):
229
-
230
- """
231
- Parameters related to the movie channels
232
- Rewrite all of it
233
-
234
- """
235
-
236
- self.channel_grid = QGridLayout()
237
- self.channel_grid.setContentsMargins(21,30,20,30)
238
-
239
- channel_lbl = QLabel("CHANNELS")
240
- channel_lbl.setStyleSheet("""
241
- font-weight: bold;
242
- """)
243
- self.channel_grid.addWidget(channel_lbl, 0,0,1,3, alignment=Qt.AlignCenter)
244
-
245
-
246
- self.channels = ['brightfield', "live nuclei channel\n(Hoechst, NucSpot®)", "dead nuclei channel\n(PI)", "effector fluorescence\n(CFSE)",
247
- "adhesion\n(RICM, IRM)", "fluorescence miscellaneous 1\n(LAMP-1, Actin...)", "fluorescence miscellaneous 2\n(LAMP-1, Actin...)"]
248
- self.channel_mapping = ['brightfield_channel', 'live_nuclei_channel', 'dead_nuclei_channel', 'effector_fluo_channel',
249
- 'adhesion_channel', 'fluo_channel_1', 'fluo_channel_2']
250
- self.checkBoxes = [QCheckBox() for i in range(len(self.channels))]
251
- self.sliders = [QLabeledSlider(Qt.Orientation.Horizontal) for i in range(len(self.channels))]
252
-
253
- for i in range(len(self.channels)):
254
-
255
- self.checkBoxes[i].setText(self.channels[i])
256
- self.checkBoxes[i].toggled.connect(partial(self.show_slider, i))
257
-
258
- self.channel_grid.addWidget(self.checkBoxes[i], i+1, 0, 1, 1)
259
-
260
- self.sliders[i].setMinimum(0)
261
- self.sliders[i].setMaximum(len(self.channels))
262
- self.sliders[i].setEnabled(False)
263
-
264
- self.channel_grid.addWidget(self.sliders[i], i+1, 1, 1, 2, alignment = Qt.AlignRight)
265
-
266
- # Add channel button
267
- self.addChannelBtn = QPushButton('Add channel')
268
- self.addChannelBtn.setIcon(icon(MDI6.plus,color="white"))
269
- self.addChannelBtn.setIconSize(QSize(25, 25))
270
- self.addChannelBtn.setStyleSheet(self.button_style_sheet)
271
- self.addChannelBtn.clicked.connect(self.add_custom_channel)
272
- self.channel_grid.addWidget(self.addChannelBtn, 1000, 0, 1, 1)
273
-
274
- def add_custom_channel(self):
275
-
276
- self.CustomChannelWidget = CelldetectiveWidget()
277
- self.CustomChannelWidget.setWindowTitle("Custom channel")
278
- layout = QVBoxLayout()
279
- self.CustomChannelWidget.setLayout(layout)
280
-
281
- self.name_le = QLineEdit('custom_channel')
282
- hbox = QHBoxLayout()
283
- hbox.addWidget(QLabel('channel name: '), 33)
284
- hbox.addWidget(self.name_le, 66)
285
- layout.addLayout(hbox)
286
-
287
- self.createBtn = QPushButton('create')
288
- self.createBtn.setStyleSheet(self.button_style_sheet)
289
- self.createBtn.clicked.connect(self.write_custom_channel)
290
- layout.addWidget(self.createBtn)
291
- center_window(self.CustomChannelWidget)
292
- self.CustomChannelWidget.show()
293
-
294
- print('new channel added')
295
-
296
- def write_custom_channel(self):
297
-
298
- self.new_channel_name = self.name_le.text()
299
- name_map = self.new_channel_name
300
- name_map = name_map.replace('_channel','')
301
- name_map = name_map.replace('channel','')
302
- name_map = name_map.replace(' ','')
303
- if not name_map.endswith('_channel'):
304
- name_map += '_channel'
305
-
306
- self.channels.append(self.new_channel_name)
307
- self.channel_mapping.append(name_map)
308
- self.checkBoxes.append(QCheckBox())
309
- self.sliders.append(QLabeledSlider(Qt.Orientation.Horizontal))
310
- self.CustomChannelWidget.close()
311
-
312
- self.checkBoxes[-1].setText(self.channels[-1])
313
- self.checkBoxes[-1].toggled.connect(partial(self.show_slider, len(self.channels)-1))
314
- self.channel_grid.addWidget(self.checkBoxes[-1], len(self.channels)+1, 0, 1, 1)
315
-
316
- self.sliders[-1].setMinimum(0)
317
- for i in range(len(self.channels)):
318
- self.sliders[i].setMaximum(len(self.channels))
319
- self.sliders[-1].setEnabled(False)
320
- self.channel_grid.addWidget(self.sliders[-1], len(self.channels)+1, 1, 1, 2, alignment = Qt.AlignRight)
321
-
322
- def show_slider(self, index):
323
- if self.checkBoxes[index].isChecked():
324
- self.sliders[index].setEnabled(True)
325
- else:
326
- self.sliders[index].setEnabled(False)
327
-
328
- def generate_population_params_box(self):
329
-
330
- """
331
- Parameters related to the movie channels
332
- Rewrite all of it
333
-
334
- """
335
-
336
- self.population_grid = QGridLayout()
337
- self.population_grid.setContentsMargins(21,30,20,30)
338
-
339
- pop_lbl = QLabel("CELL POPULATIONS")
340
- pop_lbl.setStyleSheet("""
341
- font-weight: bold;
342
- """)
343
- self.population_grid.addWidget(pop_lbl, 0,0,1,3, alignment=Qt.AlignCenter)
344
-
345
-
346
- self.populations = ['effectors','targets']
347
- self.population_checkboxes = [QCheckBox() for i in range(len(self.populations))]
348
-
349
- for i in range(len(self.populations)):
350
-
351
- self.population_checkboxes[i].setText(self.populations[i])
352
- self.population_checkboxes[i].setChecked(True)
353
- self.population_grid.addWidget(self.population_checkboxes[i], i+1, 0, 1, 1)
354
-
355
- # Add channel button
356
- self.addPopBtn = QPushButton('Add a cell population')
357
- self.addPopBtn.setIcon(icon(MDI6.plus,color="white"))
358
- self.addPopBtn.setIconSize(QSize(25, 25))
359
- self.addPopBtn.setStyleSheet(self.button_style_sheet)
360
- self.addPopBtn.clicked.connect(self.add_custom_population)
361
- self.population_grid.addWidget(self.addPopBtn, 1000, 0, 1, 1)
362
-
363
-
364
- def add_custom_population(self):
365
- self.CustomPopWidget = CelldetectiveWidget()
366
- self.CustomPopWidget.setWindowTitle("Define custom population")
367
- layout = QVBoxLayout()
368
- self.CustomPopWidget.setLayout(layout)
369
-
370
- self.name_le = QLineEdit()
371
- self.name_le.setPlaceholderText('name')
372
- self.name_le.textChanged.connect(self.check_population_name)
373
- hbox = QHBoxLayout()
374
- hbox.addWidget(QLabel('population name: '), 33)
375
- hbox.addWidget(self.name_le, 66)
376
- layout.addLayout(hbox)
377
-
378
- self.addPopBtn = QPushButton('add')
379
- self.addPopBtn.setStyleSheet(self.button_style_sheet)
380
- self.addPopBtn.setEnabled(False)
381
- self.addPopBtn.clicked.connect(self.write_custom_population)
382
- layout.addWidget(self.addPopBtn)
383
- center_window(self.CustomPopWidget)
384
- self.CustomPopWidget.show()
385
-
386
- def check_population_name(self, text):
387
- # define all conditions for valid population name (like no space)
388
- if len(text)>0 and ' ' not in text:
389
- self.addPopBtn.setEnabled(True)
390
- else:
391
- self.addPopBtn.setEnabled(False)
392
-
393
- def write_custom_population(self):
394
-
395
- self.new_population_name = self.name_le.text()
396
- name_map = self.new_population_name
397
-
398
- self.populations.append(self.new_population_name)
399
- self.population_checkboxes.append(QCheckBox())
400
- self.CustomPopWidget.close()
401
-
402
- self.population_checkboxes[-1].setText(self.populations[-1])
403
- self.population_grid.addWidget(self.population_checkboxes[-1], len(self.populations)+1, 0, 1, 1)
404
-
405
- def browse_experiment_folder(self):
406
-
407
- """
408
- Set a new base directory.
409
- """
410
-
411
- self.newExpFolder = str(QFileDialog.getExistingDirectory(self, 'Select directory'))
412
- self.supFolder.setText(self.newExpFolder)
413
-
414
- def create_config(self):
415
-
416
- """
417
- Create the folder tree, the config, issue a warning if the experiment folder already exists.
418
- """
419
-
420
- channel_indices = []
421
- for i in range(len(self.channels)):
422
- if self.checkBoxes[i].isChecked():
423
- channel_indices.append(self.sliders[i].value())
424
- if not channel_indices:
425
- msgBox = QMessageBox()
426
- msgBox.setIcon(QMessageBox.Warning)
427
- msgBox.setText("Please set at least one channel before proceeding.")
428
- msgBox.setWindowTitle("Warning")
429
- msgBox.setStandardButtons(QMessageBox.Ok)
430
- returnValue = msgBox.exec()
431
- if returnValue == QMessageBox.Ok:
432
- return None
433
-
434
- if len(channel_indices) != len(set(channel_indices)):
435
- msgBox = QMessageBox()
436
- msgBox.setIcon(QMessageBox.Warning)
437
- msgBox.setText("Some channel indices are repeated. Please check your configuration.")
438
- msgBox.setWindowTitle("Warning")
439
- msgBox.setStandardButtons(QMessageBox.Ok)
440
- returnValue = msgBox.exec()
441
- if returnValue == QMessageBox.Ok:
442
- return None
443
-
444
- populations_checked = [self.population_checkboxes[i].isChecked() for i in range(len(self.population_checkboxes))]
445
- if not np.any(populations_checked):
446
- msgBox = QMessageBox()
447
- msgBox.setIcon(QMessageBox.Warning)
448
- msgBox.setText("Please set at least one cell population before proceeding...")
449
- msgBox.setWindowTitle("Warning")
450
- msgBox.setStandardButtons(QMessageBox.Ok)
451
- returnValue = msgBox.exec()
452
- if returnValue == QMessageBox.Ok:
453
- return None
454
-
455
- sorted_list = set(channel_indices)
456
- expected_list = set(np.arange(max(sorted_list)+1))
457
- print(sorted_list, expected_list, sorted_list==expected_list)
458
-
459
- if not sorted_list==expected_list:
460
- msgBox = QMessageBox()
461
- msgBox.setIcon(QMessageBox.Warning)
462
- msgBox.setText("There is a gap in your channel indices. Please check your configuration.")
463
- msgBox.setWindowTitle("Warning")
464
- msgBox.setStandardButtons(QMessageBox.Ok)
465
- returnValue = msgBox.exec()
466
- if returnValue == QMessageBox.Ok:
467
- return None
468
-
469
- try:
470
-
471
- folder = self.supFolder.text()
472
- folder = folder.replace('\\','/')
473
- folder = rf"{folder}"
474
-
475
- name = str(self.expName.text())
476
- name = name.replace(' ','')
477
-
478
- self.directory = os.sep.join([folder,name])
479
- os.mkdir(self.directory)
480
- os.chdir(self.directory)
481
- self.create_subfolders()
482
- self.annotate_wells()
483
-
484
- except FileExistsError:
485
- msgBox = QMessageBox()
486
- msgBox.setIcon(QMessageBox.Warning)
487
- msgBox.setText("This experiment already exists... Please select another name.")
488
- msgBox.setWindowTitle("Warning")
489
- msgBox.setStandardButtons(QMessageBox.Ok)
490
- returnValue = msgBox.exec()
491
- if returnValue == QMessageBox.Ok:
492
- return None
493
-
494
- def create_subfolders(self):
495
-
496
- """
497
- Create the well folders and position folders within the wells.
498
- """
499
-
500
- self.nbr_wells = self.SliderWells.value()
501
- self.nbr_positions = self.SliderPos.value()
502
- for k in range(self.nbr_wells):
503
- well_name = f"W{k+1}"+os.sep
504
- os.mkdir(well_name)
505
- for p in range(self.nbr_positions):
506
- position_name = well_name+f"{k+1}0{p}"+os.sep
507
- os.mkdir(position_name)
508
- os.mkdir(position_name+os.sep+"movie"+os.sep)
509
-
510
- def annotate_wells(self):
511
- self.w = SetupConditionLabels(self, self.nbr_wells)
512
- self.w.show()
513
-
514
-
515
- def create_config_file(self):
516
-
517
- """
518
-
519
- Write all user input parameters to a configuration file associated to an experiment.
520
- """
521
-
522
-
523
- config = ConfigParser(interpolation=None)
524
-
525
- config.add_section('Populations')
526
- pops = ','.join([self.populations[i].lower() for i in range(len(self.population_checkboxes)) if self.population_checkboxes[i].isChecked()])
527
- config.set('Populations','populations', pops)
528
-
529
- # add a new section and some values
530
- config.add_section('MovieSettings')
531
- config.set('MovieSettings', 'PxToUm', self.PxToUm_field.text().replace(',','.'))
532
- config.set('MovieSettings', 'FrameToMin', self.FrameToMin_field.text().replace(',','.'))
533
- config.set('MovieSettings', 'len_movie', str(self.MovieLengthSlider.value()))
534
- config.set('MovieSettings', 'shape_x', self.shape_x_field.text())
535
- config.set('MovieSettings', 'shape_y', self.shape_y_field.text())
536
- config.set('MovieSettings', 'movie_prefix', self.movie_prefix_field.text())
537
-
538
- config.add_section('Channels')
539
- for i in range(len(self.channels)):
540
- if self.checkBoxes[i].isChecked():
541
- config.set('Channels', self.channel_mapping[i], str(self.sliders[i].value()))
542
- else:
543
- config.set('Channels', self.channel_mapping[i], "nan")
544
-
545
- config.add_section('Labels')
546
- config.set('Labels', 'cell_types', self.cell_types)
547
- config.set('Labels', 'antibodies', self.antibodies)
548
- config.set('Labels', 'concentrations', self.concentrations)
549
- config.set('Labels', 'pharmaceutical_agents', self.pharmaceutical_agents)
550
-
551
- config.add_section('Metadata')
552
- config.set('Metadata', 'concentration_units', self.concentration_units)
553
-
554
-
555
- # save to a file
556
- with open('config.ini', 'w') as configfile:
557
- config.write(configfile)
558
-
559
- self.parent_window.set_experiment_path(self.directory)
560
- print(f'New experiment successfully configured in folder {self.directory}...')
561
- self.close()
40
+ def __init__(self, parent_window=None):
562
41
 
563
- class SetupConditionLabels(CelldetectiveWidget):
564
- def __init__(self, parent_window, n_wells):
565
- super().__init__()
566
- self.parent_window = parent_window
567
- self.n_wells = n_wells
568
- self.setWindowTitle("Well conditions")
569
-
570
- # --- Outer layout ---
571
- self.outer_layout = QVBoxLayout(self)
572
- self.outer_layout.setContentsMargins(10, 10, 10, 10)
573
-
574
- # --- Scroll area ---
575
- self.scroll = QScrollArea(self)
576
- self.scroll.setWidgetResizable(True)
577
- self.outer_layout.addWidget(self.scroll, stretch=1) # takes most space
578
-
579
- # Container inside scroll
580
- self.container = QWidget()
581
- self.scroll.setWidget(self.container)
582
-
583
- # Content layout (scrollable part)
584
- self.layout = QVBoxLayout(self.container)
585
- self.layout.setContentsMargins(30, 30, 30, 30)
586
-
587
- self.onlyFloat = QDoubleValidator()
588
- self.populate()
589
-
590
- self.concentration_units_le = QLineEdit('pM')
591
- self.concentration_units_le.setPlaceholderText('concentration units')
592
-
593
- concentration_units_layout = QHBoxLayout()
594
- concentration_units_layout.addWidget(QLabel('concentration\nunits: '), 5, alignment=Qt.AlignLeft)
595
- concentration_units_layout.addWidget(self.concentration_units_le, 10)
596
- concentration_units_layout.addWidget(QLabel(''), 85)
597
- self.outer_layout.addLayout(concentration_units_layout)
598
-
599
- # --- Fixed button row (not scrollable) ---
600
- btn_hbox = QHBoxLayout()
601
- btn_hbox.setContentsMargins(0, 15, 0, 0)
602
-
603
- self.skip_btn = QPushButton('Skip')
604
- self.skip_btn.setStyleSheet(self.button_style_sheet_2)
605
- self.skip_btn.clicked.connect(self.set_default_values)
606
- btn_hbox.addWidget(self.skip_btn)
607
-
608
- self.submit_btn = QPushButton('Submit')
609
- self.submit_btn.setStyleSheet(self.button_style_sheet)
610
- self.submit_btn.clicked.connect(self.set_user_values)
611
- btn_hbox.addWidget(self.submit_btn)
612
-
613
- self.outer_layout.addLayout(btn_hbox) # outside scroll
614
- self.setMinimumWidth(int(0.6 * self.parent_window.parent_window.screen_width))
615
-
616
- center_window(self)
617
-
618
- def populate(self):
619
- self.cell_type_cbs = [QLineEdit() for i in range(self.n_wells)]
620
- self.antibodies_cbs = [QLineEdit() for i in range(self.n_wells)]
621
- self.concentrations_cbs = [QLineEdit() for i in range(self.n_wells)]
622
- self.pharmaceutical_agents_cbs = [QLineEdit() for i in range(self.n_wells)]
623
-
624
- for i in range(self.n_wells):
625
- hbox = QHBoxLayout()
626
- hbox.setContentsMargins(15, 5, 15, 5)
627
- hbox.addWidget(QLabel(f'well {i+1}'), 5, alignment=Qt.AlignLeft)
628
- hbox.addWidget(QLabel('cell type: '), 5)
629
- hbox.addWidget(self.cell_type_cbs[i], 10)
630
- self.cell_type_cbs[i].setPlaceholderText('e.g. T-cell, NK')
631
-
632
- hbox.addWidget(QLabel('antibody: '), 5)
633
- hbox.addWidget(self.antibodies_cbs[i], 10)
634
- self.antibodies_cbs[i].setPlaceholderText('e.g. anti-CD4')
635
-
636
- hbox.addWidget(QLabel('concentration: '), 5)
637
- hbox.addWidget(self.concentrations_cbs[i], 10)
638
- self.concentrations_cbs[i].setPlaceholderText('e.g. 100 (pM)')
639
- self.concentrations_cbs[i].setValidator(self.onlyFloat)
640
-
641
- hbox.addWidget(QLabel('pharmaceutical agents: '), 5)
642
- hbox.addWidget(self.pharmaceutical_agents_cbs[i], 10)
643
- self.pharmaceutical_agents_cbs[i].setPlaceholderText('e.g. dextran')
644
-
645
- self.layout.addLayout(hbox)
646
-
647
-
648
- def set_default_values(self):
649
-
650
- for i in range(self.n_wells):
651
- self.cell_type_cbs[i].setText(str(i))
652
- self.antibodies_cbs[i].setText(str(i))
653
- self.concentrations_cbs[i].setText(str(i))
654
- self.pharmaceutical_agents_cbs[i].setText(str(i))
655
- self.set_attributes()
656
- self.parent_window.create_config_file()
657
- self.close()
658
-
659
- def set_user_values(self):
660
- for i in range(self.n_wells):
661
- if self.cell_type_cbs[i].text()=='':
662
- self.cell_type_cbs[i].setText(str(i))
663
- if self.antibodies_cbs[i].text()=='':
664
- self.antibodies_cbs[i].setText(str(i))
665
- if self.concentrations_cbs[i].text()=='':
666
- self.concentrations_cbs[i].setText(str(i))
667
- if self.pharmaceutical_agents_cbs[i].text()=='':
668
- self.pharmaceutical_agents_cbs[i].setText(str(i))
669
- self.set_attributes()
670
- self.parent_window.create_config_file()
671
- self.close()
672
-
673
- def set_attributes(self):
674
-
675
- cell_type_text = [c.text() for c in self.cell_type_cbs]
676
- self.parent_window.cell_types = ','.join(cell_type_text)
677
-
678
- antibodies_text = [c.text() for c in self.antibodies_cbs]
679
- self.parent_window.antibodies = ','.join(antibodies_text)
680
-
681
- concentrations_text = [c.text() for c in self.concentrations_cbs]
682
- self.parent_window.concentrations = ','.join(concentrations_text)
683
-
684
- pharamaceutical_text = [c.text() for c in self.pharmaceutical_agents_cbs]
685
- self.parent_window.pharmaceutical_agents = ','.join(pharamaceutical_text)
686
-
687
- self.parent_window.concentration_units = self.concentration_units_le.text()
42
+ super().__init__()
43
+ self.parent_window = parent_window
44
+ self.setWindowTitle("New experiment")
45
+ self.setFixedWidth(500)
46
+ self.setMaximumHeight(int(0.8 * self.parent_window.screen_height))
47
+ self.onlyFloat = QDoubleValidator()
48
+ self.newExpFolder = str(
49
+ QFileDialog.getExistingDirectory(self, "Select directory")
50
+ )
51
+ self.init_widgets()
52
+ self.add_to_layout()
53
+ self.connect_signals()
54
+ QApplication.processEvents()
55
+ self.adjustScrollArea()
56
+
57
+ def init_widgets(self):
58
+ self.scroll_area = QScrollArea(self)
59
+ self.grid = QGridLayout()
60
+
61
+ self.supFolder = QLineEdit()
62
+ self.supFolder.setAlignment(Qt.AlignLeft)
63
+ self.supFolder.setEnabled(True)
64
+ self.supFolder.setText(self.newExpFolder)
65
+
66
+ self.browse_button = QPushButton("Browse...")
67
+ self.browse_button.setIcon(icon(MDI6.folder, color="white"))
68
+ self.browse_button.setStyleSheet(self.button_style_sheet)
69
+
70
+ self.expName = QLineEdit()
71
+ self.expName.setPlaceholderText("folder_name_for_the_experiment")
72
+ self.expName.setAlignment(Qt.AlignLeft)
73
+ self.expName.setEnabled(True)
74
+ self.expName.setFixedWidth(400)
75
+ self.expName.setText("Untitled_Experiment")
76
+
77
+ self.generate_movie_settings()
78
+
79
+ self.validate_button = QPushButton("Submit")
80
+ self.validate_button.setStyleSheet(self.button_style_sheet)
81
+
82
+ def connect_signals(self):
83
+ self.browse_button.clicked.connect(self.browse_experiment_folder)
84
+ self.validate_button.clicked.connect(self.create_config)
85
+
86
+ def add_to_layout(self):
87
+
88
+ button_widget = CelldetectiveWidget()
89
+ button_widget.setLayout(self.grid)
90
+
91
+ self.grid.setContentsMargins(30, 30, 30, 30)
92
+ self.grid.addWidget(QLabel("Folder:"), 0, 0, 1, 3)
688
93
 
94
+ self.grid.addWidget(self.supFolder, 1, 0, 1, 1)
689
95
 
96
+ self.grid.addWidget(self.browse_button, 1, 1, 1, 1)
690
97
 
98
+ self.grid.addWidget(QLabel("Experiment name:"), 2, 0, 1, 3)
691
99
 
100
+ self.grid.addWidget(self.expName, 3, 0, 1, 3)
692
101
 
102
+ self.grid.addLayout(self.ms_grid, 29, 0, 1, 3)
103
+
104
+ self.generate_channel_params_box()
105
+ self.grid.addLayout(self.channel_grid, 30, 0, 1, 3)
106
+
107
+ self.generate_population_params_box()
108
+ self.grid.addLayout(self.population_grid, 31, 0, 1, 3)
109
+
110
+ self.grid.addWidget(self.validate_button, 32, 0, 1, 3, alignment=Qt.AlignBottom)
111
+ button_widget.adjustSize()
112
+
113
+ self.scroll_area.setAlignment(Qt.AlignCenter)
114
+ self.scroll_area.setWidget(button_widget)
115
+ self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
116
+ self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
117
+ self.scroll_area.setWidgetResizable(True)
118
+ self.setCentralWidget(self.scroll_area)
119
+ self.show()
120
+
121
+ def adjustScrollArea(self):
122
+ """
123
+ Auto-adjust scroll area to fill space
124
+ (from https://stackoverflow.com/questions/66417576/make-qscrollarea-use-all-available-space-of-qmainwindow-height-axis)
125
+ """
126
+
127
+ step = 5
128
+ while (
129
+ self.scroll_area.verticalScrollBar().isVisible()
130
+ and self.height() < self.maximumHeight()
131
+ ):
132
+ self.resize(self.width(), self.height() + step)
133
+
134
+ def generate_movie_settings(self):
135
+ """
136
+ Parameters related to the movie parameters
137
+ """
138
+
139
+ onlyInt = QIntValidator()
140
+ onlyInt.setRange(0, 100000)
141
+
142
+ self.ms_grid = QGridLayout()
143
+ self.ms_grid.setContentsMargins(21, 30, 20, 30)
144
+
145
+ ms_lbl = QLabel("SETTINGS")
146
+ ms_lbl.setStyleSheet(
147
+ """
148
+ font-weight: bold;
149
+ """
150
+ )
151
+ self.ms_grid.addWidget(ms_lbl, 0, 0, 1, 3, alignment=Qt.AlignCenter)
152
+
153
+ self.number_of_wells = QLabel("Number of wells:")
154
+ self.ms_grid.addWidget(self.number_of_wells, 1, 0, 1, 3)
155
+
156
+ self.help_btn = QPushButton()
157
+ self.help_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
158
+ self.help_btn.setIconSize(QSize(20, 20))
159
+ self.help_btn.clicked.connect(self.help_structure)
160
+ self.help_btn.setStyleSheet(self.button_select_all)
161
+ self.help_btn.setToolTip("Help.")
162
+ self.ms_grid.addWidget(self.help_btn, 1, 0, 1, 3, alignment=Qt.AlignRight)
163
+
164
+ self.SliderWells = QLabeledSlider(Qt.Horizontal, self)
165
+ self.SliderWells.setMinimum(1)
166
+ self.SliderWells.setMaximum(512)
167
+ self.ms_grid.addWidget(self.SliderWells, 2, 0, 1, 3, alignment=Qt.AlignTop)
168
+
169
+ self.number_of_positions = QLabel("Number of positions per well:")
170
+ self.ms_grid.addWidget(self.number_of_positions, 3, 0, 1, 3)
171
+
172
+ self.SliderPos = QLabeledSlider(Qt.Horizontal, self)
173
+ self.SliderPos.setMinimum(1)
174
+ self.SliderPos.setMaximum(512)
175
+
176
+ self.ms_grid.addWidget(self.SliderPos, 4, 0, 1, 3)
177
+
178
+ self.ms_grid.addWidget(QLabel("Calibration from pixel to µm:"), 5, 0, 1, 3)
179
+ self.PxToUm_field = QLineEdit()
180
+ self.PxToUm_field.setValidator(self.onlyFloat)
181
+ self.PxToUm_field.setPlaceholderText("1 px = XXX µm")
182
+ self.PxToUm_field.setAlignment(Qt.AlignLeft)
183
+ self.PxToUm_field.setEnabled(True)
184
+ self.PxToUm_field.setFixedWidth(400)
185
+ self.PxToUm_field.setText("1,0")
186
+ self.ms_grid.addWidget(self.PxToUm_field, 6, 0, 1, 3)
187
+
188
+ self.ms_grid.addWidget(QLabel("Calibration from frame to minutes:"), 7, 0, 1, 3)
189
+ self.FrameToMin_field = QLineEdit()
190
+ self.FrameToMin_field.setAlignment(Qt.AlignLeft)
191
+ self.FrameToMin_field.setEnabled(True)
192
+ self.FrameToMin_field.setFixedWidth(400)
193
+ self.FrameToMin_field.setValidator(self.onlyFloat)
194
+ self.FrameToMin_field.setPlaceholderText("1 frame = XXX min")
195
+ self.FrameToMin_field.setText("1,0")
196
+ self.ms_grid.addWidget(self.FrameToMin_field, 8, 0, 1, 3)
197
+
198
+ self.movie_length = QLabel("Number of frames:")
199
+ self.movie_length.setToolTip(
200
+ "Optional: depending on how the movies are encoded, the automatic extraction of the number of frames can be difficult.\nThe software will then rely on this value."
201
+ )
202
+ self.ms_grid.addWidget(self.movie_length, 9, 0, 1, 3)
203
+ self.MovieLengthSlider = QLabeledSlider(Qt.Horizontal, self)
204
+ self.MovieLengthSlider.setMinimum(1)
205
+ # self.MovieLengthSlider.setMaximum(128)
206
+ self.ms_grid.addWidget(self.MovieLengthSlider, 10, 0, 1, 3)
207
+
208
+ self.prefix_lbl = QLabel("Prefix for the movies:")
209
+ self.prefix_lbl.setToolTip(
210
+ "The stack file name must start with this prefix to be properly loaded."
211
+ )
212
+ self.ms_grid.addWidget(self.prefix_lbl, 11, 0, 1, 3)
213
+ self.movie_prefix_field = QLineEdit()
214
+ self.movie_prefix_field.setAlignment(Qt.AlignLeft)
215
+ self.movie_prefix_field.setEnabled(True)
216
+ self.movie_prefix_field.setFixedWidth(400)
217
+ self.movie_prefix_field.setText("")
218
+ self.ms_grid.addWidget(self.movie_prefix_field, 12, 0, 1, 3)
219
+
220
+ self.ms_grid.addWidget(QLabel("Image width:"), 13, 0, 1, 3)
221
+ self.shape_x_field = QLineEdit()
222
+ self.shape_x_field.setValidator(onlyInt)
223
+ self.shape_x_field.setAlignment(Qt.AlignLeft)
224
+ self.shape_x_field.setEnabled(True)
225
+ self.shape_x_field.setFixedWidth(400)
226
+ self.shape_x_field.setText("2048")
227
+ self.ms_grid.addWidget(self.shape_x_field, 14, 0, 1, 3)
228
+
229
+ self.ms_grid.addWidget(QLabel("Image height:"), 15, 0, 1, 3)
230
+ self.shape_y_field = QLineEdit()
231
+ self.shape_y_field.setValidator(onlyInt)
232
+ self.shape_y_field.setAlignment(Qt.AlignLeft)
233
+ self.shape_y_field.setEnabled(True)
234
+ self.shape_y_field.setFixedWidth(400)
235
+ self.shape_y_field.setText("2048")
236
+ self.ms_grid.addWidget(self.shape_y_field, 16, 0, 1, 3)
237
+
238
+ def help_structure(self):
239
+ """
240
+ Helper to choose an experiment structure.
241
+ """
242
+
243
+ dict_path = os.sep.join(
244
+ [
245
+ get_software_location(),
246
+ "celldetective",
247
+ "gui",
248
+ "help",
249
+ "exp-structure.json",
250
+ ]
251
+ )
252
+
253
+ with open(dict_path) as f:
254
+ d = json.load(f)
255
+
256
+ suggestion = help_generic(d)
257
+ if isinstance(suggestion, str):
258
+ logger.info(f"{suggestion=}")
259
+ msgBox = QMessageBox()
260
+ msgBox = QMessageBox()
261
+ msgBox.setIcon(QMessageBox.Information)
262
+ msgBox.setTextFormat(Qt.RichText)
263
+ msgBox.setText(
264
+ suggestion
265
+ + "\nSee <a href='https://celldetective.readthedocs.io/en/latest/get-started.html#data-organization'>the docs</a> for more information."
266
+ )
267
+ msgBox.setWindowTitle("Info")
268
+ msgBox.setStandardButtons(QMessageBox.Ok)
269
+ returnValue = msgBox.exec()
270
+ if returnValue == QMessageBox.Ok:
271
+ return None
272
+
273
+ def generate_channel_params_box(self):
274
+ """
275
+ Parameters related to the movie channels
276
+ Rewrite all of it
277
+
278
+ """
279
+
280
+ self.channel_grid = QGridLayout()
281
+ self.channel_grid.setContentsMargins(21, 30, 20, 30)
282
+
283
+ channel_lbl = QLabel("CHANNELS")
284
+ channel_lbl.setStyleSheet(
285
+ """
286
+ font-weight: bold;
287
+ """
288
+ )
289
+ self.channel_grid.addWidget(channel_lbl, 0, 0, 1, 3, alignment=Qt.AlignCenter)
290
+
291
+ self.channels = [
292
+ "brightfield",
293
+ "live nuclei channel\n(Hoechst, NucSpot®)",
294
+ "dead nuclei channel\n(PI)",
295
+ "effector fluorescence\n(CFSE)",
296
+ "adhesion\n(RICM, IRM)",
297
+ "fluorescence miscellaneous 1\n(LAMP-1, Actin...)",
298
+ "fluorescence miscellaneous 2\n(LAMP-1, Actin...)",
299
+ ]
300
+ self.channel_mapping = [
301
+ "brightfield_channel",
302
+ "live_nuclei_channel",
303
+ "dead_nuclei_channel",
304
+ "effector_fluo_channel",
305
+ "adhesion_channel",
306
+ "fluo_channel_1",
307
+ "fluo_channel_2",
308
+ ]
309
+ self.checkBoxes = [QCheckBox() for i in range(len(self.channels))]
310
+ self.sliders = [
311
+ QLabeledSlider(Qt.Orientation.Horizontal) for i in range(len(self.channels))
312
+ ]
313
+
314
+ for i in range(len(self.channels)):
315
+
316
+ self.checkBoxes[i].setText(self.channels[i])
317
+ self.checkBoxes[i].toggled.connect(partial(self.show_slider, i))
318
+
319
+ self.channel_grid.addWidget(self.checkBoxes[i], i + 1, 0, 1, 1)
320
+
321
+ self.sliders[i].setMinimum(0)
322
+ self.sliders[i].setMaximum(len(self.channels))
323
+ self.sliders[i].setEnabled(False)
324
+
325
+ self.channel_grid.addWidget(
326
+ self.sliders[i], i + 1, 1, 1, 2, alignment=Qt.AlignRight
327
+ )
328
+
329
+ # Add channel button
330
+ self.addChannelBtn = QPushButton("Add channel")
331
+ self.addChannelBtn.setIcon(icon(MDI6.plus, color="white"))
332
+ self.addChannelBtn.setIconSize(QSize(25, 25))
333
+ self.addChannelBtn.setStyleSheet(self.button_style_sheet)
334
+ self.addChannelBtn.clicked.connect(self.add_custom_channel)
335
+ self.channel_grid.addWidget(self.addChannelBtn, 1000, 0, 1, 1)
336
+
337
+ def add_custom_channel(self):
338
+
339
+ self.CustomChannelWidget = CelldetectiveWidget()
340
+ self.CustomChannelWidget.setWindowTitle("Custom channel")
341
+ layout = QVBoxLayout()
342
+ self.CustomChannelWidget.setLayout(layout)
343
+
344
+ self.name_le = QLineEdit("custom_channel")
345
+ hbox = QHBoxLayout()
346
+ hbox.addWidget(QLabel("channel name: "), 33)
347
+ hbox.addWidget(self.name_le, 66)
348
+ layout.addLayout(hbox)
349
+
350
+ self.createBtn = QPushButton("create")
351
+ self.createBtn.setStyleSheet(self.button_style_sheet)
352
+ self.createBtn.clicked.connect(self.write_custom_channel)
353
+ layout.addWidget(self.createBtn)
354
+ center_window(self.CustomChannelWidget)
355
+ self.CustomChannelWidget.show()
356
+ logger.info("new channel added")
357
+
358
+ def write_custom_channel(self):
359
+
360
+ self.new_channel_name = self.name_le.text()
361
+ name_map = self.new_channel_name
362
+ name_map = name_map.replace("_channel", "")
363
+ name_map = name_map.replace("channel", "")
364
+ name_map = name_map.replace(" ", "")
365
+ if not name_map.endswith("_channel"):
366
+ name_map += "_channel"
367
+
368
+ self.channels.append(self.new_channel_name)
369
+ self.channel_mapping.append(name_map)
370
+ self.checkBoxes.append(QCheckBox())
371
+ self.sliders.append(QLabeledSlider(Qt.Orientation.Horizontal))
372
+ self.CustomChannelWidget.close()
373
+
374
+ self.checkBoxes[-1].setText(self.channels[-1])
375
+ self.checkBoxes[-1].toggled.connect(
376
+ partial(self.show_slider, len(self.channels) - 1)
377
+ )
378
+ self.channel_grid.addWidget(
379
+ self.checkBoxes[-1], len(self.channels) + 1, 0, 1, 1
380
+ )
381
+
382
+ self.sliders[-1].setMinimum(0)
383
+ for i in range(len(self.channels)):
384
+ self.sliders[i].setMaximum(len(self.channels))
385
+ self.sliders[-1].setEnabled(False)
386
+ self.channel_grid.addWidget(
387
+ self.sliders[-1], len(self.channels) + 1, 1, 1, 2, alignment=Qt.AlignRight
388
+ )
389
+
390
+ def show_slider(self, index):
391
+ if self.checkBoxes[index].isChecked():
392
+ self.sliders[index].setEnabled(True)
393
+ else:
394
+ self.sliders[index].setEnabled(False)
395
+
396
+ def generate_population_params_box(self):
397
+ """
398
+ Parameters related to the movie channels
399
+ Rewrite all of it
400
+
401
+ """
402
+
403
+ self.population_grid = QGridLayout()
404
+ self.population_grid.setContentsMargins(21, 30, 20, 30)
405
+
406
+ pop_lbl = QLabel("CELL POPULATIONS")
407
+ pop_lbl.setStyleSheet(
408
+ """
409
+ font-weight: bold;
410
+ """
411
+ )
412
+ self.population_grid.addWidget(pop_lbl, 0, 0, 1, 3, alignment=Qt.AlignCenter)
413
+
414
+ self.populations = ["effectors", "targets"]
415
+ self.population_checkboxes = [QCheckBox() for i in range(len(self.populations))]
416
+
417
+ for i in range(len(self.populations)):
418
+
419
+ self.population_checkboxes[i].setText(self.populations[i])
420
+ self.population_checkboxes[i].setChecked(True)
421
+ self.population_grid.addWidget(
422
+ self.population_checkboxes[i], i + 1, 0, 1, 1
423
+ )
424
+
425
+ # Add channel button
426
+ self.addPopBtn = QPushButton("Add a cell population")
427
+ self.addPopBtn.setIcon(icon(MDI6.plus, color="white"))
428
+ self.addPopBtn.setIconSize(QSize(25, 25))
429
+ self.addPopBtn.setStyleSheet(self.button_style_sheet)
430
+ self.addPopBtn.clicked.connect(self.add_custom_population)
431
+ self.population_grid.addWidget(self.addPopBtn, 1000, 0, 1, 1)
432
+
433
+ def add_custom_population(self):
434
+ self.CustomPopWidget = CelldetectiveWidget()
435
+ self.CustomPopWidget.setWindowTitle("Define custom population")
436
+ layout = QVBoxLayout()
437
+ self.CustomPopWidget.setLayout(layout)
438
+
439
+ self.name_le = QLineEdit()
440
+ self.name_le.setPlaceholderText("name")
441
+ self.name_le.textChanged.connect(self.check_population_name)
442
+ hbox = QHBoxLayout()
443
+ hbox.addWidget(QLabel("population name: "), 33)
444
+ hbox.addWidget(self.name_le, 66)
445
+ layout.addLayout(hbox)
446
+
447
+ self.addPopBtn = QPushButton("add")
448
+ self.addPopBtn.setStyleSheet(self.button_style_sheet)
449
+ self.addPopBtn.setEnabled(False)
450
+ self.addPopBtn.clicked.connect(self.write_custom_population)
451
+ layout.addWidget(self.addPopBtn)
452
+ center_window(self.CustomPopWidget)
453
+ self.CustomPopWidget.show()
454
+
455
+ def check_population_name(self, text):
456
+ # define all conditions for valid population name (like no space)
457
+ if len(text) > 0 and " " not in text:
458
+ self.addPopBtn.setEnabled(True)
459
+ else:
460
+ self.addPopBtn.setEnabled(False)
461
+
462
+ def write_custom_population(self):
463
+
464
+ self.new_population_name = self.name_le.text()
465
+ name_map = self.new_population_name
466
+
467
+ self.populations.append(self.new_population_name)
468
+ self.population_checkboxes.append(QCheckBox())
469
+ self.CustomPopWidget.close()
470
+
471
+ self.population_checkboxes[-1].setText(self.populations[-1])
472
+ self.population_grid.addWidget(
473
+ self.population_checkboxes[-1], len(self.populations) + 1, 0, 1, 1
474
+ )
475
+
476
+ def browse_experiment_folder(self):
477
+ """
478
+ Set a new base directory.
479
+ """
480
+
481
+ self.newExpFolder = str(
482
+ QFileDialog.getExistingDirectory(self, "Select directory")
483
+ )
484
+ self.supFolder.setText(self.newExpFolder)
485
+
486
+ def create_config(self):
487
+ """
488
+ Create the folder tree, the config, issue a warning if the experiment folder already exists.
489
+ """
490
+
491
+ if not os.path.exists(self.supFolder.text()):
492
+ msgBox = QMessageBox()
493
+ msgBox.setIcon(QMessageBox.Critical)
494
+ msgBox.setText(
495
+ "The target path for the experiment project does not exist... Abort."
496
+ )
497
+ msgBox.setWindowTitle("Error")
498
+ msgBox.setStandardButtons(QMessageBox.Ok)
499
+ msgBox.exec()
500
+ return None
501
+
502
+ channel_indices = []
503
+ for i in range(len(self.channels)):
504
+ if self.checkBoxes[i].isChecked():
505
+ channel_indices.append(self.sliders[i].value())
506
+ if not channel_indices:
507
+ msgBox = QMessageBox()
508
+ msgBox.setIcon(QMessageBox.Warning)
509
+ msgBox.setText("Please set at least one channel before proceeding.")
510
+ msgBox.setWindowTitle("Warning")
511
+ msgBox.setStandardButtons(QMessageBox.Ok)
512
+ returnValue = msgBox.exec()
513
+ if returnValue == QMessageBox.Ok:
514
+ return None
515
+
516
+ if len(channel_indices) != len(set(channel_indices)):
517
+ msgBox = QMessageBox()
518
+ msgBox.setIcon(QMessageBox.Warning)
519
+ msgBox.setText(
520
+ "Some channel indices are repeated. Please check your configuration."
521
+ )
522
+ msgBox.setWindowTitle("Warning")
523
+ msgBox.setStandardButtons(QMessageBox.Ok)
524
+ returnValue = msgBox.exec()
525
+ if returnValue == QMessageBox.Ok:
526
+ return None
527
+
528
+ populations_checked = [
529
+ self.population_checkboxes[i].isChecked()
530
+ for i in range(len(self.population_checkboxes))
531
+ ]
532
+ if not np.any(populations_checked):
533
+ msgBox = QMessageBox()
534
+ msgBox.setIcon(QMessageBox.Warning)
535
+ msgBox.setText(
536
+ "Please set at least one cell population before proceeding..."
537
+ )
538
+ msgBox.setWindowTitle("Warning")
539
+ msgBox.setStandardButtons(QMessageBox.Ok)
540
+ returnValue = msgBox.exec()
541
+ if returnValue == QMessageBox.Ok:
542
+ return None
543
+
544
+ sorted_list = set(channel_indices)
545
+ expected_list = set(np.arange(max(sorted_list) + 1))
546
+ logger.debug(f"{sorted_list} {expected_list} {sorted_list==expected_list}")
547
+
548
+ if not sorted_list == expected_list:
549
+ msgBox = QMessageBox()
550
+ msgBox.setIcon(QMessageBox.Warning)
551
+ msgBox.setText(
552
+ "There is a gap in your channel indices. Please check your configuration."
553
+ )
554
+ msgBox.setWindowTitle("Warning")
555
+ msgBox.setStandardButtons(QMessageBox.Ok)
556
+ returnValue = msgBox.exec()
557
+ if returnValue == QMessageBox.Ok:
558
+ return None
559
+
560
+ try:
561
+
562
+ folder = self.supFolder.text()
563
+ folder = folder.replace("\\", "/")
564
+ folder = rf"{folder}"
565
+
566
+ name = str(self.expName.text())
567
+ name = name.replace(" ", "")
568
+
569
+ self.directory = "/".join([folder, name])
570
+ os.mkdir(self.directory)
571
+ os.chdir(self.directory)
572
+ self.create_subfolders()
573
+ self.annotate_wells()
574
+
575
+ except FileExistsError:
576
+ msgBox = QMessageBox()
577
+ msgBox.setIcon(QMessageBox.Warning)
578
+ msgBox.setText(
579
+ "This experiment already exists... Please select another name."
580
+ )
581
+ msgBox.setWindowTitle("Warning")
582
+ msgBox.setStandardButtons(QMessageBox.Ok)
583
+ returnValue = msgBox.exec()
584
+ if returnValue == QMessageBox.Ok:
585
+ return None
586
+
587
+ def create_subfolders(self):
588
+ """
589
+ Create the well folders and position folders within the wells.
590
+ """
591
+
592
+ self.nbr_wells = self.SliderWells.value()
593
+ self.nbr_positions = self.SliderPos.value()
594
+ for k in range(self.nbr_wells):
595
+ well_name = f"W{k+1}" + os.sep
596
+ os.mkdir(well_name)
597
+ for p in range(self.nbr_positions):
598
+ position_name = well_name + f"{k+1}0{p}" + os.sep
599
+ os.mkdir(position_name)
600
+ os.mkdir(position_name + os.sep + "movie" + os.sep)
601
+
602
+ def annotate_wells(self):
603
+ self.w = SetupConditionLabels(self, self.nbr_wells)
604
+ self.w.show()
605
+
606
+ def create_config_file(self):
607
+ """
608
+
609
+ Write all user input parameters to a configuration file associated to an experiment.
610
+ """
611
+
612
+ config = ConfigParser(interpolation=None)
613
+
614
+ config.add_section("Populations")
615
+ pops = ",".join(
616
+ [
617
+ self.populations[i].lower()
618
+ for i in range(len(self.population_checkboxes))
619
+ if self.population_checkboxes[i].isChecked()
620
+ ]
621
+ )
622
+ config.set("Populations", "populations", pops)
623
+
624
+ # add a new section and some values
625
+ config.add_section("MovieSettings")
626
+ config.set(
627
+ "MovieSettings", "PxToUm", self.PxToUm_field.text().replace(",", ".")
628
+ )
629
+ config.set(
630
+ "MovieSettings",
631
+ "FrameToMin",
632
+ self.FrameToMin_field.text().replace(",", "."),
633
+ )
634
+ config.set("MovieSettings", "len_movie", str(self.MovieLengthSlider.value()))
635
+ config.set("MovieSettings", "shape_x", self.shape_x_field.text())
636
+ config.set("MovieSettings", "shape_y", self.shape_y_field.text())
637
+ config.set("MovieSettings", "movie_prefix", self.movie_prefix_field.text())
638
+
639
+ config.add_section("Channels")
640
+ for i in range(len(self.channels)):
641
+ if self.checkBoxes[i].isChecked():
642
+ config.set(
643
+ "Channels", self.channel_mapping[i], str(self.sliders[i].value())
644
+ )
645
+ else:
646
+ config.set("Channels", self.channel_mapping[i], "nan")
647
+
648
+ config.add_section("Labels")
649
+ config.set("Labels", "cell_types", self.cell_types)
650
+ config.set("Labels", "antibodies", self.antibodies)
651
+ config.set("Labels", "concentrations", self.concentrations)
652
+ config.set("Labels", "pharmaceutical_agents", self.pharmaceutical_agents)
653
+
654
+ config.add_section("Metadata")
655
+ config.set("Metadata", "concentration_units", self.concentration_units)
656
+
657
+ # save to a file
658
+ with open("config.ini", "w") as configfile:
659
+ config.write(configfile)
660
+
661
+ self.parent_window.set_experiment_path(self.directory)
662
+ logger.info(
663
+ f"New experiment successfully configured in folder {self.directory}..."
664
+ )
665
+ self.close()
666
+
667
+
668
+ class SetupConditionLabels(CelldetectiveWidget):
669
+ def __init__(self, parent_window, n_wells):
670
+ super().__init__()
671
+ self.parent_window = parent_window
672
+ self.n_wells = n_wells
673
+ self.setWindowTitle("Well conditions")
674
+
675
+ # --- Outer layout ---
676
+ self.outer_layout = QVBoxLayout(self)
677
+ self.outer_layout.setContentsMargins(10, 10, 10, 10)
678
+
679
+ # --- Scroll area ---
680
+ self.scroll = QScrollArea(self)
681
+ self.scroll.setWidgetResizable(True)
682
+ self.outer_layout.addWidget(self.scroll, stretch=1) # takes most space
683
+
684
+ # Container inside scroll
685
+ self.container = QWidget()
686
+ self.scroll.setWidget(self.container)
687
+
688
+ # Content layout (scrollable part)
689
+ self.layout = QVBoxLayout(self.container)
690
+ self.layout.setContentsMargins(30, 30, 30, 30)
691
+
692
+ self.onlyFloat = QDoubleValidator()
693
+ self.populate()
694
+
695
+ self.concentration_units_le = QLineEdit("pM")
696
+ self.concentration_units_le.setPlaceholderText("concentration units")
697
+ self.concentration_units_le.textChanged.connect(self.update_placeholders)
698
+
699
+ concentration_units_layout = QHBoxLayout()
700
+ concentration_units_layout.addWidget(
701
+ QLabel("concentration\nunits: "), 5, alignment=Qt.AlignLeft
702
+ )
703
+ concentration_units_layout.addWidget(self.concentration_units_le, 10)
704
+ concentration_units_layout.addWidget(QLabel(""), 85)
705
+ self.outer_layout.addLayout(concentration_units_layout)
706
+
707
+ # --- Fixed button row (not scrollable) ---
708
+ btn_hbox = QHBoxLayout()
709
+ btn_hbox.setContentsMargins(0, 15, 0, 0)
710
+
711
+ self.skip_btn = QPushButton("Skip")
712
+ self.skip_btn.setStyleSheet(self.button_style_sheet_2)
713
+ self.skip_btn.clicked.connect(self.set_default_values)
714
+ btn_hbox.addWidget(self.skip_btn)
715
+
716
+ self.submit_btn = QPushButton("Submit")
717
+ self.submit_btn.setStyleSheet(self.button_style_sheet)
718
+ self.submit_btn.clicked.connect(self.set_user_values)
719
+ btn_hbox.addWidget(self.submit_btn)
720
+
721
+ self.outer_layout.addLayout(btn_hbox) # outside scroll
722
+ self.setMinimumWidth(int(0.6 * self.parent_window.parent_window.screen_width))
723
+
724
+ center_window(self)
725
+
726
+ def populate(self):
727
+ self.cell_type_cbs = [QLineEdit() for i in range(self.n_wells)]
728
+ self.antibodies_cbs = [QLineEdit() for i in range(self.n_wells)]
729
+ self.concentrations_cbs = [QLineEdit() for i in range(self.n_wells)]
730
+ self.pharmaceutical_agents_cbs = [QLineEdit() for i in range(self.n_wells)]
731
+
732
+ for i in range(self.n_wells):
733
+ hbox = QHBoxLayout()
734
+ hbox.setContentsMargins(15, 5, 15, 5)
735
+ hbox.addWidget(QLabel(f"well {i+1}"), 5, alignment=Qt.AlignLeft)
736
+ hbox.addWidget(QLabel("cell type: "), 5)
737
+ hbox.addWidget(self.cell_type_cbs[i], 10)
738
+ self.cell_type_cbs[i].setPlaceholderText("e.g. T-cell, NK")
739
+
740
+ hbox.addWidget(QLabel("antibody: "), 5)
741
+ hbox.addWidget(self.antibodies_cbs[i], 10)
742
+ self.antibodies_cbs[i].setPlaceholderText("e.g. anti-CD4")
743
+
744
+ hbox.addWidget(QLabel("concentration: "), 5)
745
+ hbox.addWidget(self.concentrations_cbs[i], 10)
746
+ self.concentrations_cbs[i].setPlaceholderText("e.g. 100 (pM)")
747
+ self.concentrations_cbs[i].setValidator(self.onlyFloat)
748
+
749
+ hbox.addWidget(QLabel("pharmaceutical agents: "), 5)
750
+ hbox.addWidget(self.pharmaceutical_agents_cbs[i], 10)
751
+ self.pharmaceutical_agents_cbs[i].setPlaceholderText("e.g. dextran")
752
+
753
+ self.layout.addLayout(hbox)
754
+
755
+ def update_placeholders(self, new_unit):
756
+ for le in self.concentrations_cbs:
757
+ le.setPlaceholderText(f"e.g. 100 ({new_unit})")
758
+
759
+ def set_default_values(self):
760
+
761
+ for i in range(self.n_wells):
762
+ self.cell_type_cbs[i].setText(str(i))
763
+ self.antibodies_cbs[i].setText(str(i))
764
+ self.concentrations_cbs[i].setText(str(i))
765
+ self.pharmaceutical_agents_cbs[i].setText(str(i))
766
+ self.set_attributes()
767
+ self.parent_window.create_config_file()
768
+ self.close()
769
+
770
+ def set_user_values(self):
771
+ for i in range(self.n_wells):
772
+ if self.cell_type_cbs[i].text() == "":
773
+ self.cell_type_cbs[i].setText(str(i))
774
+ if self.antibodies_cbs[i].text() == "":
775
+ self.antibodies_cbs[i].setText(str(i))
776
+ if self.concentrations_cbs[i].text() == "":
777
+ self.concentrations_cbs[i].setText(str(i))
778
+ if self.pharmaceutical_agents_cbs[i].text() == "":
779
+ self.pharmaceutical_agents_cbs[i].setText(str(i))
780
+ self.set_attributes()
781
+ self.parent_window.create_config_file()
782
+ self.close()
783
+
784
+ def set_attributes(self):
785
+
786
+ cell_type_text = [c.text() for c in self.cell_type_cbs]
787
+ self.parent_window.cell_types = ",".join(cell_type_text)
788
+
789
+ antibodies_text = [c.text() for c in self.antibodies_cbs]
790
+ self.parent_window.antibodies = ",".join(antibodies_text)
791
+
792
+ concentrations_text = [c.text() for c in self.concentrations_cbs]
793
+ self.parent_window.concentrations = ",".join(concentrations_text)
794
+
795
+ pharamaceutical_text = [c.text() for c in self.pharmaceutical_agents_cbs]
796
+ self.parent_window.pharmaceutical_agents = ",".join(pharamaceutical_text)
797
+
798
+ self.parent_window.concentration_units = self.concentration_units_le.text()