Coverage for openhcs/pyqt_gui/launch.py: 0.0%

141 statements  

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

1#!/usr/bin/env python3 

2""" 

3OpenHCS PyQt6 GUI Launcher 

4 

5Launch script for the OpenHCS PyQt6 GUI application. 

6Provides command-line interface and application initialization. 

7""" 

8 

9import sys 

10import argparse 

11import logging 

12import os 

13import platform 

14from pathlib import Path 

15from typing import Optional 

16 

17# Add OpenHCS to path if needed 

18try: 

19 from openhcs.core.config import GlobalPipelineConfig 

20except ImportError: 

21 # Add parent directory to path 

22 sys.path.insert(0, str(Path(__file__).parent.parent.parent)) 

23 from openhcs.core.config import GlobalPipelineConfig 

24 

25 

26from openhcs.pyqt_gui.app import OpenHCSPyQtApp 

27from openhcs.pyqt_gui.utils.window_utils import install_global_window_bounds_filter 

28 

29 

30def is_wsl() -> bool: 

31 """Check if running in Windows Subsystem for Linux.""" 

32 try: 

33 return 'microsoft' in platform.uname().release.lower() 

34 except Exception: 

35 return False 

36 

37 

38def setup_qt_platform(): 

39 """Setup Qt platform for different environments (macOS, Linux, WSL2, Windows).""" 

40 import platform 

41 from pathlib import Path 

42 

43 # Check if QT_QPA_PLATFORM is already set 

44 if 'QT_QPA_PLATFORM' in os.environ: 

45 logging.debug(f"QT_QPA_PLATFORM already set to: {os.environ['QT_QPA_PLATFORM']}") 

46 return 

47 

48 # Set appropriate Qt platform based on OS 

49 if platform.system() == 'Darwin': # macOS 

50 os.environ['QT_QPA_PLATFORM'] = 'cocoa' 

51 logging.info("macOS detected - setting QT_QPA_PLATFORM=cocoa") 

52 

53 # Set plugin path to help Qt find the cocoa plugin 

54 # Try to find PyQt6 installation directory 

55 if 'QT_QPA_PLATFORM_PLUGIN_PATH' not in os.environ: 

56 try: 

57 import PyQt6 

58 pyqt6_path = Path(PyQt6.__file__).parent 

59 plugin_path = pyqt6_path / 'Qt6' / 'plugins' / 'platforms' 

60 if plugin_path.exists(): 

61 os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(plugin_path.parent) 

62 logging.info(f"Set QT_QPA_PLATFORM_PLUGIN_PATH to: {plugin_path.parent}") 

63 else: 

64 logging.warning(f"PyQt6 plugins directory not found at: {plugin_path}") 

65 except Exception as e: 

66 logging.warning(f"Could not set QT_QPA_PLATFORM_PLUGIN_PATH: {e}") 

67 

68 elif platform.system() == 'Linux': 

69 os.environ['QT_QPA_PLATFORM'] = 'xcb' 

70 if is_wsl(): 

71 logging.info("WSL2 detected - setting QT_QPA_PLATFORM=xcb") 

72 else: 

73 logging.info("Linux detected - setting QT_QPA_PLATFORM=xcb") 

74 # Disable shared memory for X11 (helps with display issues) 

75 os.environ['QT_X11_NO_MITSHM'] = '1' 

76 # Windows doesn't need QT_QPA_PLATFORM set 

77 else: 

78 logging.debug(f"Platform {platform.system()} - using default Qt platform") 

79 

80 

81def setup_logging(log_level: str = "INFO", log_file: Optional[Path] = None): 

82 """Setup unified logging configuration for entire OpenHCS system - matches TUI exactly.""" 

83 log_level_obj = getattr(logging, log_level.upper()) 

84 

85 # Create logs directory 

86 log_dir = Path.home() / ".local" / "share" / "openhcs" / "logs" 

87 log_dir.mkdir(parents=True, exist_ok=True) 

88 

89 # Create timestamped log file if not specified 

90 if log_file is None: 

91 import time 

92 log_file = log_dir / f"openhcs_unified_{time.strftime('%Y%m%d_%H%M%S')}.log" 

93 

94 # Setup unified logging for entire OpenHCS system (EXACTLY like TUI) 

95 root_logger = logging.getLogger() 

96 

97 # Clear any existing handlers to ensure clean state 

98 root_logger.handlers.clear() 

99 

100 # Setup console + file logging (TUI only has file, GUI has both) 

101 console_handler = logging.StreamHandler(sys.stdout) 

102 console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 

103 

104 file_handler = logging.FileHandler(log_file) 

105 file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 

106 

107 root_logger.addHandler(console_handler) 

108 root_logger.addHandler(file_handler) 

109 root_logger.setLevel(log_level_obj) 

110 

111 # Prevent other modules from adding console handlers 

112 logging.basicConfig = lambda *args, **kwargs: None 

113 

114 # Set OpenHCS logger level for all components 

115 logging.getLogger("openhcs").setLevel(log_level_obj) 

116 logger = logging.getLogger("openhcs.pyqt_gui") 

117 logger.info(f"OpenHCS PyQt6 GUI logging started - Level: {logging.getLevelName(log_level_obj)}") 

118 logger.info(f"Log file: {log_file}") 

119 

120 # Reduce noise from some libraries 

121 logging.getLogger('PIL').setLevel(logging.WARNING) 

122 

123 

124def parse_arguments(): 

125 """ 

126 Parse command line arguments. 

127  

128 Returns: 

129 Parsed arguments 

130 """ 

131 parser = argparse.ArgumentParser( 

132 description="OpenHCS PyQt6 GUI - High-Content Screening Platform", 

133 formatter_class=argparse.RawDescriptionHelpFormatter, 

134 epilog=""" 

135Examples: 

136 %(prog)s # Launch with default settings 

137 %(prog)s --log-level DEBUG # Launch with debug logging 

138 %(prog)s --config config.json # Launch with custom config 

139 %(prog)s --log-file app.log # Launch with log file 

140 """ 

141 ) 

142 

143 parser.add_argument( 

144 '--log-level', 

145 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], 

146 default='INFO', 

147 help='Set logging level (default: INFO)' 

148 ) 

149 

150 parser.add_argument( 

151 '--log-file', 

152 type=Path, 

153 help='Log file path (default: console only)' 

154 ) 

155 

156 parser.add_argument( 

157 '--config', 

158 type=Path, 

159 help='Custom configuration file path' 

160 ) 

161 

162 parser.add_argument( 

163 '--no-gpu', 

164 action='store_true', 

165 help='Disable GPU acceleration' 

166 ) 

167 

168 parser.add_argument( 

169 '--version', 

170 action='version', 

171 version='OpenHCS PyQt6 GUI 1.0.0' 

172 ) 

173 

174 return parser.parse_args() 

175 

176 

177def load_configuration(config_path: Optional[Path] = None): 

178 """ 

179 Load application configuration with cache support (matches TUI pattern). 

180 

181 Args: 

182 config_path: Optional custom configuration file path 

183 

184 Returns: 

185 Global configuration object 

186 """ 

187 try: 

188 if config_path and config_path.exists(): 

189 # Load custom configuration 

190 # This would need to be implemented based on config format 

191 logging.info(f"Loading custom configuration from: {config_path}") 

192 # For now, use default config 

193 config = GlobalPipelineConfig() 

194 else: 

195 # Load cached configuration (matches TUI pattern) 

196 from openhcs.pyqt_gui.services.config_cache_adapter import load_cached_global_config_sync 

197 config = load_cached_global_config_sync() 

198 

199 return config 

200 

201 except Exception as e: 

202 logging.error(f"Failed to load configuration: {e}") 

203 logging.info("Falling back to default configuration") 

204 return GlobalPipelineConfig() 

205 

206 

207def check_dependencies(): 

208 """ 

209 Check for required dependencies. 

210  

211 Returns: 

212 True if all dependencies are available, False otherwise 

213 """ 

214 missing_deps = [] 

215 

216 # Check PyQt6 

217 try: 

218 import PyQt6 

219 logging.debug(f"PyQt6 version: {PyQt6.QtCore.PYQT_VERSION_STR}") 

220 except ImportError: 

221 missing_deps.append("PyQt6") 

222 

223 # Check PyQtGraph (optional) 

224 try: 

225 import pyqtgraph 

226 logging.debug(f"PyQtGraph version: {pyqtgraph.__version__}") 

227 except ImportError: 

228 logging.warning("PyQtGraph not available - system monitor will use fallback display") 

229 

230 # Check other optional dependencies 

231 optional_deps = { 

232 'cupy': 'GPU acceleration', 

233 'dill': 'Pipeline serialization', 

234 'psutil': 'System monitoring' 

235 } 

236 

237 for dep, description in optional_deps.items(): 

238 try: 

239 __import__(dep) 

240 logging.debug(f"{dep} available for {description}") 

241 except ImportError: 

242 logging.warning(f"{dep} not available - {description} may be limited") 

243 

244 if missing_deps: 

245 logging.error(f"Missing required dependencies: {', '.join(missing_deps)}") 

246 return False 

247 

248 return True 

249 

250 

251def main(): 

252 """ 

253 Main entry point for the OpenHCS PyQt6 GUI launcher. 

254  

255 Returns: 

256 Exit code 

257 """ 

258 # Parse command line arguments 

259 args = parse_arguments() 

260 

261 # Setup logging 

262 setup_logging(args.log_level, args.log_file) 

263 

264 logging.info("Starting OpenHCS PyQt6 GUI...") 

265 logging.info(f"Python version: {sys.version}") 

266 logging.info(f"Platform: {sys.platform}") 

267 

268 # Setup Qt platform (must be done before creating QApplication) 

269 setup_qt_platform() 

270 

271 try: 

272 # Check dependencies 

273 if not check_dependencies(): 

274 logging.error("Dependency check failed") 

275 return 1 

276 

277 # Load configuration 

278 config = load_configuration(args.config) 

279 

280 # Apply command line overrides 

281 if args.no_gpu: 

282 logging.info("GPU acceleration disabled by command line") 

283 # This would need to be implemented in the config 

284 # config.disable_gpu = True 

285 

286 # Setup GPU registry (must be done before creating app) 

287 from openhcs.core.orchestrator.gpu_scheduler import setup_global_gpu_registry 

288 setup_global_gpu_registry(global_config=config) 

289 logging.info("GPU registry setup completed") 

290 

291 # Create and run application 

292 logging.info("Initializing PyQt6 application...") 

293 app = OpenHCSPyQtApp(sys.argv, config) 

294 install_global_window_bounds_filter(app) # install once, early 

295 

296 logging.info("Starting application event loop...") 

297 exit_code = app.run() 

298 

299 logging.info(f"Application exited with code: {exit_code}") 

300 return exit_code 

301 

302 except KeyboardInterrupt: 

303 logging.info("Application interrupted by user") 

304 return 130 # Standard exit code for Ctrl+C 

305 

306 except Exception as e: 

307 logging.critical(f"Unexpected error: {e}", exc_info=True) 

308 return 1 

309 

310 

311if __name__ == "__main__": 

312 sys.exit(main())