Coverage for openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py: 0.0%

130 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +0000

1""" 

2Synthetic Plate Generator Window for PyQt6 

3 

4Provides a user-friendly interface for generating synthetic microscopy plates 

5for testing OpenHCS without requiring real microscopy data. 

6""" 

7 

8import logging 

9import tempfile 

10from pathlib import Path 

11from typing import Optional 

12 

13from PyQt6.QtWidgets import ( 

14 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

15 QScrollArea, QWidget, QMessageBox, QFileDialog 

16) 

17from PyQt6.QtCore import Qt, pyqtSignal 

18from PyQt6.QtGui import QFont 

19 

20from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager 

21from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator 

22from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

23from openhcs.tests.generators.generate_synthetic_data import SyntheticMicroscopyGenerator 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28class SyntheticPlateGeneratorWindow(QDialog): 

29 """ 

30 Dialog window for generating synthetic microscopy plates. 

31  

32 Provides a parameter form for SyntheticMicroscopyGenerator with sensible 

33 defaults from the test suite, allowing users to easily generate test data. 

34 """ 

35 

36 # Signals 

37 plate_generated = pyqtSignal(str, str) # output_dir path, pipeline_path 

38 

39 def __init__(self, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

40 """ 

41 Initialize the synthetic plate generator window. 

42  

43 Args: 

44 color_scheme: Color scheme for styling (optional) 

45 parent: Parent widget 

46 """ 

47 super().__init__(parent) 

48 

49 # Initialize color scheme and style generator 

50 self.color_scheme = color_scheme or PyQt6ColorScheme() 

51 self.style_generator = StyleSheetGenerator(self.color_scheme) 

52 

53 # Output directory (will be set by user or use temp) 

54 self.output_dir: Optional[str] = None 

55 

56 # Setup UI 

57 self.setup_ui() 

58 

59 logger.debug("Synthetic plate generator window initialized") 

60 

61 def setup_ui(self): 

62 """Setup the user interface.""" 

63 self.setWindowTitle("Generate Synthetic Plate") 

64 self.setModal(True) 

65 self.setMinimumSize(700, 600) 

66 self.resize(800, 700) 

67 

68 layout = QVBoxLayout(self) 

69 layout.setSpacing(10) 

70 

71 # Header with title and description 

72 header_widget = QWidget() 

73 header_layout = QVBoxLayout(header_widget) 

74 header_layout.setContentsMargins(10, 10, 10, 10) 

75 

76 header_label = QLabel("Generate Synthetic Microscopy Plate") 

77 header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold)) 

78 header_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};") 

79 header_layout.addWidget(header_label) 

80 

81 description_label = QLabel( 

82 "Generate synthetic microscopy data for testing OpenHCS without real data. " 

83 "The defaults match the test suite configuration and will create a small 4-well plate " 

84 "with 3x3 grid, 2 channels, and 3 z-levels." 

85 ) 

86 description_label.setWordWrap(True) 

87 description_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_secondary)}; padding: 5px;") 

88 header_layout.addWidget(description_label) 

89 

90 layout.addWidget(header_widget) 

91 

92 # Output directory selection 

93 output_dir_widget = self._create_output_dir_selector() 

94 layout.addWidget(output_dir_widget) 

95 

96 # Parameter form with scroll area 

97 scroll_area = QScrollArea() 

98 scroll_area.setWidgetResizable(True) 

99 scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

100 scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

101 

102 # Create form manager from SyntheticMicroscopyGenerator class 

103 # This automatically builds the UI from the __init__ signature (same pattern as function_pane.py) 

104 # CRITICAL: Pass color_scheme as parameter to ensure consistent theming with other parameter forms 

105 self.form_manager = ParameterFormManager( 

106 object_instance=SyntheticMicroscopyGenerator, # Pass the class itself, not __init__ 

107 field_id="synthetic_plate_generator", 

108 parent=self, 

109 context_obj=None, 

110 exclude_params=['output_dir', 'skip_files', 'include_all_components', 'random_seed'], # Exclude advanced params (self is auto-excluded) 

111 color_scheme=self.color_scheme # Pass color_scheme as instance parameter, not class attribute 

112 ) 

113 

114 scroll_area.setWidget(self.form_manager) 

115 layout.addWidget(scroll_area) 

116 

117 # Button row 

118 button_row = QHBoxLayout() 

119 button_row.setContentsMargins(10, 5, 10, 10) 

120 button_row.setSpacing(10) 

121 

122 button_row.addStretch() 

123 

124 # Get centralized button styles 

125 button_styles = self.style_generator.generate_config_button_styles() 

126 

127 # Cancel button 

128 cancel_button = QPushButton("Cancel") 

129 cancel_button.setFixedHeight(28) 

130 cancel_button.setMinimumWidth(100) 

131 cancel_button.clicked.connect(self.reject) 

132 cancel_button.setStyleSheet(button_styles["cancel"]) 

133 button_row.addWidget(cancel_button) 

134 

135 # Generate button 

136 generate_button = QPushButton("Generate Plate") 

137 generate_button.setFixedHeight(28) 

138 generate_button.setMinimumWidth(100) 

139 generate_button.clicked.connect(self.generate_plate) 

140 generate_button.setStyleSheet(button_styles["save"]) 

141 button_row.addWidget(generate_button) 

142 

143 layout.addLayout(button_row) 

144 

145 # Apply window styling 

146 self.setStyleSheet(self.style_generator.generate_dialog_style()) 

147 

148 def _create_output_dir_selector(self) -> QWidget: 

149 """Create the output directory selector widget.""" 

150 widget = QWidget() 

151 layout = QHBoxLayout(widget) 

152 layout.setContentsMargins(10, 5, 10, 5) 

153 layout.setSpacing(10) 

154 

155 label = QLabel("Output Directory:") 

156 label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)};") 

157 layout.addWidget(label) 

158 

159 self.output_dir_label = QLabel("<temp directory>") 

160 self.output_dir_label.setStyleSheet( 

161 f"color: {self.color_scheme.to_hex(self.color_scheme.text_secondary)}; " 

162 f"font-style: italic; padding: 5px;" 

163 ) 

164 layout.addWidget(self.output_dir_label, 1) 

165 

166 browse_button = QPushButton("Browse...") 

167 browse_button.setFixedHeight(28) 

168 browse_button.setMinimumWidth(80) 

169 browse_button.clicked.connect(self.browse_output_dir) 

170 browse_button.setStyleSheet(self.style_generator.generate_button_style()) 

171 layout.addWidget(browse_button) 

172 

173 return widget 

174 

175 def browse_output_dir(self): 

176 """Open file dialog to select output directory.""" 

177 dir_path = QFileDialog.getExistingDirectory( 

178 self, 

179 "Select Output Directory for Synthetic Plate", 

180 str(Path.home()), 

181 QFileDialog.Option.ShowDirsOnly 

182 ) 

183 

184 if dir_path: 

185 self.output_dir = dir_path 

186 self.output_dir_label.setText(dir_path) 

187 self.output_dir_label.setStyleSheet( 

188 f"color: {self.color_scheme.to_hex(self.color_scheme.text_normal)}; padding: 5px;" 

189 ) 

190 

191 def generate_plate(self): 

192 """Generate the synthetic plate with current parameters.""" 

193 try: 

194 # Get parameters from form 

195 params = self.form_manager.get_current_values() 

196 

197 # Determine output directory 

198 if self.output_dir is None: 

199 # Use temp directory 

200 temp_dir = tempfile.mkdtemp(prefix="openhcs_synthetic_plate_") 

201 output_dir = temp_dir 

202 logger.info(f"Using temporary directory: {output_dir}") 

203 else: 

204 output_dir = self.output_dir 

205 

206 # Add output_dir to params 

207 params['output_dir'] = output_dir 

208 

209 # Show progress message 

210 QMessageBox.information( 

211 self, 

212 "Generating Plate", 

213 f"Generating synthetic plate at:\n{output_dir}\n\n" 

214 f"This may take a moment...", 

215 QMessageBox.StandardButton.Ok 

216 ) 

217 

218 # Create generator and generate dataset 

219 logger.info(f"Generating synthetic plate with params: {params}") 

220 generator = SyntheticMicroscopyGenerator(**params) 

221 generator.generate_dataset() 

222 

223 logger.info(f"Successfully generated synthetic plate at: {output_dir}") 

224 

225 # Get path to test_pipeline.py 

226 from openhcs.tests import test_pipeline 

227 pipeline_path = Path(test_pipeline.__file__) 

228 

229 # Emit signal with output directory and pipeline path 

230 self.plate_generated.emit(output_dir, str(pipeline_path)) 

231 

232 # Show success message 

233 QMessageBox.information( 

234 self, 

235 "Success", 

236 f"Synthetic plate generated successfully!\n\nLocation: {output_dir}", 

237 QMessageBox.StandardButton.Ok 

238 ) 

239 

240 # Close dialog 

241 self.accept() 

242 

243 except Exception as e: 

244 logger.error(f"Failed to generate synthetic plate: {e}", exc_info=True) 

245 QMessageBox.critical( 

246 self, 

247 "Generation Failed", 

248 f"Failed to generate synthetic plate:\n\n{str(e)}", 

249 QMessageBox.StandardButton.Ok 

250 ) 

251 

252 def reject(self): 

253 """Handle dialog rejection (Cancel button).""" 

254 # Cleanup before closing 

255 self._cleanup() 

256 super().reject() 

257 

258 def accept(self): 

259 """Handle dialog acceptance (Generate button).""" 

260 # Cleanup before closing 

261 self._cleanup() 

262 super().accept() 

263 

264 def _cleanup(self): 

265 """Cleanup resources before window closes.""" 

266 # Unregister from cross-window updates 

267 if hasattr(self, 'form_manager') and self.form_manager is not None: 

268 try: 

269 self.form_manager.unregister_from_cross_window_updates() 

270 except RuntimeError: 

271 # Widget already deleted, ignore 

272 pass 

273 

274 # Disconnect all signals to prevent accessing deleted Qt objects 

275 try: 

276 self.plate_generated.disconnect() 

277 except (RuntimeError, TypeError): 

278 # Already disconnected or no connections, ignore 

279 pass 

280