pyREyes 3.4.0__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 (42) hide show
  1. pyREyes/REyes.py +958 -0
  2. pyREyes/append_targets_nav.py +343 -0
  3. pyREyes/config/filtering_options.json +19 -0
  4. pyREyes/config/microscope_configs.json +92 -0
  5. pyREyes/create_final_targets_nav.py +476 -0
  6. pyREyes/dif_map.py +1219 -0
  7. pyREyes/docs/README_FULL.md +522 -0
  8. pyREyes/docs/modules/diffraction-map.md +90 -0
  9. pyREyes/docs/modules/eucentricity.md +64 -0
  10. pyREyes/docs/modules/final-targets.md +92 -0
  11. pyREyes/docs/modules/grid-squares.md +130 -0
  12. pyREyes/docs/modules/manual-squares.md +61 -0
  13. pyREyes/docs/modules/map-plotter.md +95 -0
  14. pyREyes/docs/modules/monitor.md +113 -0
  15. pyREyes/docs/modules/navigation.md +88 -0
  16. pyREyes/docs/modules/targets.md +96 -0
  17. pyREyes/eucentricity.py +392 -0
  18. pyREyes/grid_squares.py +530 -0
  19. pyREyes/lib/REyes_errors.py +18 -0
  20. pyREyes/lib/REyes_logging.py +44 -0
  21. pyREyes/lib/REyes_microscope_configurations.py +36 -0
  22. pyREyes/lib/REyes_utils.py +35 -0
  23. pyREyes/lib/__init__.py +0 -0
  24. pyREyes/lib/diffraction/DiffractionDataProcessor.py +210 -0
  25. pyREyes/lib/grid_processing/CentroidProcessor.py +692 -0
  26. pyREyes/lib/grid_processing/GridProcessor.py +50 -0
  27. pyREyes/lib/grid_processing/filtering_options.py +30 -0
  28. pyREyes/lib/image_processing/CartImage.py +660 -0
  29. pyREyes/lib/image_processing/CartImage_utils.py +51 -0
  30. pyREyes/lib/image_processing/PlottingManager.py +210 -0
  31. pyREyes/lib/image_processing/__init__.py +0 -0
  32. pyREyes/lib/image_processing/image_processor.py +531 -0
  33. pyREyes/lib/ui/REyes_ui.py +16 -0
  34. pyREyes/lib/ui/__init__.py +0 -0
  35. pyREyes/manual_squares.py +148 -0
  36. pyREyes/mnt_maps_targets.py +650 -0
  37. pyREyes/write_sorted_targets_nav.py +158 -0
  38. pyreyes-3.4.0.dist-info/METADATA +117 -0
  39. pyreyes-3.4.0.dist-info/RECORD +42 -0
  40. pyreyes-3.4.0.dist-info/WHEEL +4 -0
  41. pyreyes-3.4.0.dist-info/entry_points.txt +10 -0
  42. pyreyes-3.4.0.dist-info/licenses/LICENSE +15 -0
pyREyes/REyes.py ADDED
@@ -0,0 +1,958 @@
1
+ """
2
+ REyes Monitor
3
+ Monitoring tool for REyes processing scripts to automate the workflow.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import logging
9
+ from subprocess import run, PIPE, Popen
10
+ import re
11
+ import sys
12
+ from enum import Enum, auto
13
+ import argparse
14
+ import shutil
15
+ import threading
16
+
17
+
18
+ from pyREyes.lib.ui.REyes_ui import print_banner
19
+ from pyREyes.lib.REyes_microscope_configurations import MicroscopeConfig, load_microscope_configs
20
+
21
+ MICROSCOPE_CONFIGS = load_microscope_configs()
22
+
23
+ class ProcessingState(Enum):
24
+ WAITING_FOR_MONTAGE = auto()
25
+ WAITING_FOR_GRID_SQUARES = auto()
26
+ WAITING_FOR_MANUAL_SQUARES = auto()
27
+ WAITING_FOR_DIFFRACTION_MAP = auto()
28
+ GENERATING_TARGETS = auto()
29
+ GENERATING_FINAL_MAP = auto()
30
+ WAITING_FOR_MOVIES = auto()
31
+ RUNNING_AUTOSOLVE = auto()
32
+ COMPLETED = auto()
33
+
34
+ class CustomHelpFormatter(argparse.HelpFormatter):
35
+ def _split_lines(self, text, width):
36
+ if text.startswith('States:'):
37
+ return text.splitlines()
38
+ return super()._split_lines(text, width)
39
+
40
+ __version__ = '3.4.0'
41
+ __min_required_version__ = '3.4.0'
42
+
43
+ class REyesProcessor:
44
+ def __init__(self,
45
+ working_directory='.',
46
+ microscope='Arctica-CETA',
47
+ filtering='default',
48
+ camera_length=None,
49
+ manual_squares=False,
50
+ start_state=None,
51
+ current_block=1,
52
+ top_target_per_category=2,
53
+ top_target_per_block=None,
54
+ stepscan_only=False,
55
+ # Autoprocess parameters
56
+ autoprocess=False,
57
+ microscope_config=None,
58
+ rotation_axis=None,
59
+ frame_size=None,
60
+ signal_pixel=None,
61
+ min_pixel=None,
62
+ background_pixel=None,
63
+ pixel_size=None,
64
+ wavelength=None,
65
+ beam_center_x=None,
66
+ beam_center_y=None,
67
+ file_extension=None,
68
+ detector_distance=None,
69
+ exposure=None,
70
+ rotation=None,
71
+ # Autosolve parameters
72
+ autosolve=False,
73
+ shelx=None,
74
+ ntryt=None,
75
+ ntryf=None):
76
+
77
+ # Setup logging
78
+ log_file_path = os.path.join('REyes_logs', 'REyes_monitor.log')
79
+
80
+ # Ensure the log directory exists
81
+ os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
82
+
83
+ formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
84
+
85
+ file_handler = logging.FileHandler(log_file_path)
86
+ file_handler.setFormatter(formatter)
87
+
88
+ class FlushingStreamHandler(logging.StreamHandler):
89
+ def emit(self, record):
90
+ super().emit(record)
91
+ self.flush()
92
+
93
+ console_handler = FlushingStreamHandler(sys.stdout)
94
+ console_handler.setFormatter(formatter)
95
+
96
+ logging.basicConfig(
97
+ level=logging.INFO, # or INFO
98
+ handlers=[file_handler, console_handler]
99
+ )
100
+
101
+ self.logger = logging.getLogger(__name__)
102
+ self.working_directory = os.path.abspath(working_directory)
103
+ self.processed_logs = set()
104
+ self.state = ProcessingState[start_state] if start_state else ProcessingState.WAITING_FOR_MONTAGE
105
+
106
+ self.nav_pattern = re.compile(r'(.+)_(\d+)LM_(\d+)x(\d+)_nav\.nav$')
107
+
108
+ self.current_block = current_block
109
+ self.microscope = microscope
110
+ self.filtering = filtering
111
+ self.manual_squares = manual_squares
112
+ self.camera_length = camera_length
113
+ self.top_target_per_category = top_target_per_category
114
+ self.top_target_per_block = top_target_per_block
115
+
116
+ self.stepscan_only = stepscan_only
117
+
118
+ self.autoprocess = autoprocess
119
+ # Save autoprocess arguments individually
120
+ if autoprocess:
121
+ self.microscope_config = microscope_config
122
+ self.rotation_axis = rotation_axis
123
+ self.frame_size = frame_size
124
+ self.signal_pixel = signal_pixel
125
+ self.min_pixel = min_pixel
126
+ self.background_pixel = background_pixel
127
+ self.pixel_size = pixel_size
128
+ self.wavelength = wavelength
129
+ self.beam_center_x = beam_center_x
130
+ self.beam_center_y = beam_center_y
131
+ self.file_extension = file_extension
132
+ self.detector_distance = detector_distance
133
+ self.exposure = exposure
134
+ self.rotation = rotation
135
+
136
+
137
+ self.autosolve = autosolve
138
+
139
+ if autosolve:
140
+ self.shelx = shelx
141
+ self.ntryt = ntryt
142
+ self.ntryf = ntryf
143
+
144
+
145
+ self.processed_movies = set()
146
+ self.movies_count = 0
147
+ self.movies_directory = os.path.join(self.working_directory, 'movies')
148
+
149
+ self.logger.info(f"Initializing with state: {self.state.name}")
150
+ self.logger.info(f"Current block: {self.current_block}")
151
+
152
+ def run_module(self, module_name, args=None):
153
+ """Run a package module as a subprocess command with arguments"""
154
+ try:
155
+ self.logger.info(f"Running module: {module_name}")
156
+
157
+ # Map module names to their corresponding command line names
158
+ module_commands = {
159
+ 'grid_squares': 'grid-squares-0',
160
+ 'manual_squares': 'manual-squares-0-1',
161
+ 'eucentricity': 'eucentricity-1',
162
+ 'dif_map': 'dif-map-2',
163
+ 'write_targets': 'write-targets-3',
164
+ 'append_targets': 'append-targets-3-1',
165
+ 'create_final_targets': 'create-final-targets-4',
166
+ 'mnt_maps_targets': 'mnt-maps-targets-5'
167
+ }
168
+
169
+ if module_name not in module_commands:
170
+ self.logger.error(f"Module {module_name} not found")
171
+ return False
172
+
173
+ # Construct the command with the entry point name
174
+ cmd = [module_commands[module_name]]
175
+
176
+ # Add arguments if provided
177
+ if args:
178
+ for key, value in args.items():
179
+ if isinstance(value, bool):
180
+ if value: # Only add flag if True
181
+ cmd.append(f"--{key.replace('_', '-')}")
182
+ else:
183
+ cmd.append(f"--{key.replace('_', '-')}")
184
+ cmd.append(str(value))
185
+
186
+ self.logger.info(f"Running command: {' '.join(cmd)}")
187
+
188
+ # Run the command and capture output
189
+ result = run(cmd, stdout=PIPE, stderr=PIPE, text=True, cwd=self.working_directory)
190
+
191
+ # Log the complete output to the main log file
192
+ if result.stdout:
193
+ self.logger.info(f"Process output:\n{result.stdout}")
194
+
195
+ # Log stderr as well, but as a warning or error
196
+ if result.stderr:
197
+ self.logger.warning(f"Process stderr:\n{result.stderr}")
198
+
199
+ if result.returncode == 0:
200
+ self.logger.info(f"Successfully executed {module_name}")
201
+ return True
202
+ else:
203
+ self.logger.error(f"{module_name} failed with return code {result.returncode}")
204
+ return False
205
+
206
+ except Exception as e:
207
+ self.logger.error(f"Error running module {module_name}: {str(e)}")
208
+ import traceback
209
+ self.logger.error(traceback.format_exc())
210
+ return False
211
+
212
+ def find_nav_files(self):
213
+ """Find .nav files and extract their information"""
214
+ nav_files = [f for f in os.listdir(self.working_directory) if f.endswith('_nav.nav')]
215
+ nav_info = []
216
+
217
+ for nav_file in nav_files:
218
+ match = self.nav_pattern.match(nav_file)
219
+ if match:
220
+ prefix, lm_number, x_dim, y_dim = match.groups()
221
+ nav_info.append({
222
+ 'nav_file': nav_file,
223
+ 'prefix': prefix,
224
+ 'lm_number': lm_number,
225
+ 'dimensions': f"{x_dim}x{y_dim}"
226
+ })
227
+ self.logger.debug(f"Found .nav file: {nav_file} with prefix {prefix}, "
228
+ f"LM number {lm_number}, dimensions {x_dim}x{y_dim}")
229
+
230
+ return nav_info
231
+
232
+ def find_montage_logs(self):
233
+ """Find montage log files based on .nav file information"""
234
+ nav_info_list = self.find_nav_files()
235
+ montage_logs = []
236
+
237
+ for nav_info in nav_info_list:
238
+ # Look for corresponding log file with the same prefix and dimensions
239
+ expected_log = f"{nav_info['prefix']}_{nav_info['lm_number']}LM_{nav_info['dimensions']}.log"
240
+ if (os.path.exists(os.path.join(self.working_directory, expected_log)) and
241
+ expected_log not in self.processed_logs):
242
+ montage_logs.append(expected_log)
243
+ self.logger.debug(f"Found matching montage log file: {expected_log}")
244
+
245
+ return montage_logs
246
+
247
+ def find_grid_squares_logs(self):
248
+ """Find grid squares log files"""
249
+ return [f for f in os.listdir(self.working_directory)
250
+ if f.endswith('_grid_squares.log') and
251
+ f not in self.processed_logs]
252
+
253
+ def find_diffraction_map_logs(self, nav_info):
254
+ """Find diffraction map log files for current block"""
255
+ block_log = f"{nav_info['prefix']}_{self.current_block}.log"
256
+ log_path = os.path.join(self.working_directory, block_log)
257
+
258
+ if os.path.exists(log_path) and block_log not in self.processed_logs:
259
+ return [block_log]
260
+ return []
261
+
262
+ def find_movie_logs(self):
263
+ """Find movie acquisition log file"""
264
+ movie_logs = [f for f in os.listdir(self.working_directory)
265
+ if f.endswith('_targets.log') and
266
+ f not in self.processed_logs]
267
+ #self.logger.info(f"Found movie log files: {movie_logs}")
268
+ return movie_logs
269
+
270
+ def find_new_movies(self, log_file):
271
+ """Find newly acquired movies from the targets log file"""
272
+ try:
273
+ new_movies = []
274
+ current_folder = None
275
+ current_movie = None
276
+
277
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
278
+ for line in f:
279
+ line = line.strip()
280
+
281
+ # Extract movie folder information
282
+ if 'Movie folder:' in line:
283
+ current_folder = line.split('Movie folder:')[-1].strip()
284
+
285
+ # Extract acquired movie name
286
+ elif 'Acquired movie:' in line:
287
+ current_movie = line.split('Acquired movie:')[-1].strip()
288
+
289
+ # If we have both folder and movie name, create movie info
290
+ if current_folder and current_movie:
291
+ movie_info = {
292
+ 'folder': current_folder,
293
+ 'acquired_name': current_movie,
294
+ 'suffix': '_integrated_movie.mrc' # Generic filename suffix
295
+ }
296
+
297
+ # Create a unique identifier for tracking processed movies
298
+ movie_id = f"{current_folder}/{current_movie}"
299
+
300
+ if movie_id not in self.processed_movies:
301
+ new_movies.append(movie_info)
302
+
303
+ # Reset for next movie
304
+ current_folder = None
305
+ current_movie = None
306
+
307
+ return new_movies
308
+
309
+ except Exception as e:
310
+ self.logger.error(f"Error reading movie log file {log_file}: {str(e)}")
311
+ return []
312
+
313
+ def check_montage_completion(self, log_file):
314
+ """Check if montage is complete"""
315
+ try:
316
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
317
+ content = f.read()
318
+ # Extract the LM number from the filename
319
+ match = re.search(r'_(\d+)LM_', log_file)
320
+ if match:
321
+ expected_number = match.group(1)
322
+ completion_pattern = f'completed {expected_number}x lm montage data collection'
323
+ if completion_pattern.lower() in content.lower():
324
+ self.logger.info(f"Montage completion detected in {log_file}")
325
+ return True
326
+ except Exception as e:
327
+ self.logger.error(f"Error reading montage log file {log_file}: {str(e)}")
328
+ return False
329
+
330
+ def check_nav_file_ready(self, nav_file):
331
+ """Check if nav file has more than one item"""
332
+ try:
333
+ with open(nav_file, 'r') as f:
334
+ content = f.read()
335
+ items = re.findall(r'\[Item\s*=\s*(\d+)\]', content)
336
+ item_count = len([item for item in items if item != '1'])
337
+ self.logger.debug(f"Found {item_count} additional items in nav file")
338
+ return item_count > 0
339
+ except Exception as e:
340
+ self.logger.error(f"Error checking nav file {nav_file}: {str(e)}")
341
+ return False
342
+
343
+ def handle_manual_squares_state(self):
344
+ """Handle the manual squares state processing"""
345
+ start_time = time.time()
346
+ timeout = 3600
347
+
348
+ # Add clear initial message
349
+ self.logger.info("Waiting for manual grid squares to be added...")
350
+ self.logger.info("Please add grid squares through SerialEM")
351
+ self.logger.info("Once all grid squares are added, initiate the next script")
352
+
353
+ while time.time() - start_time < timeout:
354
+ nav_info_list = self.find_nav_files()
355
+
356
+ for nav_info in nav_info_list:
357
+ nav_file = os.path.join(self.working_directory, nav_info['nav_file'])
358
+ if self.check_nav_file_ready(nav_file):
359
+ self.logger.info("Additional grid squares detected in nav file")
360
+ if self.run_module('manual_squares', {}):
361
+ # Wait for log file to confirm completion
362
+ log_file = os.path.join(self.working_directory, 'REyes_logs', 'manual_squares.log')
363
+ if os.path.exists(log_file) and self.check_manual_squares_completion(log_file):
364
+ self.state = ProcessingState.WAITING_FOR_GRID_SQUARES
365
+ self.logger.info("Manual squares processing completed")
366
+ self.logger.info("Moving to eucentricity adjustment state")
367
+ return True
368
+ time.sleep(5)
369
+ self.logger.warning("Manual squares processing timed out")
370
+ return False
371
+
372
+ def check_manual_squares_completion(self, log_file):
373
+ """Check if manual squares processing is complete"""
374
+ try:
375
+ with open(log_file, 'r', encoding='utf-8') as f:
376
+ content = f.read()
377
+ if 'processing completed successfully!'.lower() in content.lower():
378
+ self.logger.info("Manual squares completion detected")
379
+ return True
380
+ except Exception as e:
381
+ self.logger.error(f"Error reading manual squares log file: {str(e)}")
382
+ return False
383
+
384
+ def check_grid_squares_completion(self, log_file):
385
+ """Check if grid squares processing is complete"""
386
+ try:
387
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
388
+ content = f.read()
389
+ if 'completed fine eucentricity for all grid squares'.lower() in content.lower():
390
+ self.logger.info(f"Grid squares completion detected in {log_file}")
391
+ return True
392
+ except Exception as e:
393
+ self.logger.error(f"Error reading grid squares log file {log_file}: {str(e)}")
394
+ return False
395
+
396
+ def check_diffraction_map_completion(self, log_file):
397
+ """Check if diffraction map processing is complete for current block"""
398
+ try:
399
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
400
+ content = f.read()
401
+ if 'completed stepscan for current grid square'.lower() in content.lower():
402
+ self.logger.info(f"Diffraction map block {self.current_block} completion detected in {log_file}")
403
+ return True
404
+ except Exception as e:
405
+ self.logger.error(f"Error reading diffraction map log file {log_file}: {str(e)}")
406
+ return False
407
+
408
+ def check_all_maps_completion(self, log_file):
409
+ """Check if all diffraction map blocks are complete"""
410
+ try:
411
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
412
+ content = f.read()
413
+ if 'completed stepscan for all grid squares'.lower() in content.lower():
414
+ self.logger.info("Diffraction mapping completed")
415
+ return True
416
+ except Exception as e:
417
+ self.logger.error(f"Error reading diffraction map log file {log_file}: {str(e)}")
418
+ return False
419
+
420
+ def check_targets_completion(self, log_file):
421
+ """Check diffraction movies complete"""
422
+ try:
423
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
424
+ content = f.read()
425
+ if 'completed movie acquisition'.lower() in content.lower():
426
+ self.logger.info("Diffraction movie completion detected")
427
+ return True
428
+ except Exception as e:
429
+ self.logger.error(f"Error reading diffraction map log file {log_file}: {str(e)}")
430
+ return False
431
+
432
+ def check_all_targets_completion(self, log_file):
433
+ """Check all diffraction movies complete"""
434
+ try:
435
+ with open(os.path.join(self.working_directory, log_file), 'r', encoding='utf-8') as f:
436
+ content = f.read()
437
+ if 'completed acquiring tilt movies for all crystals!'.lower() in content.lower():
438
+ self.logger.info("All diffraction movies completion detected")
439
+ return True
440
+ except Exception as e:
441
+ self.logger.error(f"Error reading diffraction map log file {log_file}: {str(e)}")
442
+ return False
443
+
444
+ def process_movie(self, movie_info):
445
+ def find_movie_file(root_dir, target_name, suffix):
446
+ for dirpath, _, filenames in os.walk(root_dir):
447
+ for f in filenames:
448
+ if f == target_name or f.endswith(suffix):
449
+ return os.path.join(dirpath, f)
450
+ return None
451
+
452
+ """Process a single movie using autoprocess.py"""
453
+ try:
454
+ # Construct the full path to the movie directory
455
+ movie_dir = os.path.join(self.movies_directory, movie_info['folder'])
456
+
457
+ # Recursively search for the movie file
458
+ movie_path = find_movie_file(movie_dir, movie_info['acquired_name'], movie_info['suffix'])
459
+ if not movie_path:
460
+ self.logger.error(f"No integrated movie file found in directory or subdirectories: {movie_dir}")
461
+ return False
462
+
463
+ target_path = os.path.join(movie_dir, movie_info['acquired_name'])
464
+
465
+ # Rename if needed
466
+ if movie_path != target_path:
467
+ try:
468
+ self.logger.info(f"Renaming {movie_path} → {target_path}")
469
+ os.rename(movie_path, target_path)
470
+ self.logger.info(f"Successfully renamed file to {movie_info['acquired_name']}")
471
+ except Exception as e:
472
+ self.logger.error(f"Failed to rename movie file: {str(e)}")
473
+ return False
474
+
475
+ # Create unique movie identifier for tracking
476
+ movie_id = f"{movie_info['folder']}/{movie_info['acquired_name']}"
477
+ self.processed_movies.add(movie_id)
478
+ self.movies_count += 1
479
+
480
+ if self.autoprocess:
481
+ self.logger.info(f"Running autoprocess on movie {movie_info['acquired_name']}")
482
+
483
+ cmd = ["autoprocess"]
484
+
485
+ # Add autoprocess arguments
486
+ def add_arg(flag, value):
487
+ if value is not None:
488
+ cmd.append(f"--{flag}")
489
+ if not isinstance(value, bool):
490
+ cmd.append(str(value))
491
+
492
+ add_arg("microscope-config", self.microscope_config)
493
+ add_arg("rotation-axis", self.rotation_axis)
494
+ add_arg("frame-size", self.frame_size)
495
+ add_arg("signal-pixel", self.signal_pixel)
496
+ add_arg("min-pixel", self.min_pixel)
497
+ add_arg("background-pixel", self.background_pixel)
498
+ add_arg("pixel-size", self.pixel_size)
499
+ add_arg("wavelength", self.wavelength)
500
+ add_arg("beam-center-x", self.beam_center_x)
501
+ add_arg("beam-center-y", self.beam_center_y)
502
+ add_arg("file-extension", self.file_extension)
503
+ add_arg("detector-distance", self.detector_distance)
504
+ add_arg("exposure", self.exposure)
505
+ add_arg("rotation", self.rotation)
506
+
507
+
508
+ self.logger.info(f"Running autoprocess command: {' '.join(cmd)}")
509
+
510
+ # Run the command and capture output
511
+ try:
512
+ result = run(cmd, stdout=PIPE, stderr=PIPE, text=True, cwd=movie_dir)
513
+
514
+ # Print stdout directly without logger formatting
515
+ if result.stdout:
516
+ self.logger.info(f"Autoprocess stdout:\n{result.stdout}")
517
+
518
+ # Print stderr directly without logger formatting
519
+ if result.stderr:
520
+ self.logger.warning(f"Autoprocess stderr:\n{result.stderr}")
521
+
522
+ if result.returncode == 0:
523
+ self.logger.info(f"Successfully processed movie {movie_info['acquired_name']}")
524
+ return True
525
+ else:
526
+ self.logger.error(f"Autoprocess failed with return code {result.returncode}")
527
+ return False
528
+ except Exception as e:
529
+ self.logger.error(f"Error running autoprocess: {str(e)}")
530
+ return False
531
+ else:
532
+ self.logger.info(f"Skipping autoprocess for movie {movie_info['acquired_name']} (autoprocess disabled)")
533
+
534
+ self.logger.info(f"Successfully handled movie {self.movies_count}: {movie_info['acquired_name']}")
535
+ return True
536
+
537
+ except Exception as e:
538
+ self.logger.error(f"Error processing movie {movie_info['acquired_name']}: {str(e)}")
539
+ import traceback
540
+ self.logger.error(traceback.format_exc())
541
+ return False
542
+
543
+ def run_autosolve(self):
544
+ def stream_reader(pipe, log_func):
545
+ for line in iter(pipe.readline, ''):
546
+ log_func(line.strip())
547
+
548
+ """Run the AutoSolve pipeline after REyes is completed."""
549
+ try:
550
+ autosolve_cmd = [
551
+ "autosolve",
552
+ "--shelx", self.shelx,
553
+ "--ntryt", str(self.ntryt),
554
+ "--ntryf", str(self.ntryf)
555
+ ]
556
+
557
+ self.logger.info(f"Running AutoSolve command: {' '.join(autosolve_cmd)}")
558
+
559
+ process = Popen(autosolve_cmd, cwd=self.working_directory, stdout=PIPE, stderr=PIPE, text=True)
560
+
561
+ # Start threads to read stdout and stderr concurrently
562
+ stdout_thread = threading.Thread(target=stream_reader, args=(process.stdout, self.logger.info))
563
+ stderr_thread = threading.Thread(target=stream_reader, args=(process.stderr, self.logger.warning))
564
+ stdout_thread.start()
565
+ stderr_thread.start()
566
+
567
+ # Wait for process and threads to complete
568
+ returncode = process.wait()
569
+ stdout_thread.join()
570
+ stderr_thread.join()
571
+
572
+ if returncode == 0:
573
+ self.logger.info("AutoSolve completed successfully!")
574
+ return True
575
+ else:
576
+ # Fixed: changed result.returncode to returncode
577
+ self.logger.error(f"AutoSolve failed with return code {returncode}")
578
+ return False
579
+
580
+ except Exception as e:
581
+ self.logger.error(f"Error running AutoSolve: {str(e)}")
582
+ import traceback
583
+ self.logger.error(traceback.format_exc())
584
+
585
+ def monitor(self):
586
+ """Main monitoring loop with state machine"""
587
+ self.logger.info(f"Starting monitoring in working directory: {self.working_directory}")
588
+
589
+ try:
590
+ while True:
591
+ if self.state == ProcessingState.WAITING_FOR_MONTAGE:
592
+ self.logger.debug("Waiting for montage completion...")
593
+ montage_logs = self.find_montage_logs()
594
+
595
+ for log_file in montage_logs:
596
+ if self.check_montage_completion(log_file):
597
+ self.processed_logs.add(log_file)
598
+ if self.run_module('grid_squares', {
599
+ 'microscope': self.microscope,
600
+ 'filtering': self.filtering
601
+ }):
602
+ if self.manual_squares:
603
+ self.state = ProcessingState.WAITING_FOR_MANUAL_SQUARES
604
+ self.logger.info("Moving to manual squares monitoring state")
605
+ else:
606
+ self.state = ProcessingState.WAITING_FOR_GRID_SQUARES
607
+ self.logger.info("Moving to grid squares monitoring state")
608
+
609
+ elif self.state == ProcessingState.WAITING_FOR_MANUAL_SQUARES:
610
+ if self.handle_manual_squares_state():
611
+ continue
612
+ time.sleep(5) # Wait before checking again
613
+
614
+ elif self.state == ProcessingState.WAITING_FOR_GRID_SQUARES:
615
+ self.logger.debug("Waiting for eucentricity adjustment...")
616
+ grid_logs = self.find_grid_squares_logs()
617
+
618
+ for log_file in grid_logs:
619
+ if self.check_grid_squares_completion(log_file):
620
+ self.processed_logs.add(log_file)
621
+ if self.run_module('eucentricity', {}):
622
+ self.state = ProcessingState.WAITING_FOR_DIFFRACTION_MAP
623
+ self.logger.info("Moving to diffraction map monitoring state")
624
+
625
+ elif self.state == ProcessingState.WAITING_FOR_DIFFRACTION_MAP:
626
+ self.logger.debug(f"Waiting for diffraction map block {self.current_block} completion...")
627
+ nav_info_list = self.find_nav_files()
628
+
629
+ for nav_info in nav_info_list:
630
+ diffraction_logs = self.find_diffraction_map_logs(nav_info)
631
+
632
+ for log_file in diffraction_logs:
633
+ # First check if current block is complete and process it
634
+ if self.check_diffraction_map_completion(log_file):
635
+ self.processed_logs.add(log_file)
636
+ folder_name = log_file.replace(".log", "")
637
+
638
+ success = self.run_module('dif_map', {
639
+ 'microscope': self.microscope,
640
+ 'folder': folder_name,
641
+ 'skip_processed': True,
642
+ 'proc_blocks': 1
643
+ })
644
+
645
+ if success:
646
+ if self.check_all_maps_completion(log_file):
647
+ self.state = ProcessingState.GENERATING_TARGETS
648
+ self.logger.info("All diffraction map blocks completed, moving to target generation")
649
+ else:
650
+ self.current_block += 1
651
+ self.logger.info(f"Moving to next block: {self.current_block}")
652
+ else:
653
+ self.logger.warning(f"Failed to process {folder_name}, skipping to next block")
654
+ self.current_block += 1
655
+
656
+ elif self.state == ProcessingState.GENERATING_TARGETS:
657
+ self.logger.debug("Generating target lists...")
658
+ write_targets_args = {
659
+ 'microscope': self.microscope
660
+ }
661
+ if self.camera_length is not None:
662
+ write_targets_args['camera_length'] = self.camera_length
663
+
664
+ if self.run_module('write_targets', write_targets_args):
665
+ self.logger.info("All targets identified")
666
+ self.logger.debug("Generating spatialy aware targets navigator...")
667
+ create_targets_args = {
668
+ 'top-target-per-category': self.top_target_per_category
669
+ }
670
+ if self.top_target_per_block is not None:
671
+ create_targets_args['top-target-per-block'] = self.top_target_per_block
672
+
673
+ if self.run_module('create_final_targets', create_targets_args):
674
+ self.state = ProcessingState.GENERATING_FINAL_MAP
675
+ self.logger.info("Moving to final map generation state")
676
+
677
+ elif self.state == ProcessingState.GENERATING_FINAL_MAP:
678
+ self.logger.debug("Generating final diffraction map...")
679
+ if self.run_module('mnt_maps_targets', {
680
+ 'microscope': self.microscope
681
+ }):
682
+ if self.stepscan_only:
683
+ self.state = ProcessingState.COMPLETED
684
+ self.logger.info("REyes processing completed! (Step scan only)")
685
+ return
686
+ else:
687
+ self.state = ProcessingState.WAITING_FOR_MOVIES
688
+ self.logger.info("REyes processing completed!")
689
+ self.logger.debug("Monitoring for new movies...")
690
+
691
+ elif self.state == ProcessingState.WAITING_FOR_MOVIES:
692
+ self.logger.debug("Waiting for the next tilt dataset...")
693
+ movie_logs = self.find_movie_logs()
694
+
695
+ for log_file in movie_logs:
696
+ # Find any new movies
697
+ new_movies = self.find_new_movies(log_file)
698
+
699
+ # Process each new movie
700
+ for movie_info in new_movies:
701
+ if self.process_movie(movie_info):
702
+ self.logger.info(f"Successfully processed movie {self.movies_count}")
703
+ else:
704
+ self.logger.error(f"Failed to process movie: {movie_info['acquired_name']}")
705
+
706
+ # Check if all movies are completed
707
+ if self.check_all_targets_completion(log_file):
708
+ if self.autosolve:
709
+ self.state = ProcessingState.RUNNING_AUTOSOLVE
710
+ self.logger.info(f"All movies processed ({self.movies_count} total). Starting AutoSolve.")
711
+ else:
712
+ self.state = ProcessingState.COMPLETED
713
+ self.logger.info(f"All movies processed ({self.movies_count} total). Sample to Indexing completed.")
714
+ return
715
+
716
+ elif self.state == ProcessingState.RUNNING_AUTOSOLVE:
717
+ self.logger.debug("Running AutoSolve...")
718
+ if self.run_autosolve():
719
+ self.state = ProcessingState.COMPLETED
720
+ self.logger.info("AutoSolve completed. Marking processing as COMPLETED.")
721
+ else:
722
+ self.logger.error("AutoSolve failed. Still marking processing as COMPLETED.")
723
+ self.state = ProcessingState.COMPLETED
724
+ return
725
+
726
+
727
+ time.sleep(5)
728
+
729
+ except KeyboardInterrupt:
730
+ self.logger.info("Shutting down monitor...")
731
+ except Exception as e:
732
+ self.logger.error(f"Unexpected error in monitor loop: {str(e)}")
733
+ import traceback
734
+ self.logger.error(traceback.format_exc())
735
+ raise
736
+
737
+ def main():
738
+ """
739
+ Args:
740
+ args (argparse.Namespace): Must contain:
741
+ - working_directory (str): Path to working directory
742
+ - microscope (str): Microscope type
743
+ - filtering (str): Filtering type
744
+ Returns:
745
+ int: 0 on success, non-zero on failure
746
+ """
747
+ print_banner()
748
+ parser = argparse.ArgumentParser(
749
+ description='REyes Processing Monitor Settings',
750
+ formatter_class=CustomHelpFormatter
751
+ )
752
+
753
+ state_descriptions = {
754
+ 'WAITING_FOR_MONTAGE': 'Wait for montage image acquisition',
755
+ 'WAITING_FOR_GRID_SQUARES': 'Wait for grid squares processing and eucentricity',
756
+ 'WAITING_FOR_MANUAL_SQUARES': 'Wait for manual grid squares selection',
757
+ 'WAITING_FOR_DIFFRACTION_MAP': 'Wait for diffraction mapping',
758
+ 'GENERATING_TARGETS': 'Generate target lists',
759
+ 'GENERATING_FINAL_MAP': 'Generate final diffraction atlas',
760
+ 'WAITING_FOR_MOVIES': 'Wait for movie acquisition',
761
+ 'RUNNING_AUTOSOLVE': 'Run AutoSolve for structure solution',
762
+ 'COMPLETED': 'Processing completed'
763
+ }
764
+
765
+ states_help_text = 'States:\n' + '\n'.join(f' {state}: {desc}'
766
+ for state, desc in state_descriptions.items())
767
+
768
+ # Create argument groups
769
+ reyes_group = parser.add_argument_group('REyes arguments', 'Arguments for REyes processing')
770
+ autoprocess_group = parser.add_argument_group('Autoprocess arguments', 'Arguments passed to autoprocess.py')
771
+ autosolve_group = parser.add_argument_group('Autosolve arguments', 'Arguments passed to autosolve.py')
772
+
773
+ # Microscope-specific arguments
774
+ parser.add_argument('--microscope',
775
+ type=str,
776
+ choices=list(MICROSCOPE_CONFIGS.keys()),
777
+ default="Arctica-CETA",
778
+ help='Microscope configuration to use')
779
+
780
+ # REyes-specific arguments
781
+ reyes_group.add_argument('--filtering',
782
+ choices=['default', '1', '4', '9', '16', '25', '36', '49', '64', '81', '100', '121', '144', '169', '196', '96', 'None'],
783
+ default='default',
784
+ help='Filtering type to use (default: default)')
785
+ reyes_group.add_argument('--manual-squares',
786
+ action='store_true',
787
+ help='Enable manual squares processing step')
788
+
789
+ reyes_group.add_argument('--camera-length',
790
+ type=float,
791
+ default=None,
792
+ help='Optional override for camera length in mm (default: based on microscope config)')
793
+
794
+ reyes_group.add_argument('--top-target-per-category',
795
+ type=int,
796
+ default=2,
797
+ help='Number of top targets to select per category (used by create-final-targets)')
798
+
799
+ reyes_group.add_argument('--top-target-per-block',
800
+ type=int,
801
+ default=None,
802
+ help='Number of top targets to select per block (used by create-final-targets)')
803
+
804
+
805
+ # Add state selection argument
806
+ reyes_group.add_argument('--starting-state',
807
+ choices=[state.name for state in ProcessingState],
808
+ help=states_help_text)
809
+
810
+ reyes_group.add_argument('--current-block',
811
+ type=int,
812
+ default=1,
813
+ help='Current block number (for diffraction map state)')
814
+
815
+ reyes_group.add_argument('--stepscan-only',
816
+ action='store_true',
817
+ help='Do not collect tilt series movies for targets')
818
+
819
+ reyes_group.add_argument('--autoprocess',
820
+ action='store_true',
821
+ help='Run AutoProcess after movie acquisition')
822
+
823
+ # Autoprocess arguments (parsed individually like autosolve)
824
+ autoprocess_group.add_argument('--microscope-config',
825
+ type=str,
826
+ required='--autoprocess' in sys.argv,
827
+ help='Microscope config name (e.g., Arctica-CETA-mrc-SM)')
828
+
829
+ autoprocess_group.add_argument('--rotation-axis',
830
+ type=str,
831
+ help='Rotation axis (e.g., "-1 0 0")')
832
+
833
+ autoprocess_group.add_argument('--frame-size',
834
+ type=int,
835
+ help='Frame size of the movie (e.g., 2048)')
836
+
837
+ autoprocess_group.add_argument('--signal-pixel',
838
+ type=int,
839
+ help='Signal pixel radius')
840
+
841
+ autoprocess_group.add_argument('--min-pixel',
842
+ type=int,
843
+ help='Minimum pixel radius')
844
+
845
+ autoprocess_group.add_argument('--background-pixel',
846
+ type=int,
847
+ help='Background pixel radius')
848
+
849
+ autoprocess_group.add_argument('--pixel-size',
850
+ type=float,
851
+ help='Pixel size in microns (e.g., 0.028)')
852
+
853
+ autoprocess_group.add_argument('--wavelength',
854
+ type=float,
855
+ help='Beam wavelength (e.g., 0.0251)')
856
+
857
+ autoprocess_group.add_argument('--beam-center-x',
858
+ type=int,
859
+ help='Beam center X coordinate')
860
+
861
+ autoprocess_group.add_argument('--beam-center-y',
862
+ type=int,
863
+ help='Beam center Y coordinate')
864
+
865
+ autoprocess_group.add_argument('--file-extension',
866
+ type=str,
867
+ help='File extension for movies (e.g., ".mrc" or ".ser")')
868
+
869
+ autoprocess_group.add_argument('--detector-distance',
870
+ type=float,
871
+ help='Detector distance in mm')
872
+
873
+ autoprocess_group.add_argument('--exposure',
874
+ type=float,
875
+ help='Exposure time')
876
+
877
+ autoprocess_group.add_argument('--rotation',
878
+ type=float,
879
+ help='Rotation value')
880
+
881
+ autosolve_group.add_argument('--autosolve',
882
+ action='store_true',
883
+ help='Run AutoSolve after AutoProcess')
884
+
885
+ autosolve_group.add_argument('--shelx',
886
+ choices=['t', 'd', 'td'],
887
+ default='td',
888
+ help='Which SHELX programs to run (t: SHELXT, d: SHELXD, td: both)')
889
+
890
+ autosolve_group.add_argument('--ntryt',
891
+ type=int,
892
+ default=1000,
893
+ help='Number of trials for initial SHELXD screening (default: 1000)')
894
+
895
+ autosolve_group.add_argument('--ntryf',
896
+ type=int,
897
+ default=10000,
898
+ help='Number of trials for extended SHELXD runs (default: 10000)')
899
+
900
+ args = parser.parse_args()
901
+
902
+ # Validate autoprocess dependencies
903
+ if args.autoprocess and not args.microscope_config:
904
+ parser.error("--microscope-config is required when using --autoprocess")
905
+
906
+
907
+
908
+ working_dir = os.path.abspath('.')
909
+
910
+ processor_kwargs = dict(
911
+ working_directory=working_dir,
912
+ microscope=args.microscope,
913
+ filtering=args.filtering,
914
+ camera_length=args.camera_length,
915
+ manual_squares=args.manual_squares,
916
+ start_state=args.starting_state,
917
+ current_block=args.current_block,
918
+ autoprocess=args.autoprocess,
919
+ top_target_per_category=args.top_target_per_category,
920
+ top_target_per_block=args.top_target_per_block,
921
+ stepscan_only=args.stepscan_only
922
+ )
923
+
924
+ # Only include AutoProcess args if the flag is set
925
+ if args.autoprocess:
926
+ processor_kwargs.update({
927
+ 'microscope_config': args.microscope_config,
928
+ 'rotation_axis': args.rotation_axis,
929
+ 'frame_size': args.frame_size,
930
+ 'signal_pixel': args.signal_pixel,
931
+ 'min_pixel': args.min_pixel,
932
+ 'background_pixel': args.background_pixel,
933
+ 'pixel_size': args.pixel_size,
934
+ 'wavelength': args.wavelength,
935
+ 'beam_center_x': args.beam_center_x,
936
+ 'beam_center_y': args.beam_center_y,
937
+ 'file_extension': args.file_extension,
938
+ 'detector_distance': args.detector_distance,
939
+ 'exposure': args.exposure,
940
+ 'rotation': args.rotation,
941
+ })
942
+
943
+ # Only include AutoSolve args if the flag is set
944
+ if args.autosolve:
945
+ processor_kwargs.update({
946
+ 'autosolve': True,
947
+ 'shelx': args.shelx,
948
+ 'ntryt': args.ntryt,
949
+ 'ntryf': args.ntryf,
950
+ })
951
+
952
+ processor = REyesProcessor(**processor_kwargs)
953
+
954
+ processor.monitor()
955
+ return 0
956
+
957
+ if __name__ == "__main__":
958
+ main()