From b0863e5ddba5d7310148a6d17752b77cc02c32cd Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 6 Jul 2026 11:52:21 +0200 Subject: [PATCH] style: format with ruff --- docs/conf.py | 86 ++- pyproject.toml | 15 + scripts/alc_bkg_comp.py | 30 +- scripts/camera_stat_thread.py | 204 +++--- scripts/free_busy_state.py | 3 +- scripts/gui_desginer.py | 85 +-- scripts/pshell_client_old.py | 15 +- scripts/redis_dump.py | 11 +- src/aare/daq/aaredb.py | 73 +-- src/aare/daq/auth.py | 58 +- src/aare/daq/autofocus.py | 6 +- src/aare/daq/beamcenterfit.py | 33 +- src/aare/daq/config.py | 153 ++--- src/aare/daq/config_model.py | 3 +- src/aare/daq/daq.py | 596 +++++------------ src/aare/daq/devices.py | 41 +- src/aare/daq/mlbox.py | 119 +--- .../daq/operations/common/ml_bounding_box.py | 49 +- src/aare/daq/operations/common/models.py | 3 +- src/aare/daq/operations/common/services.py | 15 +- .../operations/common/simulate_scan_result.py | 23 +- .../daq/operations/face_detection/__init__.py | 11 +- .../daq/operations/face_detection/models.py | 11 +- .../daq/operations/face_detection/service.py | 46 +- .../daq/operations/face_detection/utils.py | 41 +- .../daq/operations/loop_centering/__init__.py | 2 +- .../daq/operations/loop_centering/analyzer.py | 33 +- .../daq/operations/loop_centering/models.py | 9 +- .../daq/operations/loop_centering/service.py | 33 +- src/aare/daq/operations/mounting/__init__.py | 6 +- src/aare/daq/operations/mounting/service.py | 19 +- src/aare/daq/operations/raster/__init__.py | 6 +- src/aare/daq/operations/raster/models.py | 5 +- src/aare/daq/operations/raster/service.py | 196 ++---- src/aare/daq/operations/rotation/models.py | 7 +- src/aare/daq/operations/rotation/service.py | 44 +- src/aare/daq/operations/screenshot/service.py | 36 +- src/aare/daq/server.py | 232 ++----- src/aare/daq/server_exception_handler.py | 38 +- src/aare/daq/spreadsheetupdater.py | 15 +- src/aare/daq/tell_state_machine.py | 48 +- src/aare/daq/tellupdater.py | 77 +-- src/aare/daq/workflows.py | 12 +- src/aare/devices/aerotech.py | 29 +- src/aare/devices/area_detector.py | 9 +- src/aare/devices/bec_worker.py | 60 +- src/aare/devices/enum_pv.py | 3 +- src/aare/devices/filter_transmission.py | 4 +- src/aare/devices/fluorimeter.py | 8 +- src/aare/devices/jfjoch.py | 41 +- src/aare/devices/mx_lib.py | 57 +- src/aare/devices/my_motor.py | 18 +- src/aare/devices/set_get_pv.py | 23 +- src/aare/devices/smargon.py | 18 +- src/aare/devices/tell_backend.py | 49 +- src/aare/devices/tell_client.py | 44 +- src/aare/devices/workflow_tools.py | 9 +- src/aare/gui/auth.py | 4 +- src/aare/gui/gui.py | 40 +- src/aare/gui/main_window.py | 600 +++++------------- src/aare/gui/models/bookmark.py | 4 +- src/aare/gui/models/gui_state_manager.py | 3 +- src/aare/gui/models/sample_queue_model.py | 26 +- src/aare/gui/models/user_sample_model.py | 8 +- src/aare/gui/panels/LogPanel.py | 16 +- src/aare/gui/panels/automation_panel.py | 47 +- src/aare/gui/panels/axis_video_panel.py | 10 +- src/aare/gui/panels/beamline_controls.py | 2 +- .../gui/panels/beamline_recovery_panel.py | 23 +- src/aare/gui/panels/beamline_state_panel.py | 47 +- .../gui/panels/compact_automation_panel.py | 10 +- .../gui/panels/data_collection_settings.py | 11 +- src/aare/gui/panels/developer_help_dialog.py | 93 +-- src/aare/gui/panels/face_detection_panel.py | 16 +- src/aare/gui/panels/file_path_panel.py | 28 +- .../panels/fluorescence_data_collection.py | 12 +- src/aare/gui/panels/fluorescence_panel.py | 21 +- src/aare/gui/panels/illumination_panel.py | 8 +- src/aare/gui/panels/local_contact_panel.py | 171 ++--- src/aare/gui/panels/manual_sample_panel.py | 9 +- src/aare/gui/panels/portrait_mode.py | 100 +-- .../gui/panels/prediction_metrics_panel.py | 27 +- src/aare/gui/panels/raster_data_collection.py | 60 +- src/aare/gui/panels/reference_tools_panel.py | 28 +- .../gui/panels/rotation_data_collection.py | 73 +-- src/aare/gui/panels/samcam_panel.py | 31 +- src/aare/gui/panels/sample_queue_panel.py | 54 +- src/aare/gui/panels/scan_settings_panel.py | 19 +- src/aare/gui/panels/smargon_panel.py | 12 +- src/aare/gui/panels/smargon_trace_panel.py | 97 ++- src/aare/gui/panels/smart_rotation_panel.py | 79 +-- src/aare/gui/panels/target_stability_panel.py | 213 ++----- src/aare/gui/panels/tell_sample_panel.py | 28 +- src/aare/gui/panels/zoom_panel.py | 4 +- .../gui/scan_logic/raster_grid_manager.py | 145 +---- src/aare/gui/styles.py | 2 +- src/aare/gui/threads/axis_video_thread.py | 21 +- src/aare/gui/threads/camera_thread.py | 24 +- src/aare/gui/threads/daq_worker.py | 266 ++------ src/aare/gui/threads/prediction_subscriber.py | 21 +- src/aare/gui/threads/sse_client.py | 20 +- .../gui/tutorials/controls_help_dialog.py | 9 +- src/aare/gui/tutorials/tutorial_manager.py | 29 +- src/aare/gui/tutorials/tutorial_models.py | 2 +- .../gui/tutorials/tutorial_registration.py | 22 +- src/aare/gui/tutorials/tutorial_runtime.py | 40 +- src/aare/gui/tutorials/tutorial_targets.py | 10 +- src/aare/gui/tutorials/tutroial_texts.py | 16 +- src/aare/gui/widgets/alert_banner.py | 16 +- src/aare/gui/widgets/automation_progress.py | 10 +- src/aare/gui/widgets/baton_request_dialog.py | 26 +- src/aare/gui/widgets/busy_overlay.py | 9 +- src/aare/gui/widgets/camera_image.py | 101 +-- src/aare/gui/widgets/clickable_label.py | 2 +- .../widgets/local_contact_status_widget.py | 50 +- src/aare/gui/widgets/login.py | 30 +- src/aare/gui/widgets/message_box.py | 20 +- src/aare/gui/widgets/number_line_edit.py | 68 +- src/aare/gui/widgets/pgroup_dialog.py | 21 +- src/aare/gui/widgets/raster_grid_table.py | 34 +- src/aare/gui/widgets/splash_screen.py | 2 +- src/aare/gui/widgets/status_bar.py | 96 +-- src/aare/gui/widgets/text_list_dialog.py | 10 +- src/aare/gui/widgets/value_label.py | 7 +- src/aare/gui/widgets/video_image.py | 4 +- tests/conftest.py | 30 +- tests/integration/daq/test_daq_server.py | 7 +- .../daq/test_database_connection.py | 3 +- tests/unit/common/test_aare_exception.py | 12 +- tests/unit/common/test_coordinate.py | 8 +- .../common/test_data_collection_parameters.py | 4 +- .../unit/common/test_diffraction_geometry.py | 4 +- tests/unit/common/test_error_codes.py | 14 +- tests/unit/common/test_exception_handler.py | 4 +- tests/unit/common/test_find_xtal.py | 20 +- tests/unit/common/test_models_extra.py | 10 +- tests/unit/common/test_raster_grid_common.py | 8 +- tests/unit/common/test_recurrence_watcher.py | 8 +- .../test_face_detection_logic.py | 29 +- .../test_face_detection_service.py | 96 +-- .../test_loop_centering_analyzer.py | 63 +- .../test_loop_centering_service.py | 32 +- .../mounting/test_mounting_service.py | 18 +- .../screenshot/test_screenshot_service.py | 20 +- .../daq/operations/test_ml_raster_plan.py | 16 +- .../unit/daq/test_aare_daq_loop_centering.py | 14 +- tests/unit/daq/test_aaredb.py | 68 +- tests/unit/daq/test_auth.py | 49 +- tests/unit/daq/test_autofocus.py | 15 +- .../test_automation_progress_state_manager.py | 24 +- tests/unit/daq/test_beamcenterfit.py | 31 +- tests/unit/daq/test_face_detection.py | 33 +- tests/unit/daq/test_gui_timeout.py | 37 +- tests/unit/daq/test_mlbox.py | 3 +- tests/unit/daq/test_mount.py | 62 +- tests/unit/daq/test_raster_logic.py | 70 +- .../test_rotation_screening_diffraction.py | 6 +- tests/unit/daq/test_server.py | 90 +-- .../unit/daq/test_server_exception_handler.py | 29 +- tests/unit/daq/test_spreadsheetupdater.py | 22 +- tests/unit/daq/test_tell_state_updater.py | 12 +- tests/unit/daq/test_tellupdater.py | 62 +- tests/unit/daq/test_workflows.py | 12 +- tests/unit/devices/test_aerotech.py | 14 +- tests/unit/devices/test_enum_pv.py | 23 +- tests/unit/devices/test_jfjoch.py | 9 +- tests/unit/devices/test_mx_lib.py | 3 +- tests/unit/devices/test_my_motor.py | 105 +-- tests/unit/devices/test_tell_client.py | 8 +- tests/unit/devices/test_workflow_tools.py | 9 +- tests/unit/gui/test_auth_mock.py | 8 +- .../gui/test_automation_progress_parser.py | 12 +- tests/unit/gui/test_axis_video_thread.py | 22 +- .../unit/gui/test_data_collection_settings.py | 20 +- tests/unit/gui/test_login.py | 15 +- tests/unit/gui/test_message_box.py | 37 +- tests/unit/gui/test_models.py | 9 +- tests/unit/gui/test_panels.py | 4 +- tests/unit/gui/test_sse_client.py | 16 +- tests/unit/gui/test_tutorials.py | 44 +- tests/unit/gui/test_widgets.py | 19 +- 181 files changed, 2237 insertions(+), 5272 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 932cfdea..3c0422f6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,29 +5,29 @@ import os import sys -sys.path.insert(0, os.path.abspath('../src')) -sys.path.insert(0, os.path.abspath('../src/aare')) -sys.path.insert(0, os.path.abspath('../src/aare/daq')) -sys.path.insert(0, os.path.abspath('../src/aare/common')) -sys.path.insert(0, os.path.abspath('../src/aare/gui')) +sys.path.insert(0, os.path.abspath("../src")) +sys.path.insert(0, os.path.abspath("../src/aare")) +sys.path.insert(0, os.path.abspath("../src/aare/daq")) +sys.path.insert(0, os.path.abspath("../src/aare/common")) +sys.path.insert(0, os.path.abspath("../src/aare/gui")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'AareDAQ' -copyright = '2026, Paul Scherrer Institute' -author = 'Martin Appleby, Guillaume Gotthard, Filip Leonarski and Dawn (Jiaxin) Daun' -release = '0.3.1' -master_doc = 'index' -root_doc = 'index' +project = "AareDAQ" +copyright = "2026, Paul Scherrer Institute" +author = "Martin Appleby, Guillaume Gotthard, Filip Leonarski and Dawn (Jiaxin) Daun" +release = "0.3.1" +master_doc = "index" +root_doc = "index" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration autodoc_mock_imports = [ - 'aaredb', - 'jfjoch_client', - 'numpy', - 'requests', + "aaredb", + "jfjoch_client", + "numpy", + "requests", "pyJW", "pyzmq", "opencv-python-headless", @@ -43,16 +43,16 @@ autodoc_mock_imports = [ "psi-pshell", ] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] extensions = [ # 'fuse_search', - 'sphinx.ext.intersphinx', - 'myst_parser', - 'sphinx_immaterial', - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', + "sphinx.ext.intersphinx", + "myst_parser", + "sphinx_immaterial", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", ] # Optional: allow includes in Markdown @@ -63,11 +63,11 @@ myst_enable_extensions = [ "html_image", "linkify", "substitution", - 'smartquotes', + "smartquotes", "tasklist", ] -templates_path = ['_templates'] +templates_path = ["_templates"] myst_heading_anchor = 3 @@ -86,10 +86,10 @@ myst_heading_anchor = 3 # ] # } -#Static Files -html_static_path = ['_static'] +# Static Files +html_static_path = ["_static"] html_css_files = [ - 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css', + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" # 'css/fuse-search.css', ] # html_js_files = [ @@ -100,11 +100,11 @@ html_css_files = [ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_immaterial' +html_theme = "sphinx_immaterial" html_theme_options = { "icon": { - "repo": "fontawesome/brands/github", # or gitlab/gitea icon + "repo": "fontawesome/brands/github" # or gitlab/gitea icon }, "site_url": "https://gitea.psi.ch/mx/aaredaq", "repo_url": "https://gitea.psi.ch/mx/aaredaq", @@ -114,7 +114,7 @@ html_theme_options = { "icon": "fontawesome/brands/github", "link": "https://gitea.psi.ch/mx/aaredaq", "name": "AareDAQ on Gitea", - }, + } ], "palette": [ { @@ -122,20 +122,14 @@ html_theme_options = { "scheme": "default", "primary": "indigo", "accent": "lime", - "toggle": { - "icon": "material/lightbulb-outline", - "name": "Switch to dark mode", - }, + "toggle": {"icon": "material/lightbulb-outline", "name": "Switch to dark mode"}, }, { "media": "(prefers-color-scheme: dark)", "scheme": "slate", "primary": "indigo", "accent": "lime", - "toggle": { - "icon": "material/lightbulb", - "name": "Switch to light mode", - }, + "toggle": {"icon": "material/lightbulb", "name": "Switch to light mode"}, }, ], "features": [ @@ -152,14 +146,10 @@ html_theme_options = { "globaltoc_collapse": False, "toc_title": "On this page", "toc_title_container": True, - "font": { - "code": "JetBrains Mono", - }, + "font": {"code": "JetBrains Mono"}, } html_theme_options["google_fonts"] = [] -html_sidebars = { - "**": ["logo-text.html", "globaltoc.html", "searchbox.html"] -} +html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "searchbox.html"]} html_show_sourcelink = False @@ -167,8 +157,8 @@ html_show_sourcelink = False # Optional autodoc_default_options = { - 'members': True, - 'undoc-members': True, - 'show-inheritance': True, - 'member-order': 'bysource', + "members": True, + "undoc-members": True, + "show-inheritance": True, + "member-order": "bysource", } diff --git a/pyproject.toml b/pyproject.toml index a48bcaf1..69a5a8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,3 +65,18 @@ markers = [ [build-system] requires = ["setuptools>=75.6.0"] build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 100 +skip-magic-trailing-comma = true + +[tool.isort] +profile = "black" +line_length = 100 +multi_line_output = 3 + +[tool.ruff] +line-length = 100 + +[tool.ruff.format] +skip-magic-trailing-comma = true diff --git a/scripts/alc_bkg_comp.py b/scripts/alc_bkg_comp.py index efa844ef..1ead13b4 100644 --- a/scripts/alc_bkg_comp.py +++ b/scripts/alc_bkg_comp.py @@ -1,6 +1,9 @@ import cv2 -#%% -bkg_old = cv2.imread("/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50_old.jpg") + +# %% +bkg_old = cv2.imread( + "/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50_old.jpg" +) bkg_new = cv2.imread("/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50.jpg") grey_old = cv2.cvtColor(bkg_old, cv2.COLOR_RGB2GRAY) grey_new = cv2.cvtColor(bkg_new, cv2.COLOR_RGB2GRAY) @@ -8,17 +11,22 @@ diff_image = cv2.absdiff(grey_old, grey_new) _, thresh_diff = cv2.threshold(diff_image, 1, 255, cv2.THRESH_BINARY) # Display the results -cv2.imshow('Original Image 1', bkg_old) -cv2.imshow('Original Image 2', bkg_new) -cv2.imshow('Grayscale 1', grey_old) -cv2.imshow('Grayscale 2', grey_new) -cv2.imshow('Difference Image', diff_image) -cv2.imshow('Thresholded Difference', thresh_diff) +cv2.imshow("Original Image 1", bkg_old) +cv2.imshow("Original Image 2", bkg_new) +cv2.imshow("Grayscale 1", grey_old) +cv2.imshow("Grayscale 2", grey_new) +cv2.imshow("Difference Image", diff_image) +cv2.imshow("Thresholded Difference", thresh_diff) # Save the difference image -cv2.imwrite('/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_difference_image.jpg', diff_image) -cv2.imwrite('/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_thresholded_difference.jpg', thresh_diff) +cv2.imwrite( + "/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_difference_image.jpg", diff_image +) +cv2.imwrite( + "/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_thresholded_difference.jpg", + thresh_diff, +) # Wait for a key press and close all windows cv2.waitKey(0) -cv2.destroyAllWindows() \ No newline at end of file +cv2.destroyAllWindows() diff --git a/scripts/camera_stat_thread.py b/scripts/camera_stat_thread.py index 43dcdac8..0c97325c 100644 --- a/scripts/camera_stat_thread.py +++ b/scripts/camera_stat_thread.py @@ -58,10 +58,16 @@ class ImageStatsReceiver: x_coords = np.arange(len(x_projection)) y_coords = np.arange(len(y_projection)) - x_centroid = np.sum(x_coords * x_projection) / np.sum(x_projection) if np.sum(x_projection) > 0 else len( - x_projection) / 2 - y_centroid = np.sum(y_coords * y_projection) / np.sum(y_projection) if np.sum(y_projection) > 0 else len( - y_projection) / 2 + x_centroid = ( + np.sum(x_coords * x_projection) / np.sum(x_projection) + if np.sum(x_projection) > 0 + else len(x_projection) / 2 + ) + y_centroid = ( + np.sum(y_coords * y_projection) / np.sum(y_projection) + if np.sum(y_projection) > 0 + else len(y_projection) / 2 + ) # Calculate FWHM (Full Width at Half Maximum) approximation def calculate_fwhm(profile): @@ -76,20 +82,20 @@ class ImageStatsReceiver: y_fwhm = calculate_fwhm(y_projection) return { - 'x_projection': x_projection.tolist(), - 'y_projection': y_projection.tolist(), - 'x_projection_max': x_projection_max.tolist(), - 'y_projection_max': y_projection_max.tolist(), - 'x_peak_position': int(x_peak_idx), - 'y_peak_position': int(y_peak_idx), - 'x_peak_value': float(x_peak_value), - 'y_peak_value': float(y_peak_value), - 'x_centroid': float(x_centroid), - 'y_centroid': float(y_centroid), - 'x_fwhm': float(x_fwhm), - 'y_fwhm': float(y_fwhm), - 'x_coords': list(range(len(x_projection))), - 'y_coords': list(range(len(y_projection))) + "x_projection": x_projection.tolist(), + "y_projection": y_projection.tolist(), + "x_projection_max": x_projection_max.tolist(), + "y_projection_max": y_projection_max.tolist(), + "x_peak_position": int(x_peak_idx), + "y_peak_position": int(y_peak_idx), + "x_peak_value": float(x_peak_value), + "y_peak_value": float(y_peak_value), + "x_centroid": float(x_centroid), + "y_centroid": float(y_centroid), + "x_fwhm": float(x_fwhm), + "y_fwhm": float(y_fwhm), + "x_coords": list(range(len(x_projection))), + "y_coords": list(range(len(y_projection))), } def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> Dict[str, Any]: @@ -104,7 +110,7 @@ class ImageStatsReceiver: center_y, center_x = np.array(gray_image.shape) // 2 # Create coordinate grids - y_coords, x_coords = np.ogrid[:gray_image.shape[0], :gray_image.shape[1]] + y_coords, x_coords = np.ogrid[: gray_image.shape[0], : gray_image.shape[1]] # Calculate distance from center for each pixel distances = np.sqrt((x_coords - center_x) ** 2 + (y_coords - center_y) ** 2) @@ -137,13 +143,13 @@ class ImageStatsReceiver: pixel_counts.append(0) return { - 'r_centers': r_centers.tolist(), - 'radial_profile': radial_profile, - 'radial_std': radial_std, - 'pixel_counts': pixel_counts, - 'max_radius': float(max_radius), - 'center': [int(center_x), int(center_y)], - 'num_bins': num_bins + "r_centers": r_centers.tolist(), + "radial_profile": radial_profile, + "radial_std": radial_std, + "pixel_counts": pixel_counts, + "max_radius": float(max_radius), + "center": [int(center_x), int(center_y)], + "num_bins": num_bins, } def calculate_image_stats(self, image: np.ndarray) -> Dict[str, Any]: @@ -151,30 +157,30 @@ class ImageStatsReceiver: image_float = image.astype(np.float64) stats = { - 'timestamp': time.time(), - 'shape': image.shape, - 'dtype': str(image.dtype), - 'mean': float(np.mean(image_float)), - 'std': float(np.std(image_float)), - 'median': float(np.median(image_float)), - 'min': float(np.min(image_float)), - 'max': float(np.max(image_float)), - 'message_count': self.message_count + "timestamp": time.time(), + "shape": image.shape, + "dtype": str(image.dtype), + "mean": float(np.mean(image_float)), + "std": float(np.std(image_float)), + "median": float(np.median(image_float)), + "min": float(np.min(image_float)), + "max": float(np.max(image_float)), + "message_count": self.message_count, } # Calculate per-channel stats if color image if len(image.shape) == 3 and image.shape[2] > 1: for channel in range(image.shape[2]): channel_data = image_float[:, :, channel] - stats[f'mean_ch{channel}'] = float(np.mean(channel_data)) - stats[f'std_ch{channel}'] = float(np.std(channel_data)) - stats[f'median_ch{channel}'] = float(np.median(channel_data)) + stats[f"mean_ch{channel}"] = float(np.mean(channel_data)) + stats[f"std_ch{channel}"] = float(np.std(channel_data)) + stats[f"median_ch{channel}"] = float(np.median(channel_data)) radial_data = self.calculate_radial_integration(image) - stats['radial'] = radial_data + stats["radial"] = radial_data - #projection_data = self.calculate_projections(image) - #stats['projections'] = projection_data + # projection_data = self.calculate_projections(image) + # stats['projections'] = projection_data return stats @@ -200,7 +206,7 @@ class ImageStatsReceiver: rgb_image = rgb_image[:, ::-1, :].copy() stats = self.calculate_image_stats(rgb_image) - stats['image_type'] = 'rgb_from_bayer' + stats["image_type"] = "rgb_from_bayer" with self.stats_lock: self.latest_stats = stats @@ -213,7 +219,7 @@ class ImageStatsReceiver: rgb_image = rgb_image.reshape(header_shape) stats = self.calculate_image_stats(rgb_image) - stats['image_type'] = 'rgb_direct' + stats["image_type"] = "rgb_direct" with self.stats_lock: self.latest_stats = stats @@ -245,45 +251,53 @@ class ImageStatsReceiver: def print_formatted_stats(self, stats: Dict[str, Any]): """Print formatted statistics in a compact terminal format""" - timestamp = time.strftime('%H:%M:%S', time.localtime(stats['timestamp'])) + timestamp = time.strftime("%H:%M:%S", time.localtime(stats["timestamp"])) # Compact one-line format - print(f"[{timestamp}] #{stats['message_count']:4d} | " - f"Shape: {stats['shape']} | " - f"Mean: {stats['mean']:6.1f} | " - f"Std: {stats['std']:6.1f} | " - f"Median: {stats['median']:6.1f} | " - f"Range: [{stats['min']:3.0f}-{stats['max']:3.0f}]", - flush=True) + print( + f"[{timestamp}] #{stats['message_count']:4d} | " + f"Shape: {stats['shape']} | " + f"Mean: {stats['mean']:6.1f} | " + f"Std: {stats['std']:6.1f} | " + f"Median: {stats['median']:6.1f} | " + f"Range: [{stats['min']:3.0f}-{stats['max']:3.0f}]", + flush=True, + ) - if 'radial' in stats: - radial = stats['radial'] - center_intensity = radial['radial_profile'][0] if radial['radial_profile'] else 0 - edge_intensity = radial['radial_profile'][-1] if radial['radial_profile'] else 0 - peak_radius_idx = np.argmax(radial['radial_profile']) if radial['radial_profile'] else 0 - peak_radius = radial['r_centers'][peak_radius_idx] if radial['r_centers'] else 0 + if "radial" in stats: + radial = stats["radial"] + center_intensity = radial["radial_profile"][0] if radial["radial_profile"] else 0 + edge_intensity = radial["radial_profile"][-1] if radial["radial_profile"] else 0 + peak_radius_idx = np.argmax(radial["radial_profile"]) if radial["radial_profile"] else 0 + peak_radius = radial["r_centers"][peak_radius_idx] if radial["r_centers"] else 0 - print(f"{'':21} Radial: Center={center_intensity:.1f} | " - f"Edge={edge_intensity:.1f} | " - f"Peak@r={peak_radius:.1f} | " - f"Center=({radial['center'][0]},{radial['center'][1]})", - flush=True) + print( + f"{'':21} Radial: Center={center_intensity:.1f} | " + f"Edge={edge_intensity:.1f} | " + f"Peak@r={peak_radius:.1f} | " + f"Center=({radial['center'][0]},{radial['center'][1]})", + flush=True, + ) # Print projection summary - if 'projections' in stats: - proj = stats['projections'] - print(f"{'':21} X-Profile: Peak@{proj['x_peak_position']}({proj['x_peak_value']:.1f}) | " - f"Centroid={proj['x_centroid']:.1f} | FWHM={proj['x_fwhm']:.1f}", - flush=True) - print(f"{'':21} Y-Profile: Peak@{proj['y_peak_position']}({proj['y_peak_value']:.1f}) | " - f"Centroid={proj['y_centroid']:.1f} | FWHM={proj['y_fwhm']:.1f}", - flush=True) + if "projections" in stats: + proj = stats["projections"] + print( + f"{'':21} X-Profile: Peak@{proj['x_peak_position']}({proj['x_peak_value']:.1f}) | " + f"Centroid={proj['x_centroid']:.1f} | FWHM={proj['x_fwhm']:.1f}", + flush=True, + ) + print( + f"{'':21} Y-Profile: Peak@{proj['y_peak_position']}({proj['y_peak_value']:.1f}) | " + f"Centroid={proj['y_centroid']:.1f} | FWHM={proj['y_fwhm']:.1f}", + flush=True, + ) # Optional: Print per-channel stats if available - if any(k.startswith('mean_ch') for k in stats.keys()): + if any(k.startswith("mean_ch") for k in stats.keys()): channels = [] i = 0 - while f'mean_ch{i}' in stats: + while f"mean_ch{i}" in stats: channels.append(f"Ch{i}({stats[f'mean_ch{i}']:.1f})") i += 1 if channels: @@ -292,16 +306,23 @@ class ImageStatsReceiver: def get_radial_profile_summary(self): """Get a summary of the current radial profile""" with self.stats_lock: - if self.latest_stats and 'radial' in self.latest_stats: - radial = self.latest_stats['radial'] + if self.latest_stats and "radial" in self.latest_stats: + radial = self.latest_stats["radial"] return { - 'r_centers': radial['r_centers'], - 'radial_profile': radial['radial_profile'], - 'center_intensity': radial['radial_profile'][0] if radial['radial_profile'] else 0, - 'edge_intensity': radial['radial_profile'][-1] if radial['radial_profile'] else 0, - 'max_intensity_radius': radial['r_centers'][np.argmax(radial['radial_profile'])] if radial[ - 'radial_profile'] else 0, - 'max_intensity_value': max(radial['radial_profile']) if radial['radial_profile'] else 0 + "r_centers": radial["r_centers"], + "radial_profile": radial["radial_profile"], + "center_intensity": radial["radial_profile"][0] + if radial["radial_profile"] + else 0, + "edge_intensity": radial["radial_profile"][-1] + if radial["radial_profile"] + else 0, + "max_intensity_radius": radial["r_centers"][np.argmax(radial["radial_profile"])] + if radial["radial_profile"] + else 0, + "max_intensity_value": max(radial["radial_profile"]) + if radial["radial_profile"] + else 0, } return None @@ -311,21 +332,23 @@ class ImageStatsReceiver: filename = f"radial_profile_{int(time.time())}.txt" with self.stats_lock: - if self.latest_stats and 'radial' in self.latest_stats: - radial = self.latest_stats['radial'] + if self.latest_stats and "radial" in self.latest_stats: + radial = self.latest_stats["radial"] - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write("# Radial Integration Profile\n") f.write(f"# Timestamp: {time.ctime(self.latest_stats['timestamp'])}\n") f.write(f"# Image shape: {self.latest_stats['shape']}\n") f.write(f"# Center: {radial['center']}\n") f.write("# Radius(pixels)\tMean_Intensity\tStd_Intensity\tPixel_Count\n") - for i in range(len(radial['r_centers'])): - f.write(f"{radial['r_centers'][i]:.2f}\t" - f"{radial['radial_profile'][i]:.2f}\t" - f"{radial['radial_std'][i]:.2f}\t" - f"{radial['pixel_counts'][i]}\n") + for i in range(len(radial["r_centers"])): + f.write( + f"{radial['r_centers'][i]:.2f}\t" + f"{radial['radial_profile'][i]:.2f}\t" + f"{radial['radial_std'][i]:.2f}\t" + f"{radial['pixel_counts'][i]}\n" + ) print(f"Radial profile saved to {filename}") return filename @@ -419,6 +442,7 @@ def save_current_radial_profile(filename: str = None): return stats_receiver.save_radial_profile_to_file(filename) return None + # # Auto-start when daq.py is imported/run # if __name__ == "__main__": # # If running daq.py directly @@ -444,4 +468,4 @@ def save_current_radial_profile(filename: str = None): # # else: # # If daq.py is imported as a module, auto-start the receiver -# start_image_stats_receiver() \ No newline at end of file +# start_image_stats_receiver() diff --git a/scripts/free_busy_state.py b/scripts/free_busy_state.py index e53d4b08..27eb9e31 100644 --- a/scripts/free_busy_state.py +++ b/scripts/free_busy_state.py @@ -1,4 +1,5 @@ from aaredaq.config import BeamlineConfig from aaredaqlib.beamline import MXBeamline + c = BeamlineConfig(MXBeamline.X06DA) -c.state_busy = False \ No newline at end of file +c.state_busy = False diff --git a/scripts/gui_desginer.py b/scripts/gui_desginer.py index 228a503c..82cd2ca9 100644 --- a/scripts/gui_desginer.py +++ b/scripts/gui_desginer.py @@ -1,13 +1,17 @@ from PySide6.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QFrame, QPushButton, QScrollArea, QStackedWidget, + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QLabel, + QFrame, + QPushButton, + QScrollArea, + QStackedWidget, QSizePolicy, ) from PySide6.QtCore import Qt, QPointF, QRectF -from PySide6.QtGui import ( - QPainter, QColor, QPen, QLinearGradient, - QFont, QFontMetrics, -) +from PySide6.QtGui import QPainter, QColor, QPen, QLinearGradient, QFont, QFontMetrics import sys import math @@ -15,15 +19,15 @@ import math # --------------------------------------------------------------------------- # Colour palette # --------------------------------------------------------------------------- -BG = "#071018" -CARD_BG = "#0E1A26" -ACCENT = "#62D8C8" -ACCENT_DIM = "#1A3A36" -TEXT = "#F5F7FA" -SUBTEXT = "#8A9BB0" -BUTTON_BG = "#132131" -LED_OFF = "#1C2E3E" -LED_ON = ACCENT +BG = "#071018" +CARD_BG = "#0E1A26" +ACCENT = "#62D8C8" +ACCENT_DIM = "#1A3A36" +TEXT = "#F5F7FA" +SUBTEXT = "#8A9BB0" +BUTTON_BG = "#132131" +LED_OFF = "#1C2E3E" +LED_ON = ACCENT ACTIVE_STEP = "#FFFFFF" @@ -147,10 +151,7 @@ class LEDStages(QWidget): line_color = QColor(ACCENT) if i < self._active else QColor(LED_OFF) pen = QPen(line_color, 2) p.setPen(pen) - p.drawLine( - QPointF(cx + led_r + 3, cy), - QPointF(next_cx - led_r - 3, cy), - ) + p.drawLine(QPointF(cx + led_r + 3, cy), QPointF(next_cx - led_r - 3, cy)) # --- LED circle --- p.setPen(Qt.NoPen) @@ -162,14 +163,8 @@ class LEDStages(QWidget): pen = QPen(QColor(BG), 2) pen.setCapStyle(Qt.RoundCap) p.setPen(pen) - p.drawLine( - QPointF(cx - 4, cy), - QPointF(cx - 1, cy + 3), - ) - p.drawLine( - QPointF(cx - 1, cy + 3), - QPointF(cx + 4, cy - 3), - ) + p.drawLine(QPointF(cx - 4, cy), QPointF(cx - 1, cy + 3)) + p.drawLine(QPointF(cx - 1, cy + 3), QPointF(cx + 4, cy - 3)) elif i == self._active: # active — bright with glow ring glow_pen = QPen(QColor(ACCENT + "55"), 4) @@ -194,9 +189,7 @@ class LEDStages(QWidget): fm = QFontMetrics(font) text_w = fm.horizontalAdvance(name) p.drawText( - QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), - Qt.AlignCenter, - name, + QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), Qt.AlignCenter, name ) @@ -317,7 +310,9 @@ class QueueItemCard(QFrame): text_col.setContentsMargins(0, 0, 0, 0) title_lbl = QLabel(self._title) - title_lbl.setStyleSheet(f"color: {TEXT}; font-size: 13px; font-weight: 600; background: transparent;") + title_lbl.setStyleSheet( + f"color: {TEXT}; font-size: 13px; font-weight: 600; background: transparent;" + ) sub_lbl = QLabel(self._subtitle) sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; background: transparent;") @@ -399,7 +394,9 @@ class SampleCamera(QWidget): # ── Title ────────────────────────────────────────────────────── title = QLabel("S A M C A M E R A") title.setAlignment(Qt.AlignCenter) - title.setStyleSheet(f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 600;") + title.setStyleSheet( + f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 600;" + ) layout.addWidget(title) # ── Camera card ──────────────────────────────────────────────── @@ -422,7 +419,7 @@ class SampleCamera(QWidget): layout.addWidget(sub_lbl) # ── LED step indicator ───────────────────────────────────────── - self._leds = LEDStages(active_step=1) # 0=Mount done, 1=Centre active + self._leds = LEDStages(active_step=1) # 0=Mount done, 1=Centre active layout.addWidget(self._leds) # ── Transport controls ───────────────────────────────────────── @@ -435,7 +432,7 @@ class SampleCamera(QWidget): buttons = [ ("⏮", False), ("⏪", False), - ("⏸", True), # primary / highlighted + ("⏸", True), # primary / highlighted ("⏩", False), ("⏭", False), ] @@ -449,7 +446,9 @@ class SampleCamera(QWidget): # ── Up next header ───────────────────────────────────────────── up_next_row = QHBoxLayout() up_next_lbl = QLabel("UP NEXT") - up_next_lbl.setStyleSheet(f"color: {ACCENT}; font-size: 11px; letter-spacing: 2px; font-weight: 700;") + up_next_lbl.setStyleSheet( + f"color: {ACCENT}; font-size: 11px; letter-spacing: 2px; font-weight: 700;" + ) samples_lbl = QLabel("5 SAMPLES") samples_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; letter-spacing: 1px;") up_next_row.addWidget(up_next_lbl) @@ -459,11 +458,11 @@ class SampleCamera(QWidget): # ── Queue preview cards ──────────────────────────────────────── queue_items = [ - ("Crystal Plate 14 · Well C8", "Serial MX · 1 kHz", 4100, 6000, True), - ("Crystal Plate 15 · Grid Scan", "Raster · 10 Hz", 0, 2000, False), - ("Crystal Plate 13 · Well A1", "Serial MX · 1 kHz", 0, 5000, False), - ("Crystal Plate 13 · Well B3", "Serial MX · 1 kHz", 0, 4500, False), - ("Crystal Plate 14 · Well D5", "Serial MX · 1 kHz", 0, 6000, False), + ("Crystal Plate 14 · Well C8", "Serial MX · 1 kHz", 4100, 6000, True), + ("Crystal Plate 15 · Grid Scan", "Raster · 10 Hz", 0, 2000, False), + ("Crystal Plate 13 · Well A1", "Serial MX · 1 kHz", 0, 5000, False), + ("Crystal Plate 13 · Well B3", "Serial MX · 1 kHz", 0, 4500, False), + ("Crystal Plate 14 · Well D5", "Serial MX · 1 kHz", 0, 6000, False), ] queue_widget = QWidget() @@ -502,7 +501,9 @@ class SampleCamera(QWidget): title = QLabel("SAMPLE QUEUE") title.setAlignment(Qt.AlignCenter) - title.setStyleSheet(f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 700;") + title.setStyleSheet( + f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 700;" + ) layout.addWidget(title) scroll = QScrollArea() @@ -583,4 +584,4 @@ if __name__ == "__main__": app = QApplication(sys.argv) ui = SampleCamera() ui.show() - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) diff --git a/scripts/pshell_client_old.py b/scripts/pshell_client_old.py index aaa223bd..50ee1614 100644 --- a/scripts/pshell_client_old.py +++ b/scripts/pshell_client_old.py @@ -175,7 +175,8 @@ class PShellClient: """ return self._get_response( requests.put( - url=self.url + "/run", json={"script": script, "pars": pars, "background": background, "async": False} + url=self.url + "/run", + json={"script": script, "pars": pars, "background": background, "async": False}, ) ) @@ -191,7 +192,9 @@ class PShellClient: Command id (int), which is used to retrieve command execution status/result (get_result). """ statement = quote(statement) - return int(self._get_response(requests.get(url=self.url + "/evalAsync/" + statement), False)) + return int( + self._get_response(requests.get(url=self.url + "/evalAsync/" + statement), False) + ) def start_run(self, script, pars=None, background=False): """Starts execution of a script in the interpreter. @@ -279,9 +282,13 @@ class PShellClient: if type == "json": return self._get_response(requests.get(url=self.url + "/data-json/" + path), True) elif type == "bin": - return self._get_binary_response(requests.get(url=self.url + "/data-bin/" + path, stream=True)) + return self._get_binary_response( + requests.get(url=self.url + "/data-bin/" + path, stream=True) + ) - return self._get_response(requests.get(url=self.url + "/data" + ("" if path is None else ("/" + path))), False) + return self._get_response( + requests.get(url=self.url + "/data" + ("" if path is None else ("/" + path))), False + ) def print_logs(self): for log_line in self.get_logs(): diff --git a/scripts/redis_dump.py b/scripts/redis_dump.py index f9e40423..0d31b97a 100644 --- a/scripts/redis_dump.py +++ b/scripts/redis_dump.py @@ -99,15 +99,10 @@ def main(argv=None): help="Key pattern for SCAN (e.g., 'user:*'). If omitted, all keys are scanned.", ) parser.add_argument( - "--scan-count", - type=int, - default=1000, - help="Hint for SCAN per iteration (not a limit).", + "--scan-count", type=int, default=1000, help="Hint for SCAN per iteration (not a limit)." ) parser.add_argument( - "--include-ttl", - action="store_true", - help="Include TTL (seconds) for each key.", + "--include-ttl", action="store_true", help="Include TTL (seconds) for each key." ) args = parser.parse_args(argv) @@ -125,4 +120,4 @@ def main(argv=None): if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/src/aare/daq/aaredb.py b/src/aare/daq/aaredb.py index 2bc745be..f12d05c7 100644 --- a/src/aare/daq/aaredb.py +++ b/src/aare/daq/aaredb.py @@ -22,11 +22,7 @@ from aarecommon.models.models import ( SampleShortInfo, ScanResultPayloadModel, ) -from aarecommon.models.raster_grid import ( - CenterOfMassModel, - RasterGridRequest, - RasterPayloadModel, -) +from aarecommon.models.raster_grid import CenterOfMassModel, RasterGridRequest, RasterPayloadModel from aarecommon.models.rotation_scan import RotationScanRequest from aareDB import ( BeamlineParametersInput, @@ -48,19 +44,13 @@ logger = setup_logger("aareDAQ") class AareWrapper: - def __init__( - self, - bl: MXBeamline, - host: str = "https://mx-aaredb-dmz-01.psi.ch/dispatcher", - ): + def __init__(self, bl: MXBeamline, host: str = "https://mx-aaredb-dmz-01.psi.ch/dispatcher"): configuration = aareDB.Configuration(host=host) # --- mTLS & SSL CONFIGURATION --- # 1. Trust the Server (CA that signed mx-aaredb-dmz-01) configuration.verify_ssl = True - configuration.ssl_ca_cert = ( - "/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem" - ) + configuration.ssl_ca_cert = "/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem" # 2. Present Machine Identity (The certs that worked in curl) beamline_name = bl.value.lower() @@ -75,9 +65,7 @@ class AareWrapper: self.client = aareDB.ApiClient(configuration) # Identity Forwarding (Optional now that mTLS is active, but safe to keep) - self.client.default_headers["X-Shared-Password"] = os.getenv( - "AAREDB_SHARED_PASSWORD" - ) + self.client.default_headers["X-Shared-Password"] = os.getenv("AAREDB_SHARED_PASSWORD") self.__host = host self.__tell_api = aareDB.TellsRunnerApi(self.client) self.__sample_api = aareDB.SamplesRunnerApi(self.client) @@ -95,15 +83,11 @@ class AareWrapper: for i in input_list: t = SetTellPosition( - puck_name=i.puck_name, - segment=i.location.segment, - puck_in_segment=i.location.pos, + puck_name=i.puck_name, segment=i.location.segment, puck_in_segment=i.location.pos ) o.append(t) payload = SetTellPositionRequest(pucks=o, tell=self.__bl.value.upper()) - ret = self.__tell_api.set_tell_positions( - set_tell_position_request=payload, - ) + ret = self.__tell_api.set_tell_positions(set_tell_position_request=payload) logger.debug(ret) @log_timing(logger, "AareDB call") @@ -111,9 +95,7 @@ class AareWrapper: from aareDB.models import ManualSampleCreate manual_sample = ManualSampleCreate( - pgroup=s.user, - sample_name=s.sample_name, - data_collection_parameters=s.aaredb_params, + pgroup=s.user, sample_name=s.sample_name, data_collection_parameters=s.aaredb_params ) try: @@ -123,16 +105,11 @@ class AareWrapper: @log_timing(logger, "AareDB call") def send_sample_event( - self, - sample_id: StrictInt, - event_type: SampleEventType, - comment: Optional[str] = None, + self, sample_id: StrictInt, event_type: SampleEventType, comment: Optional[str] = None ) -> None: if sample_id is None or sample_id < 0: if sample_id is None: - logger.debug( - f"Sample db_id is None, skipping sample event {event_type!s}" - ) + logger.debug(f"Sample db_id is None, skipping sample event {event_type!s}") elif sample_id < 0: logger.debug( f"Sample db_id is invalid ({sample_id}), skipping sample event {event_type!s}" @@ -141,20 +118,14 @@ class AareWrapper: try: self.__sample_api.create_sample_event( sample_id=sample_id, - sample_event_create=SampleEventCreate( - event_type=event_type, comment=comment - ), + sample_event_create=SampleEventCreate(event_type=event_type, comment=comment), ) except Exception as e: logger.error(f"Error sending sample event {event_type!s} to db: {e}") @log_timing(logger, "AareDB call") def upload_image( - self, - sample_id: int, - filename: str, - bgr_image: np.ndarray, - message: Optional[str] = None, + self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None ): _, buffer = cv2.imencode(".jpg", bgr_image) jpeg_bytes = io.BytesIO(buffer) @@ -176,9 +147,7 @@ class AareWrapper: logger.debug(f"Response status code: {response.status_code}") @log_timing(logger, "AareDB call") - def upload_jpg( - self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None - ): + def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None): logger.debug(f"jppg_image of type: {type(jpg_image)}") url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images" headers = { @@ -226,9 +195,7 @@ class AareWrapper: ) characterization = None dataset = Datasets( - filepath=r.file_prefix, - status="written", - written_at=datetime.datetime.now(), + filepath=r.file_prefix, status="written", written_at=datetime.datetime.now() ) detector_data = DetectorParameters( manufacturer="DECTRIS", @@ -265,8 +232,7 @@ class AareWrapper: type="standard", beamline_parameters=beamline_params, sample_id=s.db_id ) response = self.__sample_api.create_experiment_parameters_for_sample( - sample_id=s.db_id, - experiment_parameters_create=experiment_params_payload, + sample_id=s.db_id, experiment_parameters_create=experiment_params_payload ) # logger.debug("Experiment parameters created:", response) except Exception as e: @@ -292,9 +258,7 @@ class AareWrapper: exposureTime_s=round(r.exp_time_s, 4), ) dataset = Datasets( - filepath=r.file_prefix, - status="written", - written_at=datetime.datetime.now(), + filepath=r.file_prefix, status="written", written_at=datetime.datetime.now() ) detector_data = DetectorParameters( manufacturer="DECTRIS", @@ -330,8 +294,7 @@ class AareWrapper: type="standard", beamline_parameters=beamline_params, sample_id=s.db_id ) response = self.__sample_api.create_experiment_parameters_for_sample( - sample_id=s.db_id, - experiment_parameters_create=experiment_params_payload, + sample_id=s.db_id, experiment_parameters_create=experiment_params_payload ) # logger.info("Experiment parameters created:", response) except Exception as e: @@ -416,9 +379,7 @@ class AareWrapper: for img in raster_result.images ] except Exception as e: - logger.warning( - f"raster score computation failed, sending null score: {e}" - ) + logger.warning(f"raster score computation failed, sending null score: {e}") score = None payload = RasterPayloadModel( diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 908ff930..00eed9cc 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -14,12 +14,7 @@ from aarecommon.errors.exception_handler import ( AuthErrorCode, UserRightsException, ) -from aarecommon.models.auth import ( - BatonRequest, - BatonRequestStatus, - BatonStatus, - BatonTransferQueue, -) +from aarecommon.models.auth import BatonRequest, BatonRequestStatus, BatonStatus, BatonTransferQueue from aarecommon.models.models import SessionsStateEnum from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -55,9 +50,7 @@ class TokenData(BaseModel): def create_access_token(token: TokenData): to_encode = token.model_dump() - to_encode.update( - {"exp": datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)} - ) + to_encode.update({"exp": datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -82,9 +75,7 @@ def authenticate_from_proxy_header(request: Request) -> str: only the Apache proxy running on the same host can supply it. """ client_host = request.client.host if request.client else None - logger.debug( - f"[auth] /token client_host={client_host!r} request.client={request.client!r}" - ) + logger.debug(f"[auth] /token client_host={client_host!r} request.client={request.client!r}") if not _is_loopback(client_host): logger.warning( f"[auth] Rejecting X-Remote-User: client_host {client_host!r} is not loopback" @@ -114,13 +105,9 @@ def authenticate_user(cfg: BeamlineConfig, username: str) -> str: super_user = username in SUPER_USERS pgroups = [group for group in supplementary_groups if group.startswith("p")] staff = ( - "unx-mxgroup" in supplementary_groups - or "unx-sls_mx" in supplementary_groups - or super_user - ) - token = TokenData( - sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session() + "unx-mxgroup" in supplementary_groups or "unx-sls_mx" in supplementary_groups or super_user ) + token = TokenData(sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session()) return create_access_token(token) @@ -169,9 +156,7 @@ def check_jwt_rw(cfg: BeamlineConfig, data: TokenData) -> None: def check_jwt_staff_only(data: TokenData) -> None: if not data.staff: raise UserRightsException( - message="Not member of the MX staff.", - status_code=403, - code=AuthErrorCode.NOT_STAFF, + message="Not member of the MX staff.", status_code=403, code=AuthErrorCode.NOT_STAFF ) @@ -201,9 +186,7 @@ def force_current_sesion(cfg: BeamlineConfig, data: TokenData) -> None: ) -def _finalize_expired_baton_request( - cfg: BeamlineConfig, pending: BatonRequest -) -> BatonStatus: +def _finalize_expired_baton_request(cfg: BeamlineConfig, pending: BatonRequest) -> BatonStatus: """ Resolve an expired baton request in one place. @@ -357,19 +340,10 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict: to_pgroup=cfg.pgroup, expiry_sec=SESSION_EXPIRE_SECONDS, ) - return { - "granted": True, - "override": True, - "message": "Staff override - baton acquired", - } + return {"granted": True, "override": True, "message": "Staff override - baton acquired"} # Policy check: non-staff requesting from staff - if ( - holder - and holder.is_staff - and not data.staff - and not cfg.allow_non_staff_request_from_staff - ): + if holder and holder.is_staff and not data.staff and not cfg.allow_non_staff_request_from_staff: return { "error": True, "message": "Requesting baton from staff is disabled by backend policy.", @@ -402,9 +376,7 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict: msg = f"Request sent to {holder.username if holder else 'current holder'}" if is_busy: - msg += ( - " (Note: beamline is currently busy, transfer will be queued if accepted)" - ) + msg += " (Note: beamline is currently busy, transfer will be queued if accepted)" return { "pending": True, @@ -415,9 +387,7 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict: } -def respond_to_baton_request( - cfg: BeamlineConfig, data: TokenData, accept: bool -) -> dict: +def respond_to_baton_request(cfg: BeamlineConfig, data: TokenData, accept: bool) -> dict: """ Current baton holder responds to a pending request. """ @@ -441,11 +411,7 @@ def respond_to_baton_request( to_pgroup=cfg.pgroup, expiry_sec=SESSION_EXPIRE_SECONDS, ) - return { - "accepted": True, - "transferred": True, - "message": "Baton transferred", - } + return {"accepted": True, "transferred": True, "message": "Baton transferred"} else: # Beamline busy, queue the transfer cfg.queued_baton_transfer = BatonTransferQueue( diff --git a/src/aare/daq/autofocus.py b/src/aare/daq/autofocus.py index cceaf17f..8e405ddd 100644 --- a/src/aare/daq/autofocus.py +++ b/src/aare/daq/autofocus.py @@ -2,7 +2,9 @@ import cv2 import numpy as np -def calculate_focus_measure(image: np.ndarray, center_x: float, center_y: float, radius: float) -> float: +def calculate_focus_measure( + image: np.ndarray, center_x: float, center_y: float, radius: float +) -> float: """ Calculate the focus measure within a circular region around a point. @@ -22,7 +24,7 @@ def calculate_focus_measure(image: np.ndarray, center_x: float, center_y: float, # Create a circular mask height, width = gray.shape y, x = np.ogrid[:height, :width] - mask = ((x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2) + mask = (x - center_x) ** 2 + (y - center_y) ** 2 <= radius**2 # Apply Laplacian operator laplacian = cv2.Laplacian(gray, cv2.CV_64F) diff --git a/src/aare/daq/beamcenterfit.py b/src/aare/daq/beamcenterfit.py index a03cd9d9..de0f815c 100644 --- a/src/aare/daq/beamcenterfit.py +++ b/src/aare/daq/beamcenterfit.py @@ -2,6 +2,7 @@ import numpy as np import cv2 from scipy.optimize import curve_fit + class Gaussian2Dfit: center_x: float center_y: float @@ -10,6 +11,7 @@ class Gaussian2Dfit: rotation_angle: float peak_intensity: float + def gaussian_2d(xy, x0, y0, sigma_x, sigma_y, theta, A, offset): x, y = xy x_rot = (x - x0) * np.cos(theta) + (y - y0) * np.sin(theta) @@ -17,7 +19,8 @@ def gaussian_2d(xy, x0, y0, sigma_x, sigma_y, theta, A, offset): gaussian = A * np.exp(-(x_rot**2 / (2 * sigma_x**2) + y_rot**2 / (2 * sigma_y**2))) + offset return gaussian.ravel() -def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit: + +def beamcenter_fit(image: np.ndarray) -> Gaussian2Dfit: """ Takes a np.ndarray image and returns a Gaussian2Dfit object containing the fitted parameters Returns None if the fit did not converge @@ -25,7 +28,9 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit: """ # Preprocess the image blurred_image = cv2.GaussianBlur(image, (5, 5), 0) - normalized_image = cv2.normalize(blurred_image, None, 0, 255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U) + normalized_image = cv2.normalize( + blurred_image, None, 0, 255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U + ) # Threshold to isolate the brightest spot (main blob) _, binary_mask = cv2.threshold(normalized_image, 180, 255, cv2.THRESH_BINARY) @@ -43,7 +48,7 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit: new_h = min(image.shape[0] - new_y, int(h * (1 + enlargement_factor))) # Update the ROI - roi = normalized_image[new_y:new_y + new_h, new_x:new_x + new_w] + roi = normalized_image[new_y : new_y + new_h, new_x : new_x + new_w] # Fit the 2D Gaussian X, Y = np.meshgrid(np.arange(roi.shape[1]), np.arange(roi.shape[0])) @@ -51,13 +56,13 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit: # Initial guess for Gaussian parameters initial_guess = ( - new_w / 2, # x0 - new_h / 2, # y0 - new_w / 4, # sigma_x - new_h / 4, # sigma_y - 0, # theta - np.max(roi), # A (peak intensity) - np.min(roi) # offset + new_w / 2, # x0 + new_h / 2, # y0 + new_w / 4, # sigma_x + new_h / 4, # sigma_y + 0, # theta + np.max(roi), # A (peak intensity) + np.min(roi), # offset ) # Flatten ROI for curve fitting @@ -71,10 +76,10 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit: # Extract fitted parameters x0, y0, sigma_x, sigma_y, ini_theta, intensity, _ = popt result = Gaussian2Dfit() - result.center_x = x0+new_x - result.center_y = y0+new_y + result.center_x = x0 + new_x + result.center_y = y0 + new_y result.sigma_x = sigma_x result.sigma_y = sigma_y - result.rotation_angle = np.degrees(ini_theta)%360 + result.rotation_angle = np.degrees(ini_theta) % 360 result.peak_intensity = intensity - return result \ No newline at end of file + return result diff --git a/src/aare/daq/config.py b/src/aare/daq/config.py index 36ee8161..b4abc7eb 100644 --- a/src/aare/daq/config.py +++ b/src/aare/daq/config.py @@ -130,9 +130,7 @@ class BeamlineConfig: return f"{self.__bl}:gui_session:{session}" def _write_gui_session( - self, - payload: OpenGuiSessionInfo, - expiry_sec: int | None = None, + self, payload: OpenGuiSessionInfo, expiry_sec: int | None = None ) -> None: expiry = int(expiry_sec or self.GUI_SESSION_EXPIRE_SECONDS) @@ -163,12 +161,7 @@ class BeamlineConfig: return None def touch_gui_session( - self, - *, - session: int, - username: str, - staff: bool = False, - expiry_sec: int, + self, *, session: int, username: str, staff: bool = False, expiry_sec: int ) -> OpenGuiSessionInfo: existing = self._read_gui_session(session) holder = self.baton_holder @@ -179,19 +172,11 @@ class BeamlineConfig: username=username, staff=staff, last_seen_ts=time.time(), - last_interaction_ts=existing.last_interaction_ts - if existing is not None - else None, + last_interaction_ts=existing.last_interaction_ts if existing is not None else None, close_requested=existing.close_requested if existing is not None else False, - close_requested_by=existing.close_requested_by - if existing is not None - else None, - close_requested_at=existing.close_requested_at - if existing is not None - else None, - close_grace_seconds=existing.close_grace_seconds - if existing is not None - else None, + close_requested_by=existing.close_requested_by if existing is not None else None, + close_requested_at=existing.close_requested_at if existing is not None else None, + close_grace_seconds=existing.close_grace_seconds if existing is not None else None, holds_baton=holds_baton, ) @@ -213,11 +198,7 @@ class BeamlineConfig: return payload def request_gui_close( - self, - *, - session: int, - requested_by: str, - grace_seconds: int = 60, + self, *, session: int, requested_by: str, grace_seconds: int = 60 ) -> OpenGuiSessionInfo | None: payload = self._read_gui_session(session) if payload is None: @@ -293,9 +274,7 @@ class BeamlineConfig: return None holder = self.baton_holder - payload.holds_baton = bool( - holder is not None and holder.session == payload.session - ) + payload.holds_baton = bool(holder is not None and holder.session == payload.session) return payload # Session and authentication management @@ -331,9 +310,7 @@ class BeamlineConfig: return baton.session def session_status(self, session: int) -> SessionStatus: - return SessionStatus( - session=self.session_state(session), current_pgroup=self.pgroup - ) + return SessionStatus(session=self.session_state(session), current_pgroup=self.pgroup) def session_state(self, session: int) -> SessionsStateEnum: holder = self.baton_holder @@ -362,9 +339,7 @@ class BeamlineConfig: return SessionsStateEnum.OwnedByElse def try_set_active_session(self, session: int, expiry_sec: int) -> None: - with redis_lock.Lock( - self.__client, f"{self.__bl}:active_session_lock", expire=10 - ): + with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10): active = self.active_session if active is None: self.__client.set(f"{self.__bl}:active_session", session) @@ -376,14 +351,10 @@ class BeamlineConfig: # TODO finish setting this up! def try_extend_active_session(self, session: int, expiry_sec: int) -> None: - with redis_lock.Lock( - self.__client, f"{self.__bl}:active_session_lock", expire=10 - ): + with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10): active = self.active_session if active is None: - raise Exception( - "There is no active session with given id. Try again later." - ) + raise Exception("There is no active session with given id. Try again later.") # if active == session: # self.__client.expire(f"{self.__bl}:active_session", expiry_sec, gt=True) @@ -402,9 +373,7 @@ class BeamlineConfig: ) def end_active_session(self, session: int) -> None: - with redis_lock.Lock( - self.__client, f"{self.__bl}:active_session_lock", expire=10 - ): + with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10): active = self.active_session if active is None: return @@ -414,9 +383,7 @@ class BeamlineConfig: def force_set_active_session(self, session: int, expiry_sec: int) -> None: # Ensure that there is no active try-set for active session - with redis_lock.Lock( - self.__client, f"{self.__bl}:active_session_lock", expire=10 - ): + with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10): self.__client.set(f"{self.__bl}:active_session", session) self.__client.expire(f"{self.__bl}:active_session", expiry_sec) @@ -481,9 +448,7 @@ class BeamlineConfig: if transfer is None: self.__client.delete(f"{self.__bl}:baton_transfer_queue") else: - self.__client.set( - f"{self.__bl}:baton_transfer_queue", transfer.model_dump_json() - ) + self.__client.set(f"{self.__bl}:baton_transfer_queue", transfer.model_dump_json()) def can_transfer_baton_now(self) -> bool: """Check if baton can be transferred (beamline not mid-operation).""" @@ -507,16 +472,11 @@ class BeamlineConfig: Atomically transfer the baton to a new holder. Use existing active_session_lock for consistency. """ - with redis_lock.Lock( - self.__client, f"{self.__bl}:active_session_lock", expire=10 - ): + with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10): self.__client.set(f"{self.__bl}:active_session", to_session) self.__client.expire(f"{self.__bl}:active_session", expiry_sec) self.baton_holder = BatonHolderInfo( - username=to_username, - session=to_session, - is_staff=to_is_staff, - pgroup=to_pgroup, + username=to_username, session=to_session, is_staff=to_is_staff, pgroup=to_pgroup ) # Clear any pending request or queued transfer self.clear_pending_baton_request() @@ -635,14 +595,11 @@ class BeamlineConfig: def pixel_to_mm(self, zoom: float) -> float: cfg = self.settings base_pixel_in_mm = 1.0 / ( - cfg.camera_translation_factor_b - * np.exp(cfg.camera_translation_factor_a * zoom) + cfg.camera_translation_factor_b * np.exp(cfg.camera_translation_factor_a * zoom) ) # Apply lens magnification correction relative to the default 10x lens. # A lower magnification lens (e.g. 5x) makes each pixel cover more physical space. - lens_magnification = cfg_get( - "daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION - ) + lens_magnification = cfg_get("daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION) try: lens_magnification = float(lens_magnification) except (TypeError, ValueError): @@ -659,15 +616,11 @@ class BeamlineConfig: z = ln(lens_factor / (b * target)) / a """ if target_pixel_in_mm <= 0: - raise ValueError( - f"target_pixel_in_mm must be > 0, got {target_pixel_in_mm}" - ) + raise ValueError(f"target_pixel_in_mm must be > 0, got {target_pixel_in_mm}") cfg = self.settings a = cfg.camera_translation_factor_a b = cfg.camera_translation_factor_b - lens_magnification = cfg_get( - "daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION - ) + lens_magnification = cfg_get("daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION) try: lens_magnification = float(lens_magnification) except (TypeError, ValueError): @@ -733,9 +686,7 @@ class BeamlineConfig: def settings(self, data: BeamlineSettingsModel): with redis_lock.Lock(self.__client, f"{self.__bl}:settings_lock", expire=10): current = self.__get_settings() - updated_data = current.model_copy( - update=data.model_dump(exclude_unset=True) - ) + updated_data = current.model_copy(update=data.model_dump(exclude_unset=True)) self.__client.set(f"{self.__bl}:settings", updated_data.model_dump_json()) @property @@ -752,14 +703,10 @@ class BeamlineConfig: self.__client.set(f"{self.__bl}:cryojet_settings", data.model_dump_json()) def get_alc_bkg(self, zoom: float, exp: float, gain: float) -> np.ndarray | None: - return base64_to_numpy( - self.__client.get(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}") - ) + return base64_to_numpy(self.__client.get(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}")) def put_alc_bkg(self, zoom: float, exp: float, gain: float, data: np.ndarray): - self.__client.set( - f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data) - ) + self.__client.set(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data)) @property def spreadsheet(self) -> SampleShortInfoList: @@ -881,15 +828,10 @@ class BeamlineConfig: mode = self.zoom_mode if not mode or not isinstance(mode, ZoomModeEnum): raise Exception("incorrect zoom settings mode used") - self.__client.set( - f"{self.__bl}:{self.zoom_setting_string(mode)}", data.model_dump_json() - ) + self.__client.set(f"{self.__bl}:{self.zoom_setting_string(mode)}", data.model_dump_json()) def save_zoom_camera_setting( - self, - zoom_value: float, - settings: SampleCameraSettings, - mode: ZoomModeEnum | None = None, + self, zoom_value: float, settings: SampleCameraSettings, mode: ZoomModeEnum | None = None ) -> None: """Insert/replace the camera settings for a single zoom stop in a given mode's table (persisted to Redis under that mode's key). Defaults to the @@ -897,11 +839,7 @@ class BeamlineConfig: mode = mode or self.zoom_mode key = f"{self.__bl}:{self.zoom_setting_string(mode)}" tmp = self.__client.get(key) - model = ( - ZoomModel(**json.loads(tmp)) - if tmp is not None - else zoom_manager(mode, self.__mxb) - ) + model = ZoomModel(**json.loads(tmp)) if tmp is not None else zoom_manager(mode, self.__mxb) model.z[zoom_value] = settings self.__client.set(key, model.model_dump_json()) @@ -960,8 +898,7 @@ class BeamlineConfig: def mark_beam(self, x_pxl: float, y_pxl: float, zoom: float): self.__client.hset( - f"{self.__bl}:beam_mark", - mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})}, + f"{self.__bl}:beam_mark", mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})} ) vals = self.__client.hgetall(f"{self.__bl}:beam_mark") @@ -1061,9 +998,7 @@ class BeamlineConfig: if input_params is None: self.__client.delete(f"{self.__bl}:simple_input_params") else: - self.__client.set( - f"{self.__bl}:simple_input_params", input_params.model_dump_json() - ) + self.__client.set(f"{self.__bl}:simple_input_params", input_params.model_dump_json()) @property def auto_params(self) -> SimpleScanParameters | None: @@ -1102,20 +1037,13 @@ class BeamlineConfig: seq = int(seq_raw) if seq_raw is not None else 0 progress = json.loads(payload_raw) if payload_raw else None - return { - "seq": seq, - "progress": progress, - } + return {"seq": seq, "progress": progress} - def set_automation_progress_state( - self, progress: AutomationProgress | dict - ) -> dict: + def set_automation_progress_state(self, progress: AutomationProgress | dict) -> dict: def _json_default(value): if isinstance(value, datetime): return value.isoformat() - raise TypeError( - f"Object of type {type(value).__name__} is not JSON serializable" - ) + raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") if isinstance(progress, dict): payload = dict(progress) @@ -1124,9 +1052,7 @@ class BeamlineConfig: elif is_dataclass(progress): payload = asdict(progress) else: - raise TypeError( - f"Unsupported automation progress type: {type(progress).__name__}" - ) + raise TypeError(f"Unsupported automation progress type: {type(progress).__name__}") next_seq = int(self.__client.incr(self._automation_progress_seq_key())) self.__client.set( @@ -1134,10 +1060,7 @@ class BeamlineConfig: json.dumps(payload, separators=(",", ":"), default=_json_default), ) - return { - "seq": next_seq, - "progress": payload, - } + return {"seq": next_seq, "progress": payload} @property def failed_mount_count(self) -> int: @@ -1262,12 +1185,8 @@ class BeamlineConfig: def set_detector_metadata(self, payload: dict) -> dict: safe_payload = dict(payload or {}) - safe_payload["dtz_low"] = self._coerce_optional_float( - safe_payload.get("dtz_low") - ) - safe_payload["dtz_high"] = self._coerce_optional_float( - safe_payload.get("dtz_high") - ) + safe_payload["dtz_low"] = self._coerce_optional_float(safe_payload.get("dtz_low")) + safe_payload["dtz_high"] = self._coerce_optional_float(safe_payload.get("dtz_high")) safe_payload["pixel_size_mm"] = self._coerce_optional_float( safe_payload.get("pixel_size_mm") ) diff --git a/src/aare/daq/config_model.py b/src/aare/daq/config_model.py index c8ec14a0..3882d5eb 100644 --- a/src/aare/daq/config_model.py +++ b/src/aare/daq/config_model.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field + class LocalContactConfigModel(BaseModel): mount_to_center_sleep_s: float = Field( default=0.0, @@ -26,4 +27,4 @@ class LocalContactConfigModel(BaseModel): "the ML prediction is loop_all. 0.5 means add 50% of the detected " "cell count above and below." ), - ) \ No newline at end of file + ) diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 88ba79fe..e80fc444 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -88,9 +88,7 @@ from aare.daq.operations.common.services import ( StateController, TraceWriter, ) -from aare.daq.operations.common.simulate_scan_result import ( - build_fake_rotation_result, -) +from aare.daq.operations.common.simulate_scan_result import build_fake_rotation_result from aare.daq.operations.face_detection import ( FaceDetectionContext, FaceDetectionResult, @@ -100,10 +98,7 @@ from aare.daq.operations.face_detection.models import ( FaceDetectionDependencies, FaceDetectionSettings, ) -from aare.daq.operations.loop_centering import ( - LoopCenteringContext, - LoopCenteringService, -) +from aare.daq.operations.loop_centering import LoopCenteringContext, LoopCenteringService from aare.daq.operations.loop_centering.models import ( LoopCenteringDependencies, LoopCenteringSettings, @@ -115,11 +110,7 @@ from aare.daq.operations.mounting.models import ( MountingSettings, ) from aare.daq.operations.mounting.service import MountingService -from aare.daq.operations.raster.models import ( - RasterContext, - RasterDependencies, - RasterSettings, -) +from aare.daq.operations.raster.models import RasterContext, RasterDependencies, RasterSettings from aare.daq.operations.raster.service import RasterService from aare.daq.operations.rotation.models import ( RotationContext, @@ -157,15 +148,9 @@ class _DAQNonCriticalRunner: self._daq = daq def __call__( - self, - action, - *, - description: str, - sample: SampleShortInfo | None = None, + self, action, *, description: str, sample: SampleShortInfo | None = None ) -> object | None: - return self._daq._run_noncritical( - action, description=description, sample=sample - ) + return self._daq._run_noncritical(action, description=description, sample=sample) class _DAQScreenshotSampleProvider: @@ -215,9 +200,7 @@ class _DAQSampleEventSender: def __init__(self, daq: "AareDAQ"): self._daq = daq - def send_sample_event( - self, sample_id: int, event_type, comment: str | None = None - ) -> None: + def send_sample_event(self, sample_id: int, event_type, comment: str | None = None) -> None: self._daq._AareDAQ__aare.send_sample_event(sample_id, event_type, comment) @@ -227,10 +210,7 @@ class _DAQScanIngestor: def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None: self._daq._AareDAQ__aare.ingest_scan( - sample=sample, - result=result, - geom=geom, - beam_mark_pxl=beam_mark_pxl, + sample=sample, result=result, geom=geom, beam_mark_pxl=beam_mark_pxl ) def ingest_gridscan( @@ -303,9 +283,7 @@ class AareDAQ: self._beamline = bl self.__aare = AareWrapper(bl) self.__saved_box = None - self._smargon_trace_path = ( - Path("/sls/mx/applications/logs") / "smargon_trace.csv" - ) + self._smargon_trace_path = Path("/sls/mx/applications/logs") / "smargon_trace.csv" self._face_detection_progress_cb: Callable[[dict], None] | None = None self._automation_progress_cb: Callable[[AutomationProgress], None] | None = None self._last_sample_sync_ts = 0.0 @@ -366,14 +344,8 @@ class AareDAQ: sim = self.get_runtime_simulation_state() return { - "bec": { - "mode": "simulated" if sim.get("bec") else "live", - "error": None, - }, - "detector": { - "mode": "simulated" if sim.get("detector") else "live", - "error": None, - }, + "bec": {"mode": "simulated" if sim.get("bec") else "live", "error": None}, + "detector": {"mode": "simulated" if sim.get("detector") else "live", "error": None}, "tell": { "mode": "simulated" if sim.get("tell") else "live", "error": getattr(status, "tell_error", None), @@ -392,25 +364,15 @@ class AareDAQ: self.__cfg.try_set_busy(timeout=360) try: self.__devs.restart_bec_worker(simulated=self.__cfg.simulate_bec) - return { - "ok": True, - "device": "bec", - "simulated": self.__cfg.simulate_bec, - } + return {"ok": True, "device": "bec", "simulated": self.__cfg.simulate_bec} finally: self.__cfg.state_busy = False def restart_detector(self) -> dict[str, object]: self.__cfg.try_set_busy(timeout=360) try: - beamline = ( - MXBeamline.SIMULATED - if self.__cfg.simulated_detector - else self._beamline - ) - logger.info( - f"Restarting JFJoch wrapper with simulated={self.__cfg.simulated_detector}" - ) + beamline = MXBeamline.SIMULATED if self.__cfg.simulated_detector else self._beamline + logger.info(f"Restarting JFJoch wrapper with simulated={self.__cfg.simulated_detector}") self.__jfjoch = JFJochWrapper(beamline) return { "ok": True, @@ -424,11 +386,7 @@ class AareDAQ: self.__cfg.try_set_busy(timeout=360) try: self.__devs.restart_tell(simulated=self.__cfg.simulate_tell) - return { - "ok": True, - "device": "tell", - "simulated": self.__cfg.simulate_tell, - } + return {"ok": True, "device": "tell", "simulated": self.__cfg.simulate_tell} finally: self.__cfg.state_busy = False @@ -436,11 +394,7 @@ class AareDAQ: self.__cfg.try_set_busy(timeout=360) try: self.__devs.restart_aerotech(simulated=self.__cfg.simulate_aerotech) - return { - "ok": True, - "device": "aerotech", - "simulated": self.__cfg.simulate_aerotech, - } + return {"ok": True, "device": "aerotech", "simulated": self.__cfg.simulate_aerotech} finally: self.__cfg.state_busy = False @@ -448,11 +402,7 @@ class AareDAQ: self.__cfg.try_set_busy(timeout=360) try: self.__devs.restart_smargon(simulated=self.__cfg.simulate_smargon) - return { - "ok": True, - "device": "smargon", - "simulated": self.__cfg.simulate_smargon, - } + return {"ok": True, "device": "smargon", "simulated": self.__cfg.simulate_smargon} finally: self.__cfg.state_busy = False @@ -509,18 +459,13 @@ class AareDAQ: or "must be idle" in text ) - def _raise_if_critical_jfjoch_detector_error( - self, - error: Exception, - *, - command: str, - ) -> None: + def _raise_if_critical_jfjoch_detector_error(self, error: Exception, *, command: str) -> None: if not isinstance(error, JFJochCommunicationError): return - if getattr( - error, "status_code", None - ) != 500 and not self._is_jfjoch_detector_state_error(str(error)): + if getattr(error, "status_code", None) != 500 and not self._is_jfjoch_detector_state_error( + str(error) + ): return message = ( @@ -539,12 +484,7 @@ class AareDAQ: critical=True, ) from error - def _raise_if_critical_bec_error( - self, - error: Exception, - *, - command: str, - ) -> None: + def _raise_if_critical_bec_error(self, error: Exception, *, command: str) -> None: if not isinstance(error, BECCommunicationError): return @@ -650,9 +590,7 @@ class AareDAQ: return [] @staticmethod - def _tell_phase_confirms_previous_sample_unmounted( - phase: TellPhaseEnum | None, - ) -> bool: + def _tell_phase_confirms_previous_sample_unmounted(phase: TellPhaseEnum | None) -> bool: return phase in { TellPhaseEnum.OLD_SAMPLE_RETURNED, TellPhaseEnum.PICKING_NEW_SAMPLE, @@ -669,9 +607,7 @@ class AareDAQ: state_ts is not None and state_ts >= started_at and tell_state.operation == "mount" - and self._tell_phase_confirms_previous_sample_unmounted( - tell_state.phase - ) + and self._tell_phase_confirms_previous_sample_unmounted(tell_state.phase) ): return True @@ -683,24 +619,18 @@ class AareDAQ: return True for event in reversed(self._get_tell_events_from_redis()): - if ( - event.get("class") == "Motion Sync" - and event.get("event") == "Sample put on Puck" - ): + if event.get("class") == "Motion Sync" and event.get("event") == "Sample put on Puck": event_ts = self._parse_iso_timestamp(event.get("timestamp")) if event_ts is not None and event_ts >= started_at: return True return False - def set_face_detection_progress_callback( - self, cb: Callable[[dict], None] | None - ) -> None: + def set_face_detection_progress_callback(self, cb: Callable[[dict], None] | None) -> None: self._face_detection_progress_cb = cb def set_automation_progress_callback( - self, - cb: Callable[[AutomationProgress], None] | None, + self, cb: Callable[[AutomationProgress], None] | None ) -> None: self._automation_progress_cb = cb @@ -750,8 +680,7 @@ class AareDAQ: context=event_context, ) logger.warning( - "Best-effort automation step failed; continuing workflow", - extra=event_context, + "Best-effort automation step failed; continuing workflow", extra=event_context ) def _new_automation_progress(self) -> AutomationProgress: @@ -759,21 +688,16 @@ class AareDAQ: current_step=None, steps=[ StepState(step=WorkflowStateKind.MOUNT, status=StepStatus.PENDING), - StepState( - step=WorkflowStateKind.LOOP_CENTRE, status=StepStatus.PENDING - ), + StepState(step=WorkflowStateKind.LOOP_CENTRE, status=StepStatus.PENDING), StepState(step=WorkflowStateKind.RASTER, status=StepStatus.PENDING), - StepState( - step=WorkflowStateKind.DATA_COLLECTION, status=StepStatus.PENDING - ), + StepState(step=WorkflowStateKind.DATA_COLLECTION, status=StepStatus.PENDING), StepState(step=WorkflowStateKind.FINAL, status=StepStatus.PENDING), ], finished=False, success=None, samples_in_queue=self._automation_samples_in_queue, avg_time_per_sample=( - self._automation_total_sample_time_s - / self._automation_completed_samples + self._automation_total_sample_time_s / self._automation_completed_samples if self._automation_completed_samples > 0 else 0.0 ), @@ -792,9 +716,7 @@ class AareDAQ: return labels.get(step, str(step.value)) def _get_progress_step( - self, - progress: AutomationProgress, - step: WorkflowStateKind, + self, progress: AutomationProgress, step: WorkflowStateKind ) -> StepState | None: for item in progress.steps: if item.step == step: @@ -822,17 +744,12 @@ class AareDAQ: if self._automation_completed_samples > 0: progress.avg_time_per_sample = ( - self._automation_total_sample_time_s - / self._automation_completed_samples + self._automation_total_sample_time_s / self._automation_completed_samples ) else: progress.avg_time_per_sample = 0.0 - def _record_completed_sample_time( - self, - progress: AutomationProgress, - elapsed_s: float, - ) -> None: + def _record_completed_sample_time(self, progress: AutomationProgress, elapsed_s: float) -> None: if elapsed_s <= 0: return self._automation_completed_samples += 1 @@ -880,32 +797,17 @@ class AareDAQ: self._emit_automation_progress(progress) def _mark_progress_running( - self, - progress: AutomationProgress, - step: WorkflowStateKind, - message: str = "", + self, progress: AutomationProgress, step: WorkflowStateKind, message: str = "" ) -> None: - self._set_progress_step( - progress, - step, - StepStatus.RUNNING, - message, - make_current=True, - ) + self._set_progress_step(progress, step, StepStatus.RUNNING, message, make_current=True) def _mark_progress_success( - self, - progress: AutomationProgress, - step: WorkflowStateKind, - message: str = "", + self, progress: AutomationProgress, step: WorkflowStateKind, message: str = "" ) -> None: self._set_progress_step(progress, step, StepStatus.SUCCESS, message) def _mark_progress_failed( - self, - progress: AutomationProgress, - step: WorkflowStateKind, - message: str = "", + self, progress: AutomationProgress, step: WorkflowStateKind, message: str = "" ) -> None: self._set_progress_step( progress, @@ -928,15 +830,10 @@ class AareDAQ: self._emit_automation_progress(progress) def _mark_progress_finished( - self, - progress: AutomationProgress, - success: bool, - message: str = "", + self, progress: AutomationProgress, success: bool, message: str = "" ) -> None: final_status = StepStatus.SUCCESS if success else StepStatus.FAILED - self._set_progress_step( - progress, WorkflowStateKind.FINAL, final_status, message - ) + self._set_progress_step(progress, WorkflowStateKind.FINAL, final_status, message) progress.current_step = self._step_display_name(WorkflowStateKind.FINAL) progress.finished = True progress.success = success @@ -956,21 +853,11 @@ class AareDAQ: def _build_operation_services(self) -> OperationServices: return OperationServices( screenshots=self._screenshot_service, - state=StateController( - setter=_DAQStateSetter(self), - ), - traces=TraceWriter( - appender=_DAQTraceAppender(self), - ), - events=SampleEventPublisher( - sender=_DAQSampleEventSender(self), - ), - ingestion=ScanIngestionService( - ingestor=_DAQScanIngestor(self), - ), - datacollection=DataCollectionPreparer( - runner=_DAQDatacollectionSetupRunner(self), - ), + state=StateController(setter=_DAQStateSetter(self)), + traces=TraceWriter(appender=_DAQTraceAppender(self)), + events=SampleEventPublisher(sender=_DAQSampleEventSender(self)), + ingestion=ScanIngestionService(ingestor=_DAQScanIngestor(self)), + datacollection=DataCollectionPreparer(runner=_DAQDatacollectionSetupRunner(self)), ) def _create_loop_centering_settings(self) -> LoopCenteringSettings: @@ -980,15 +867,13 @@ class AareDAQ: settings = self._create_loop_centering_settings() services = self._build_operation_services() services.predictions = PredictionProvider( - getter=_LoopCenteringPredictionGetter(self, settings), + getter=_LoopCenteringPredictionGetter(self, settings) ) return LoopCenteringService( context=LoopCenteringContext( deps=LoopCenteringDependencies( - cfg=self.__cfg, - devs=self.__devs, - mlbox=self.__mlbox, + cfg=self.__cfg, devs=self.__devs, mlbox=self.__mlbox ), runtime=self._build_runtime_state(), services=services, @@ -1000,15 +885,13 @@ class AareDAQ: def _create_face_detection_service(self) -> FaceDetectionService: services = self._build_operation_services() services.face_detection_progress = FaceDetectionProgressEmitter( - reporter=_FaceDetectionProgressReporter(self), + reporter=_FaceDetectionProgressReporter(self) ) return FaceDetectionService( context=FaceDetectionContext( deps=FaceDetectionDependencies( - cfg=self.__cfg, - devs=self.__devs, - mlbox=self.__mlbox, + cfg=self.__cfg, devs=self.__devs, mlbox=self.__mlbox ), runtime=self._build_runtime_state(), services=services, @@ -1020,13 +903,8 @@ class AareDAQ: def _create_mounting_service(self) -> MountingService: return MountingService( context=MountingContext( - deps=MountingDependencies( - cfg=self.__cfg, - devs=self.__devs, - ), - settings=MountingSettings( - mount_position=ABR_POS_MOUNT, - ), + deps=MountingDependencies(cfg=self.__cfg, devs=self.__devs), + settings=MountingSettings(mount_position=ABR_POS_MOUNT), ), logger=logger, ) @@ -1056,10 +934,7 @@ class AareDAQ: return RotationService( context=RotationContext( deps=RotationDependencies( - cfg=self.__cfg, - devs=self.__devs, - jfjoch=self.__jfjoch, - aare=self.__aare, + cfg=self.__cfg, devs=self.__devs, jfjoch=self.__jfjoch, aare=self.__aare ), runtime=self._build_runtime_state(), services=self._build_operation_services(), @@ -1113,9 +988,7 @@ class AareDAQ: try: self.__aare.send_sample_event( - sample_id=sample_id, - event_type=event_type, - comment=comment, + sample_id=sample_id, event_type=event_type, comment=comment ) except Exception as db_error: logger.exception( @@ -1148,8 +1021,7 @@ class AareDAQ: if previous_sample is None or previous_sample.db_id is None: try: previous_sample = self.sync_current_sample_from_tell( - force=True, - clear_cached_on_empty=False, + force=True, clear_cached_on_empty=False ) except Exception as sync_error: logger.warning( @@ -1157,9 +1029,7 @@ class AareDAQ: ) if previous_sample is not None and previous_sample.db_id is not None: - self.__aare.send_sample_event( - previous_sample.db_id, SampleEventType.UNMOUNTING - ) + self.__aare.send_sample_event(previous_sample.db_id, SampleEventType.UNMOUNTING) self.__set_state(BeamlineStateEnum.RobotSampleExchange) @@ -1167,9 +1037,7 @@ class AareDAQ: self.__aare.send_sample_event(sample.db_id, SampleEventType.MOUNTING) self.__devs.tell.blower_on() - mounting_result: MountingResult = self._create_mounting_service().execute( - target=sample - ) + mounting_result: MountingResult = self._create_mounting_service().execute(target=sample) if not mounting_result.success: raise mounting_result.error or MountingFailed( @@ -1184,19 +1052,13 @@ class AareDAQ: and previous_sample is not None and previous_sample.db_id is not None ): - self.__aare.send_sample_event( - previous_sample.db_id, SampleEventType.UNMOUNTED - ) + self.__aare.send_sample_event(previous_sample.db_id, SampleEventType.UNMOUNTED) self.__set_state(BeamlineStateEnum.SampleAlignment) if mounted_sample is not None and mounted_sample.db_id is not None: - self.__aare.send_sample_event( - mounted_sample.db_id, SampleEventType.MOUNTED - ) - self.save_screenshot_db( - mounted_sample.db_id, f"{mounted_sample.db_id}_mounted" - ) + self.__aare.send_sample_event(mounted_sample.db_id, SampleEventType.MOUNTED) + self.save_screenshot_db(mounted_sample.db_id, f"{mounted_sample.db_id}_mounted") return True @@ -1204,9 +1066,7 @@ class AareDAQ: self._last_mount_error_message = str(e) or "Mount failed" logger.error(f"Mount failed due to invalid transformation: {e}") self._handle_operation_error( - operation=DAQOperation.MOUNT - if sample is not None - else DAQOperation.UNMOUNT, + operation=DAQOperation.MOUNT if sample is not None else DAQOperation.UNMOUNT, sample=sample, error=e, event_type=SampleEventType.MOUNTFAILED, @@ -1217,9 +1077,7 @@ class AareDAQ: self._last_mount_error_message = str(e) or "Mount failed" logger.error(f"Tell communication error occured: {e}") self._handle_operation_error( - operation=DAQOperation.MOUNT - if sample is not None - else DAQOperation.UNMOUNT, + operation=DAQOperation.MOUNT if sample is not None else DAQOperation.UNMOUNT, sample=sample, error=e, event_type=SampleEventType.MOUNTFAILED, @@ -1254,9 +1112,7 @@ class AareDAQ: logger.exception("Failed to set state to SampleAlignment") finally: self._handle_operation_error( - operation=DAQOperation.MOUNT - if sample is not None - else DAQOperation.UNMOUNT, + operation=DAQOperation.MOUNT if sample is not None else DAQOperation.UNMOUNT, sample=sample, error=e, event_type=SampleEventType.MOUNTFAILED, @@ -1293,9 +1149,7 @@ class AareDAQ: sample=sample, error=result.error or Exception("Loop centering failed"), event_type=SampleEventType.ALCFAILED, - additional_comment=result.comment - if result.comment is not None - else "", + additional_comment=result.comment if result.comment is not None else "", ) return False @@ -1306,9 +1160,7 @@ class AareDAQ: except Exception as e: logger.error(f"Loop centering failed: {e}") if result is not None and result.error is not None: - additional_comment = ( - result.comment if result.comment is not None else "" - ) + additional_comment = result.comment if result.comment is not None else "" self._handle_operation_error( operation=DAQOperation.LOOP_CENTERING, sample=sample, @@ -1339,9 +1191,7 @@ class AareDAQ: if sample is None: try: sample = self.sample - logger.debug( - f"No sample provided, using current sample from DAQ {sample}" - ) + logger.debug(f"No sample provided, using current sample from DAQ {sample}") except Exception: logger.error("Failed to get current sample") sample = None @@ -1351,9 +1201,7 @@ class AareDAQ: aare.send_sample_event(sample.db_id, SampleEventType.LOOPFACEDETECTING) result = self._create_face_detection_service().run( - steps=steps, - step_size=step_size, - face_min_ratio=face_min_ratio, + steps=steps, step_size=step_size, face_min_ratio=face_min_ratio ) if not result.success: @@ -1392,18 +1240,10 @@ class AareDAQ: payload = ( result.payload if result is not None - else { - "running": False, - "samples": [], - "height_fit": {}, - "area_fit": {}, - } + else {"running": False, "samples": [], "height_fit": {}, "area_fit": {}} ) return FaceDetectionResult( - success=False, - payload=payload, - error=e, - comment=additional_comment, + success=False, payload=payload, error=e, comment=additional_comment ) def _execute_raster_sequence( @@ -1456,9 +1296,7 @@ class AareDAQ: self.__jfjoch.measure_raster(grid_request, status) logger.info("detector initialised") else: - logger.info( - "Simulated detector mode enabled; using fake raster result." - ) + logger.info("Simulated detector mode enabled; using fake raster result.") self.__set_state(BeamlineStateEnum.DataCollection) raster_result = raster_service.execute(grid_request) @@ -1487,8 +1325,7 @@ class AareDAQ: logger.error( "Raster sequence returned no result", extra=merge_log_context( - sample_log_context(self.sample), - raster_request_log_context(grid_request), + sample_log_context(self.sample), raster_request_log_context(grid_request) ), ) return result @@ -1497,8 +1334,7 @@ class AareDAQ: logger.exception( "Raster sequence failed due to JFJoch communication error", extra=merge_log_context( - sample_log_context(self.sample), - raster_request_log_context(grid_request), + sample_log_context(self.sample), raster_request_log_context(grid_request) ), ) self._handle_operation_error( @@ -1617,9 +1453,7 @@ class AareDAQ: try: return self._create_rotation_service().run(rotation_request) except JFJochCommunicationError as e: - logger.error( - f"Rotation sequence failed due to JFJoch Communication error: {e}" - ) + logger.error(f"Rotation sequence failed due to JFJoch Communication error: {e}") self._handle_operation_error( operation=DAQOperation.ROTATION, sample=self.sample, @@ -1672,9 +1506,7 @@ class AareDAQ: logger.warning(f"Failed to append smargon trace: {e}") def _sample_matches_mounted_address( - self, - sample: SampleShortInfo | None, - mounted_address, + self, sample: SampleShortInfo | None, mounted_address ) -> bool: if sample is None or sample.location is None or mounted_address is None: return False @@ -1684,9 +1516,7 @@ class AareDAQ: and sample.pin == mounted_address.pin ) - def _find_sample_by_mounted_address( - self, mounted_address - ) -> SampleShortInfo | None: + def _find_sample_by_mounted_address(self, mounted_address) -> SampleShortInfo | None: for sample in self.__cfg.spreadsheet.s: if self._sample_matches_mounted_address(sample, mounted_address): return sample @@ -1697,9 +1527,7 @@ class AareDAQ: return None - def _placeholder_sample_from_mounted_address( - self, mounted_address - ) -> SampleShortInfo: + def _placeholder_sample_from_mounted_address(self, mounted_address) -> SampleShortInfo: return SampleShortInfo( db_id=-1, puck_name="", @@ -1723,9 +1551,7 @@ class AareDAQ: return "sample" def sync_current_sample_from_tell( - self, - force: bool = False, - clear_cached_on_empty: bool = True, + self, force: bool = False, clear_cached_on_empty: bool = True ) -> SampleShortInfo | None: current_sample = self.__cfg.current_sample @@ -1733,10 +1559,7 @@ class AareDAQ: return current_sample now = time.monotonic() - if ( - not force - and (now - self._last_sample_sync_ts) < self._sample_sync_min_interval_s - ): + if not force and (now - self._last_sample_sync_ts) < self._sample_sync_min_interval_s: return current_sample self._last_sample_sync_ts = now @@ -1745,9 +1568,7 @@ class AareDAQ: if mounted_address is None: if current_sample is not None and current_sample.location is not None: if clear_cached_on_empty: - logger.warning( - "TELL reports no mounted sample; clearing cached current_sample" - ) + logger.warning("TELL reports no mounted sample; clearing cached current_sample") self.__cfg.current_sample = None else: logger.warning( @@ -1760,9 +1581,7 @@ class AareDAQ: resolved_sample = self._find_sample_by_mounted_address(mounted_address) if resolved_sample is None: - resolved_sample = self._placeholder_sample_from_mounted_address( - mounted_address - ) + resolved_sample = self._placeholder_sample_from_mounted_address(mounted_address) logger.warning( "Mounted sample from TELL was not found in known sample lists; using placeholder", extra={"mounted_address": str(mounted_address)}, @@ -1842,9 +1661,7 @@ class AareDAQ: if (res := getattr(aaredb_params, "targetresolution", None)) is not None: logger.debug(f"resolution: {res}") - logger.debug( - f"requested dtz: {self.diffraction_geometry.calc_dtz_mm(res)} " - ) + logger.debug(f"requested dtz: {self.diffraction_geometry.calc_dtz_mm(res)} ") new_res = 1 / ((1 / res) + 0.1) corrected_dtz = self.diffraction_geometry.calc_dtz_mm(new_res) logger.debug(f"corrected dtz: {corrected_dtz}") @@ -1941,9 +1758,7 @@ class AareDAQ: the beam-location preset (Redis), so it is re-applied on future zooms.""" zoom_value = self.zoom settings = self.samcam_settings - self.__cfg.save_zoom_camera_setting( - zoom_value, settings, mode=ZoomModeEnum.BeamLocation - ) + self.__cfg.save_zoom_camera_setting(zoom_value, settings, mode=ZoomModeEnum.BeamLocation) logger.info( f"Saved beam-location camera setting for zoom {zoom_value}: " f"gain={settings.gain}, exposure={settings.exposure}" @@ -1996,9 +1811,7 @@ class AareDAQ: def tweak_abr_meas_pos(self, c: AerotechCoordinate): self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment) try: - new_meas_pos = AerotechCoordinate( - at_mm=self.__cfg.abr_meas_pos.at_mm + c.at_mm - ) + new_meas_pos = AerotechCoordinate(at_mm=self.__cfg.abr_meas_pos.at_mm + c.at_mm) self.__cfg.abr_meas_pos = new_meas_pos self.__devs.aerotech_pos = new_meas_pos self.__saved_box = None @@ -2010,9 +1823,7 @@ class AareDAQ: def save_abr_meas_pos(self): self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment) try: - self.__cfg.abr_meas_pos = AerotechCoordinate( - at_mm=self.__devs.aerotech_pos.at_mm - ) + self.__cfg.abr_meas_pos = AerotechCoordinate(at_mm=self.__devs.aerotech_pos.at_mm) self.__devs.bec_worker.save_current_aerotech_position() self.__cfg.state_busy = False except Exception: @@ -2124,12 +1935,8 @@ class AareDAQ: current_sample = target or self.__cfg.current_sample sample_name = self._sample_mount_display_name(current_sample) if target is None: - raise UnmountingFailed( - f"Failed to {operation_name.lower()} {sample_name}" - ) - raise MountingFailed( - f"Failed to {operation_name.lower()} {sample_name}" - ) + raise UnmountingFailed(f"Failed to {operation_name.lower()} {sample_name}") + raise MountingFailed(f"Failed to {operation_name.lower()} {sample_name}") logger.info(f"Sample operation completed: {target}") self.__cfg.state_busy = False @@ -2142,9 +1949,7 @@ class AareDAQ: def list_loaded_pucks(self) -> List[PuckLoadedInfo]: return [] - def __auto_focus( - self, settings: AutofocusSettings, settle_time_s: float = 1.0 - ) -> float: + def __auto_focus(self, settings: AutofocusSettings, settle_time_s: float = 1.0) -> float: # TODO uses old code change """ Scan smargon Z and find the position with maximum focus measure. @@ -2262,23 +2067,16 @@ class AareDAQ: # self.__devs.transmission.wait() return - def _build_fake_rotation_result( - self, request: RotationScanRequest - ) -> CompletedRotationScan: + def _build_fake_rotation_result(self, request: RotationScanRequest) -> CompletedRotationScan: start_angle = 0.0 try: start_angle = float(self.omega) except Exception: pass - return build_fake_rotation_result( - request, - start_angle=start_angle, - ) + return build_fake_rotation_result(request, start_angle=start_angle) - def measure_raster( - self, request: RasterGridRequest, auto_center: bool - ) -> CompletedRasterGrid: + def measure_raster(self, request: RasterGridRequest, auto_center: bool) -> CompletedRasterGrid: """ Execute a raster scan. @@ -2292,8 +2090,7 @@ class AareDAQ: logger.info( "Received raster scan request", extra=merge_log_context( - sample_log_context(self.sample), - raster_request_log_context(request), + sample_log_context(self.sample), raster_request_log_context(request) ), ) self.__cfg.try_set_busy(timeout=ceil(360)) @@ -2325,9 +2122,7 @@ class AareDAQ: try: self.__jfjoch.wait_till_running(timeout=60.0) except Exception as e: - self._raise_if_critical_jfjoch_detector_error( - e, command="wait_till_running" - ) + self._raise_if_critical_jfjoch_detector_error(e, command="wait_till_running") raise try: if request.screening: @@ -2363,22 +2158,15 @@ class AareDAQ: self.__devs.aerotech_omega = omega_start if self.__cfg.simulated_detector: - logger.warning( - "Detector in simulation mode, returning fake zero rotation result." - ) + logger.warning("Detector in simulation mode, returning fake zero rotation result.") return self._build_fake_rotation_result(request) else: try: scan_result = self.__jfjoch.wait_till_done(60) except Exception as e: - self._raise_if_critical_jfjoch_detector_error( - e, command="wait_till_done" - ) + self._raise_if_critical_jfjoch_detector_error(e, command="wait_till_done") raise - return CompletedRotationScan( - request=copy.deepcopy(request), - result=scan_result, - ) + return CompletedRotationScan(request=copy.deepcopy(request), result=scan_result) except JFJochCommunicationError as e: logger.error(f"Exception during rotation scan related to JFJoch: {e}") @@ -2412,18 +2200,13 @@ class AareDAQ: if result is None: logger.error("Rotation scan failed, no result returned") - raise DataCollectionException( - "Rotation scan failed, no result returned" - ) + raise DataCollectionException("Rotation scan failed, no result returned") self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False return result finally: try: - if ( - self.__cfg.state_busy - and self.__cfg.state != BeamlineStateEnum.Maintenance - ): + if self.__cfg.state_busy and self.__cfg.state != BeamlineStateEnum.Maintenance: self.__set_state(BeamlineStateEnum.SampleAlignment) except Exception as cleanup_error: logger.exception( @@ -2436,9 +2219,7 @@ class AareDAQ: def dtz(self) -> float: tmp = self.__cfg.dtz if tmp is None: - return cfg_get( - "daq.data_collection_settings.default_raster_scan_settings.dtz", 200 - ) + return cfg_get("daq.data_collection_settings.default_raster_scan_settings.dtz", 200) else: return tmp @@ -2451,16 +2232,12 @@ class AareDAQ: dtz_high = self.__cfg.cached_dtz_high if dtz_low is None or dtz_high is None: - logger.warning( - "DTZ limits not found in cache, refreshing hardware metadata" - ) + logger.warning("DTZ limits not found in cache, refreshing hardware metadata") try: self.refresh_detector_metadata_cache() except Exception as e: self.__cfg.state_busy = False - raise RuntimeError( - f"DTZ limits unavailable and refresh failed: {e}" - ) from e + raise RuntimeError(f"DTZ limits unavailable and refresh failed: {e}") from e dtz_low = self.__cfg.cached_dtz_low dtz_high = self.__cfg.cached_dtz_high @@ -2596,10 +2373,7 @@ class AareDAQ: self.__cfg.try_set_busy(timeout=360) try: result = self._execute_face_detection( - steps=steps, - step_size=step_size, - face_min_ratio=face_min_ratio, - report_error=True, + steps=steps, step_size=step_size, face_min_ratio=face_min_ratio, report_error=True ) return result.payload finally: @@ -2670,9 +2444,7 @@ class AareDAQ: """ self._screenshot_service.save_local(filename, settle_time_s) - def save_screenshot_db( - self, sample_id: int, filename: str, settle_time_s: float = 0.2 - ): + def save_screenshot_db(self, sample_id: int, filename: str, settle_time_s: float = 0.2): """ Capture a screenshot and upload it to the database for a specific sample. @@ -2683,9 +2455,7 @@ class AareDAQ: """ self._screenshot_service.save_to_db(sample_id, filename, settle_time_s) - def send_screenshot_db( - self, filename: str | None = None, message: str | None = None - ) -> None: + def send_screenshot_db(self, filename: str | None = None, message: str | None = None) -> None: sample = self.sample if sample is None: raise ValueError("No sample with a valid sample_id is mounted.") @@ -2713,25 +2483,18 @@ class AareDAQ: # sample = copy.deepcopy(self.sample_spreadsheet) #switch to copy if too heavy! # sample.s = list(filter(lambda x: x.user == pgroup, sample.s)) # return sample - return SampleShortInfoList( - s=[x for x in self.sample_spreadsheet.s if x.user == pgroup] - ) + return SampleShortInfoList(s=[x for x in self.sample_spreadsheet.s if x.user == pgroup]) def get_beamline_default_raster_params(self) -> SimpleScanParameters: default_exp_time_s = cfg_get( "daq.data_collection_settings.default_raster_scan_settings.exp_time_s", 0.01 ) default_transmission = cfg_get( - "daq.data_collection_settings.default_raster_scan_settings.transmission", - 1.0, - ) - default_dtz = cfg_get( - "daq.data_collection_settings.default_raster_scan_settings.dtz", 250 + "daq.data_collection_settings.default_raster_scan_settings.transmission", 1.0 ) + default_dtz = cfg_get("daq.data_collection_settings.default_raster_scan_settings.dtz", 250) return SimpleScanParameters( - dtz=default_dtz, - exp_time_s=default_exp_time_s, - transmission=default_transmission, + dtz=default_dtz, exp_time_s=default_exp_time_s, transmission=default_transmission ) def get_beamline_default_rotation_params(self) -> SimpleScanParameters: @@ -2741,12 +2504,9 @@ class AareDAQ: default_transmission = cfg_get( "daq.data_collection_settings.default_rotation_settings.transmission", 1.0 ) - default_dtz = cfg_get( - "daq.data_collection_settings.default_rotation_settings.dtz", 250 - ) + default_dtz = cfg_get("daq.data_collection_settings.default_rotation_settings.dtz", 250) default_start_omega_deg = cfg_get( - "daq.data_collection_settings.default_rotation_settings.start_omega_deg", - 0.0, + "daq.data_collection_settings.default_rotation_settings.start_omega_deg", 0.0 ) default_increment_omega_deg = cfg_get( "daq.data_collection_settings.default_rotation_settings.incr_omega_deg", 0.2 @@ -2814,15 +2574,11 @@ class AareDAQ: return params - def get_collection_params( - self, prefer_smart: bool = False - ) -> tuple[SimpleScanParameters, str]: + def get_collection_params(self, prefer_smart: bool = False) -> tuple[SimpleScanParameters, str]: spreadsheet_params, file_prefix = self.spreadsheet_params() logger.debug(f"spreadsheet_params: {spreadsheet_params}") smart_params = self.__cfg.auto_params - default_params = SimpleScanParameters( - exp_time_s=0.04, dtz=110, incr_omega_deg=0.2 - ) + default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2) # if file_prefix is not None: # default_params.file_prefix = file_prefix # self.__aare.send_msg_to_db(self.sample,event_type=SampleEventType(''), comment=f'smart_params: {smart_params}') @@ -2900,8 +2656,7 @@ class AareDAQ: sample_started_at = time.time() progress = self._new_automation_progress() self._set_progress_context( - progress, - current_sample_name=getattr(sample, "sample_name", "") or "", + progress, current_sample_name=getattr(sample, "sample_name", "") or "" ) self._emit_automation_progress(progress) @@ -2914,44 +2669,29 @@ class AareDAQ: self._validate_automation_state(context="automation start") self.__cfg.try_set_busy(timeout=self.AUTOMATION_BUSY_TIMEOUT_S) - self._validate_automation_state( - context="after acquiring automation busy state" - ) + self._validate_automation_state(context="after acquiring automation busy state") logger.info("Cancelling any pending jfjoch operations") self.__jfjoch.cancel() self._set_progress_context( - progress, - current_sample_name=getattr(sample, "sample_name", "") or "", + progress, current_sample_name=getattr(sample, "sample_name", "") or "" ) - self._mark_progress_running( - progress, WorkflowStateKind.MOUNT, "Mounting sample" - ) + self._mark_progress_running(progress, WorkflowStateKind.MOUNT, "Mounting sample") if not self._execute_mount_and_prepare(sample): mount_error_message = self._last_mount_error_message or "Mount failed" - self._mark_progress_failed( - progress, WorkflowStateKind.MOUNT, mount_error_message - ) + self._mark_progress_failed(progress, WorkflowStateKind.MOUNT, mount_error_message) return self._end_operation(start, DAQOperation.MOUNT, error=True) self._validate_automation_state(context="after mount") - self._mark_progress_success( - progress, WorkflowStateKind.MOUNT, "Mount complete" - ) + self._mark_progress_success(progress, WorkflowStateKind.MOUNT, "Mount complete") self.__set_state(BeamlineStateEnum.SampleAlignment) - self._validate_automation_state( - context="after transition to SampleAlignment" - ) + self._validate_automation_state(context="after transition to SampleAlignment") logger.info(f"mounting done at {time.perf_counter() - start}") - self._mark_progress_running( - progress, WorkflowStateKind.LOOP_CENTRE, "Centering sample" - ) + self._mark_progress_running(progress, WorkflowStateKind.LOOP_CENTRE, "Centering sample") local_contact_config = self.get_local_contact_config() - mount_to_center_sleep_s = float( - local_contact_config.mount_to_center_sleep_s - ) + mount_to_center_sleep_s = float(local_contact_config.mount_to_center_sleep_s) if mount_to_center_sleep_s > 0: logger.info( @@ -2992,17 +2732,13 @@ class AareDAQ: logger.info(f"Loop Centering done at {time.perf_counter() - start}") face_detection_result = self._execute_face_detection( - steps=7, - step_size=30, - face_min_ratio=0.3, - report_error=True, + steps=7, step_size=30, face_min_ratio=0.3, report_error=True ) if not face_detection_result.success: logger.warning( "Face detection failed during automation; continuing with latest payload", extra=merge_log_context( - sample_log_context(sample), - {"comment": face_detection_result.comment}, + sample_log_context(sample), {"comment": face_detection_result.comment} ), ) self._validate_automation_state(context="after face detection") @@ -3012,9 +2748,7 @@ class AareDAQ: progress, WorkflowStateKind.LOOP_CENTRE, "Centering complete" ) - self._mark_progress_running( - progress, WorkflowStateKind.RASTER, "Running raster" - ) + self._mark_progress_running(progress, WorkflowStateKind.RASTER, "Running raster") hex_string = secrets.token_hex(3) raster_params = self.get_auto_raster_params() geom = self.sample_geometry @@ -3026,17 +2760,13 @@ class AareDAQ: n_x=1, n_y=1, dtz=raster_params.dtz, - grid_size_mm=Coordinate( - x=geom.beam_size_mm.x * 0.5, y=geom.beam_size_mm.y * 0.5 - ), + grid_size_mm=Coordinate(x=geom.beam_size_mm.x * 0.5, y=geom.beam_size_mm.y * 0.5), omega_deg=self.omega, transmission=raster_params.transmission, ) try: - raster_result = self._execute_raster_sequence( - raster_grid, auto_center=True - ) + raster_result = self._execute_raster_sequence(raster_grid, auto_center=True) except AutoRasterSampleSkipped as e: logger.warning( "Skipping sample during automation because auto-raster grid is too large", @@ -3047,10 +2777,7 @@ class AareDAQ: ), ) self._set_progress_step( - progress, - WorkflowStateKind.RASTER, - StepStatus.SKIPPED, - str(e), + progress, WorkflowStateKind.RASTER, StepStatus.SKIPPED, str(e) ) self._set_progress_step( progress, @@ -3067,23 +2794,16 @@ class AareDAQ: logger.error( "Raster result was None during automation", extra=merge_log_context( - sample_log_context(sample), - raster_request_log_context(raster_grid), + sample_log_context(sample), raster_request_log_context(raster_grid) ), ) - self._mark_progress_failed( - progress, WorkflowStateKind.RASTER, "Raster failed" - ) + self._mark_progress_failed(progress, WorkflowStateKind.RASTER, "Raster failed") return self._end_operation(start, DAQOperation.RASTER, error=True) self._validate_automation_state(context="after raster") - self._mark_progress_success( - progress, WorkflowStateKind.RASTER, "Raster complete" - ) + self._mark_progress_success(progress, WorkflowStateKind.RASTER, "Raster complete") self.__set_state(BeamlineStateEnum.DataCollection) - self._validate_automation_state( - context="after transition to DataCollection" - ) + self._validate_automation_state(context="after transition to DataCollection") logger.info(f"Raster scans completed at {time.perf_counter() - start}") @@ -3123,14 +2843,10 @@ class AareDAQ: self._validate_automation_state(context="automation end") except BECCommunicationError as e: - self._raise_if_critical_bec_error( - e, command=getattr(e, "operation", None) or "bec" - ) + self._raise_if_critical_bec_error(e, command=getattr(e, "operation", None) or "bec") except JFJochCommunicationError as e: - self._raise_if_critical_jfjoch_detector_error( - e, command=e.endpoint or "unknown" - ) + self._raise_if_critical_jfjoch_detector_error(e, command=e.endpoint or "unknown") raise except ( @@ -3249,9 +2965,7 @@ class AareDAQ: if target == BeamlineStateEnum.Maintenance: self.__cfg.state = BeamlineStateEnum.Maintenance - logger.warning( - "Beamline entered Maintenance state during state transition request." - ) + logger.warning("Beamline entered Maintenance state during state transition request.") return elif target == curr_state: logger.debug( @@ -3423,8 +3137,8 @@ class AareDAQ: # the preset for the current zoom on entry. if target == BeamlineStateEnum.BeamLocation: self.__cfg.zoom_mode = ZoomModeEnum.BeamLocation - self.__devs.samcam_settings = ( - self.__cfg.zoom_settings.get_camera_settings(self.__devs.zoom) + self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings( + self.__devs.zoom ) elif self.__cfg.zoom_mode == ZoomModeEnum.BeamLocation: self.__cfg.zoom_mode = ZoomModeEnum.User @@ -3467,12 +3181,8 @@ class AareDAQ: width = int(metadata.get("detector_width", 1)) height = int(metadata.get("detector_height", 1)) pixel_size_mm = float(metadata.get("pixel_size_mm", 0.15)) - detector_description = str( - metadata.get("detector_description", "unavailable") - ) - detector_serial_number = str( - metadata.get("detector_serial_number", "unavailable") - ) + detector_description = str(metadata.get("detector_description", "unavailable")) + detector_serial_number = str(metadata.get("detector_serial_number", "unavailable")) energy = self.__devs.energy_kev dtz = self.__devs.dtz beam_center = self.__cfg.beam_center @@ -3538,9 +3248,7 @@ class AareDAQ: "DTZ limits missing from cache, using conservative defaults in beamline_status" ) dtz_min = cfg_get("daq.hardware.default_detector_distance_minimum", 100) - dtz_max = cfg_get( - "daq.hardware.default_detector_distance_maximum", 1000 - ) + dtz_max = cfg_get("daq.hardware.default_detector_distance_maximum", 1000) logger.warning(f"using dtz_min {dtz_min} and dtz_max {dtz_max}") return BeamlineStatus( @@ -3590,9 +3298,7 @@ class AareDAQ: aerotech_err = f"Cannot connect to Aerotech: {e}" return aerotech_ok, aerotech_err - def _safe_geom( - self, - ) -> tuple[SampleGeometryModel, bool, str | None, bool, str | None]: + def _safe_geom(self) -> tuple[SampleGeometryModel, bool, str | None, bool, str | None]: """ Return (geom, smargon_connected, smargon_error, aerotech_connected, aerotech_error) without raising. Uses a conservative fallback geometry if Smargon access fails. @@ -3629,21 +3335,13 @@ class AareDAQ: pixel_in_mm=self.__cfg.pixel_to_mm(zoom), omega_deg=0.0, smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0.0, y=0.0, z=0.0), - phi_deg=0.0, - chi_deg=0.0, + sh_mm=Coordinate(x=0.0, y=0.0, z=0.0), phi_deg=0.0, chi_deg=0.0 ), beam_size_mm=self.__cfg.beam_size_mm, aerotech=Coordinate(x=0.0, y=0.0, z=0.0), aerotech_meas=Coordinate(x=0.0, y=0.0, z=0.0), ) - return ( - fallback, - smargon_connected, - smargon_error, - aerotech_connected, - aerotech_error, - ) + return (fallback, smargon_connected, smargon_error, aerotech_connected, aerotech_error) def _safe_beamline_status(self) -> BeamlineStatus: try: @@ -3699,9 +3397,7 @@ class AareDAQ: safe_sample, tell_ok, tell_err = self._safe_sample() # logger.debug(f"Safe sample info call took {time.perf_counter() - og_start:.3f}s") # start = time.perf_counter() - safe_geom, smargon_ok, smargon_err, aerotech_ok, aerotech_err = ( - self._safe_geom() - ) + safe_geom, smargon_ok, smargon_err, aerotech_ok, aerotech_err = self._safe_geom() # logger.debug(f"safe geom call took {time.perf_counter() - start:.3f}s") # start = time.perf_counter() safe_tell_state = self._safe_tell_state() @@ -3793,15 +3489,11 @@ class AareDAQ: return [] return [str(item) for item in devices] - def bec_reinitialise_planner_and_position_devices( - self, method: str = "auto" - ) -> list[str]: + def bec_reinitialise_planner_and_position_devices(self, method: str = "auto") -> list[str]: self.__cfg.try_set_busy(timeout=360) try: self.__devs.bec_worker.load_user_macros() - return self.__devs.bec_worker.reinitialise_planner_and_position_devices( - method=method - ) + return self.__devs.bec_worker.reinitialise_planner_and_position_devices(method=method) finally: self.__cfg.state_busy = False @@ -3850,7 +3542,5 @@ class AareDAQ: def get_local_contact_config(self) -> LocalContactConfigModel: return self.__cfg.get_local_contact_config() - def set_local_contact_config( - self, config: LocalContactConfigModel - ) -> LocalContactConfigModel: + def set_local_contact_config(self, config: LocalContactConfigModel) -> LocalContactConfigModel: return self.__cfg.set_local_contact_config(config) diff --git a/src/aare/daq/devices.py b/src/aare/daq/devices.py index 50e8d1fe..c7f204a5 100644 --- a/src/aare/daq/devices.py +++ b/src/aare/daq/devices.py @@ -76,9 +76,7 @@ class BeamlineDevices: self.__ringcurrent = self.bec_worker.ring_current self.__zoom = SetGetPV( - name=f"zoom", - setpv=f"{BEAMLINE}-ES-MS:ZOOM.VAL", - getpv=f"{BEAMLINE}-ES-MS:ZOOM.RBV", + name=f"zoom", setpv=f"{BEAMLINE}-ES-MS:ZOOM.VAL", getpv=f"{BEAMLINE}-ES-MS:ZOOM.RBV" ) self.__cryojet_pos = EnumPV( @@ -88,9 +86,7 @@ class BeamlineDevices: timeout=10.0, ) - self.__cryojet_x = MyMotor( - f"{BEAMLINE}-ES-CS:TRX" - ) # currently in is 5 out is 15? + self.__cryojet_x = MyMotor(f"{BEAMLINE}-ES-CS:TRX") # currently in is 5 out is 15? self.__cryojet_temperature_get = PV(f"{BEAMLINE}-ES-CS:TEMP_RBV") self.__cryojet_temperature_set = PV(f"{BEAMLINE}-ES-CS:TEMP.VAL") @@ -121,9 +117,7 @@ class BeamlineDevices: try: self.bec_worker.shutdown_client() except Exception as e: - logger.warning( - f"Failed to shutdown previous BEC worker cleanly: {e}" - ) + logger.warning(f"Failed to shutdown previous BEC worker cleanly: {e}") finally: beamline = MXBeamline.SIMULATED if simulated else self._beamline logger.info(f"Restarting BEC worker with simulated={simulated}") @@ -251,8 +245,7 @@ class BeamlineDevices: @property def samcam_settings(self) -> SampleCameraSettings: return SampleCameraSettings( - gain=self.__sample_cam.gain_rbv.value, - exposure=self.__sample_cam.expo_rbv.value, + gain=self.__sample_cam.gain_rbv.value, exposure=self.__sample_cam.expo_rbv.value ) @samcam_settings.setter @@ -314,17 +307,9 @@ class BeamlineDevices: @aerotech_pos.setter def aerotech_pos( - self, - coord: AerotechCoordinate, - /, - wait: bool = True, - incremental: bool = False, + self, coord: AerotechCoordinate, /, wait: bool = True, incremental: bool = False ): - self.aerotech.position( - coord, - wait=wait, - incremental=incremental, - ) + self.aerotech.position(coord, wait=wait, incremental=incremental) @property def aerotech_omega(self) -> float: @@ -334,19 +319,9 @@ class BeamlineDevices: def aerotech_omega(self, val: float): self.set_aerotech_omega(val, wait=True) - def set_aerotech_omega( - self, - val: float, - /, - wait: bool = True, - incremental: bool = False, - ): + def set_aerotech_omega(self, val: float, /, wait: bool = True, incremental: bool = False): target = AerotechCoordinate(omega_deg=val) - return self.aerotech.position( - target, - wait=wait, - incremental=incremental, - ) + return self.aerotech.position(target, wait=wait, incremental=incremental) def aerotech_stop(self): # TODO link cancel in GUI to cancel in aerotech if not already done diff --git a/src/aare/daq/mlbox.py b/src/aare/daq/mlbox.py index d1d59dd4..b612bd4d 100644 --- a/src/aare/daq/mlbox.py +++ b/src/aare/daq/mlbox.py @@ -7,12 +7,7 @@ import numpy as np from aarecommon.config.logger import setup_logger from aarecommon.config.logger_events import log_timing from aarecommon.models.beamline import MXBeamline -from aarecommon.models.models import ( - BoundingBoxModel, - MLBoxModel, - MLBoxType, - MLOutputModel, -) +from aarecommon.models.models import BoundingBoxModel, MLBoxModel, MLBoxType, MLOutputModel from aarelcinfer_client.models import LatestPredictionModel logger = setup_logger("aareDAQ") @@ -118,9 +113,7 @@ class MlBox: def _prediction_score(predictions: MLOutputModel | None) -> tuple[int, float]: if predictions is None or not predictions.boxes: return (0, 0.0) - confs = [ - float(m.conf or 0.0) for m in predictions.boxes.values() if m is not None - ] + confs = [float(m.conf or 0.0) for m in predictions.boxes.values() if m is not None] return (len(confs), max(confs) if confs else 0.0) def _fetch_prediction_bundle(self): @@ -128,9 +121,7 @@ class MlBox: for attempt in range(1, self.RETRY_COUNT + 1): try: bundle = self.__wrapper.get_latest_prediction_bundle() - logger.debug( - f"Fetched prediction bundle on attempt {attempt}/{self.RETRY_COUNT}" - ) + logger.debug(f"Fetched prediction bundle on attempt {attempt}/{self.RETRY_COUNT}") return bundle except Exception as e: last_error = e @@ -165,9 +156,7 @@ class MlBox: if isinstance(raw, (list, tuple)) and len(raw) >= 2: return float(raw[0]), float(raw[1]) except Exception as e: - logger.warning( - f"Failed to parse target_point from prediction metadata: {e}" - ) + logger.warning(f"Failed to parse target_point from prediction metadata: {e}") return None @@ -182,9 +171,7 @@ class MlBox: if raw_focus is not None: focus = float(raw_focus) except Exception as e: - logger.warning( - f"Failed to parse focus_score from prediction metadata: {e}" - ) + logger.warning(f"Failed to parse focus_score from prediction metadata: {e}") return MLBundleMeta(target_point=target_point, focus=focus) @@ -242,13 +229,9 @@ class MlBox: if best_predictions is None: logger.info("No valid prediction bundle metadata was available") elif not best_predictions.boxes: - logger.info( - "Prediction bundle candidates contained no supported detections" - ) + logger.info("Prediction bundle candidates contained no supported detections") - return MLBundle( - predictions=best_predictions, image=best_image, bundle_meta=best_meta - ) + return MLBundle(predictions=best_predictions, image=best_image, bundle_meta=best_meta) def _get_latest_bundle_image(self) -> np.ndarray | None: bundle = self._fetch_prediction_bundle() @@ -264,18 +247,8 @@ class MlBox: # Extract (x1,y1,x2,y2) from MLBoxModel/BoundingBoxModel if not box0 or not box0.box or not box1 or not box1.box: raise ValueError("Invalid boxes passed to check_box_relation") - x10, y10, x20, y20 = ( - box0.box.top_x, - box0.box.top_y, - box0.box.bottom_x, - box0.box.bottom_y, - ) - x11, y11, x21, y21 = ( - box1.box.top_x, - box1.box.top_y, - box1.box.bottom_x, - box1.box.bottom_y, - ) + x10, y10, x20, y20 = (box0.box.top_x, box0.box.top_y, box0.box.bottom_x, box0.box.bottom_y) + x11, y11, x21, y21 = (box1.box.top_x, box1.box.top_y, box1.box.bottom_x, box1.box.bottom_y) left, top, overlap_x, overlap_y = False, False, False, False # Compute centers for robustness @@ -298,12 +271,7 @@ class MlBox: else: overlap_y = True - return { - "left": left, - "top": top, - "overlap_x": overlap_x, - "overlap_y": overlap_y, - } + return {"left": left, "top": top, "overlap_x": overlap_x, "overlap_y": overlap_y} def box_relation(self, boxes: MLOutputModel, classes: list[str] | None = None): pin: MLBoxModel | None = boxes.get_best_for_class(MLBoxType.PIN) @@ -314,11 +282,7 @@ class MlBox: box_relative_to_pin: dict[str, bool] = {} for box_type in classes: # Ensure the entry exists and is an MLBoxModel - model = ( - boxes.get(box_type) - if hasattr(boxes, "get") - else boxes.boxes.get(box_type) - ) + model = boxes.get(box_type) if hasattr(boxes, "get") else boxes.boxes.get(box_type) if model: box_relative_to_pin = self.check_box_relation(pin, model) @@ -428,15 +392,9 @@ class MlBox: logger.debug(f"Failed to parse bundle detection: {e}") continue - out.add_box( - cls=cls, - box_tuple=(x1, y1, x2, y2), - conf=conf, - ) + out.add_box(cls=cls, box_tuple=(x1, y1, x2, y2), conf=conf) - logger.debug( - f"Parsed {len(out.boxes)} supported detections from bundle metadata" - ) + logger.debug(f"Parsed {len(out.boxes)} supported detections from bundle metadata") return out def get_best_detections(self, results) -> MLOutputModel | None: @@ -464,21 +422,14 @@ class MlBox: Best MLBoxModel according to preferences """ if preferred_class is None: - order = ( - MLBoxType.CRYSTAL, - MLBoxType.LOOP_FACE, - MLBoxType.LOOP_ALL, - MLBoxType.PIN, - ) + order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN) else: if isinstance(preferred_class, MLBoxType): order = (preferred_class,) elif isinstance(preferred_class, int): order = (MLBoxType(preferred_class),) else: - order = tuple( - MLBoxType(c) if isinstance(c, int) else c for c in preferred_class - ) + order = tuple(MLBoxType(c) if isinstance(c, int) else c for c in preferred_class) # Get best box from each class best_boxes = {} @@ -515,25 +466,17 @@ class MlBox: @staticmethod def get_preferred_class_box( - boxes: MLOutputModel, - preferred_class: Optional[Iterable[int] | int | MLBoxType] = None, + boxes: MLOutputModel, preferred_class: Optional[Iterable[int] | int | MLBoxType] = None ) -> Optional[MLBoxModel]: if preferred_class is None: - order = ( - MLBoxType.CRYSTAL, - MLBoxType.LOOP_FACE, - MLBoxType.LOOP_ALL, - MLBoxType.PIN, - ) + order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN) else: if isinstance(preferred_class, MLBoxType): order = (preferred_class,) elif isinstance(preferred_class, int): order = (MLBoxType(preferred_class),) else: - order = tuple( - MLBoxType(c) if isinstance(c, int) else c for c in preferred_class - ) + order = tuple(MLBoxType(c) if isinstance(c, int) else c for c in preferred_class) for cls in order: m = boxes.get_best_for_class(cls) @@ -548,12 +491,7 @@ class MlBox: confidence_min: float | None = None, return_image: bool = False, return_bundle_meta: bool = False, - ) -> ( - None - | MLBoxModel - | tuple[MLBoxModel | None, np.ndarray | None] - | MLBoxPredictionResult - ): + ) -> None | MLBoxModel | tuple[MLBoxModel | None, np.ndarray | None] | MLBoxPredictionResult: ml_bundle = self._collect_best_bundle() predictions, image, bundle_meta = ( ml_bundle.predictions, @@ -620,11 +558,7 @@ class MlBox: | MLBoxPredictionsResult ): ml_bundle = self._collect_best_bundle() - best, image, bundle_meta = ( - ml_bundle.predictions, - ml_bundle.image, - ml_bundle.bundle_meta, - ) + best, image, bundle_meta = (ml_bundle.predictions, ml_bundle.image, ml_bundle.bundle_meta) if not best: logger.debug(f"No best predictions from ML bundle: {best}") if return_bundle_meta: @@ -654,10 +588,7 @@ class MlBox: overlap_parameter: float | None = None, confidence_filter: float | None = None, return_image: bool = False, - ) -> ( - dict[str, list[MLBoxModel]] - | tuple[dict[str, list[MLBoxModel]], np.ndarray | None] - ): + ) -> dict[str, list[MLBoxModel]] | tuple[dict[str, list[MLBoxModel]], np.ndarray | None]: """ Return a dict keyed by '_' -> [MLBoxModel, ...] for each detection. The ordinal is the running count per class (1-based) in the order they appear after filtering. @@ -673,9 +604,7 @@ class MlBox: return ({}, image) if return_image else {} self._filter_predictions( - grouped, - overlap_with_pin=overlap_parameter, - confidence_min=confidence_filter, + grouped, overlap_with_pin=overlap_parameter, confidence_min=confidence_filter ) per_class_counter: dict[MLBoxType, int] = {} @@ -730,9 +659,7 @@ class MlBox: logger.debug(f"Frame {frame_idx + 1}/{n_frames}: no detection") except Exception as e: - logger.warning( - f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}" - ) + logger.warning(f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}") continue if best_box is None: diff --git a/src/aare/daq/operations/common/ml_bounding_box.py b/src/aare/daq/operations/common/ml_bounding_box.py index 57501291..37456256 100644 --- a/src/aare/daq/operations/common/ml_bounding_box.py +++ b/src/aare/daq/operations/common/ml_bounding_box.py @@ -41,12 +41,7 @@ def _box_tuple(model) -> BoxTuple | None: def _box_extends_beyond(inner: BoxTuple, outer: BoxTuple) -> bool: - return ( - inner[0] < outer[0] - or inner[1] < outer[1] - or inner[2] > outer[2] - or inner[3] > outer[3] - ) + return inner[0] < outer[0] or inner[1] < outer[1] or inner[2] > outer[2] or inner[3] > outer[3] def _box_union(a: BoxTuple, b: BoxTuple) -> BoxTuple: @@ -114,9 +109,7 @@ def get_ml_bounding_box( sample_id = getattr(sample, "db_id", None) prediction_result: MLBoxPredictionResult = mlbox.predict( - preferred_class=(3, 0), - return_image=True, - return_bundle_meta=True, + preferred_class=(3, 0), return_image=True, return_bundle_meta=True ) m = prediction_result.box bundle_image = prediction_result.image @@ -145,9 +138,7 @@ def get_ml_bounding_box( if filename is not None and bundle_image is not None: annotated_image = bundle_image.copy() - cv2.rectangle( - annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2 - ) + cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) upload_image(sample_id, filename, annotated_image) geom = sample_geometry @@ -155,10 +146,7 @@ def get_ml_bounding_box( "ML bounding box selected", extra=merge_log_context( sample_log_context(sample), - { - "sample_id": sample_id, - "ml_image_name": filename, - }, + {"sample_id": sample_id, "ml_image_name": filename}, geom_log_context(geom), { "box_x1": x1, @@ -214,9 +202,7 @@ def _box_to_raster_request( # of the n_y scan) can be padded more than the top. frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15)) frac_y_top = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15)) - frac_y_bottom = float( - cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top) - ) + frac_y_bottom = float(cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top)) pad_x = max(1, int(ceil(frac_x * n_x))) pad_y_top = max(1, int(ceil(frac_y_top * n_y))) pad_y_bottom = max(1, int(ceil(frac_y_bottom * n_y))) @@ -307,9 +293,7 @@ def _box_to_raster_request( exp_time_s=0.01, transmission=1.0, smargon_top_left=SmargonCoordinate( - chi_deg=geom.smargon.chi_deg, - phi_deg=geom.smargon.phi_deg, - sh_mm=start_coord, + chi_deg=geom.smargon.chi_deg, phi_deg=geom.smargon.phi_deg, sh_mm=start_coord ), n_x=n_x, n_y=n_y, @@ -343,8 +327,7 @@ def build_ml_raster_plan( sample_id = getattr(sample, "db_id", None) result: MLBoxPredictionsResult = mlbox.predict_all_best( - return_image=True, - return_bundle_meta=True, + return_image=True, return_bundle_meta=True ) predictions = result.predictions bundle_image = result.image @@ -356,12 +339,8 @@ def build_ml_raster_plan( focus=result.focus, ) - loop_all = ( - predictions.get_best_for_class(MLBoxType.LOOP_ALL) if predictions else None - ) - loop_face = ( - predictions.get_best_for_class(MLBoxType.LOOP_FACE) if predictions else None - ) + loop_all = predictions.get_best_for_class(MLBoxType.LOOP_ALL) if predictions else None + loop_face = predictions.get_best_for_class(MLBoxType.LOOP_FACE) if predictions else None # Grid box: prefer loop_face, else loop_all (matches the legacy (3, 0) order). grid_model = loop_face if loop_face is not None else loop_all @@ -398,18 +377,12 @@ def build_ml_raster_plan( x1, y1, x2, y2 = _box_union((x1, y1, x2, y2), cbox) logger.info( "Extended ML raster grid to include crystal outside the loop box", - extra={ - "sample_id": sample_id, - "ml_image_name": filename, - "crystal_box": cbox, - }, + extra={"sample_id": sample_id, "ml_image_name": filename, "crystal_box": cbox}, ) if filename is not None and bundle_image is not None: annotated_image = bundle_image.copy() - cv2.rectangle( - annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2 - ) + cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) upload_image(sample_id, filename, annotated_image) grid_request = _box_to_raster_request( diff --git a/src/aare/daq/operations/common/models.py b/src/aare/daq/operations/common/models.py index 22d1d305..da6605ee 100644 --- a/src/aare/daq/operations/common/models.py +++ b/src/aare/daq/operations/common/models.py @@ -24,6 +24,7 @@ class BeamlineDependencies: cfg: BeamlineConfig devs: BeamlineDevices + @dataclass class OperationResult: success: bool @@ -52,4 +53,4 @@ class BaseOperationContext(Generic[DepsT, SettingsT]): @property def status(self): - return self.runtime.status \ No newline at end of file + return self.runtime.status diff --git a/src/aare/daq/operations/common/services.py b/src/aare/daq/operations/common/services.py index 701e32e7..0b9252ef 100644 --- a/src/aare/daq/operations/common/services.py +++ b/src/aare/daq/operations/common/services.py @@ -28,7 +28,9 @@ class SampleEventSender(Protocol): class ScanIngestor(Protocol): def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None: ... - def ingest_gridscan(self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl) -> None: ... + def ingest_gridscan( + self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl + ) -> None: ... class DatacollectionSetupRunner(Protocol): @@ -81,13 +83,12 @@ class ScanIngestionService: def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None: self.ingestor.ingest_scan( - sample=sample, - result=result, - geom=geom, - beam_mark_pxl=beam_mark_pxl, + sample=sample, result=result, geom=geom, beam_mark_pxl=beam_mark_pxl ) - def ingest_gridscan(self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl) -> None: + def ingest_gridscan( + self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl + ) -> None: self.ingestor.ingest_gridscan( sample=sample, raster_result=raster_result, @@ -115,4 +116,4 @@ class OperationServices: face_detection_progress: FaceDetectionProgressEmitter | None = None events: SampleEventPublisher | None = None ingestion: ScanIngestionService | None = None - datacollection: DataCollectionPreparer | None = None \ No newline at end of file + datacollection: DataCollectionPreparer | None = None diff --git a/src/aare/daq/operations/common/simulate_scan_result.py b/src/aare/daq/operations/common/simulate_scan_result.py index 062191c5..73e6f343 100644 --- a/src/aare/daq/operations/common/simulate_scan_result.py +++ b/src/aare/daq/operations/common/simulate_scan_result.py @@ -6,11 +6,7 @@ from jfjoch_client import ScanResult, ScanResultImagesInner def build_fake_scan_result( - *, - file_prefix: str | None, - image_count: int, - rotation: bool = False, - start_angle: float = 0.0, + *, file_prefix: str | None, image_count: int, rotation: bool = False, start_angle: float = 0.0 ) -> ScanResult: total_images = max(1, image_count) angle_step = 360.0 / total_images if rotation else 0.0 @@ -33,9 +29,7 @@ def build_fake_scan_result( def build_fake_rotation_result( - request: RotationScanRequest, - *, - start_angle: float = 0.0, + request: RotationScanRequest, *, start_angle: float = 0.0 ) -> CompletedRotationScan: result = build_fake_scan_result( file_prefix=request.file_prefix, @@ -43,20 +37,13 @@ def build_fake_rotation_result( rotation=True, start_angle=start_angle, ) - return CompletedRotationScan( - request=copy.deepcopy(request), - result=result, - ) + return CompletedRotationScan(request=copy.deepcopy(request), result=result) def build_fake_raster_result(request: RasterGridRequest) -> CompletedRasterGridElem: result = build_fake_scan_result( - file_prefix=request.file_prefix, - image_count=request.n_x * request.n_y, - rotation=False, + file_prefix=request.file_prefix, image_count=request.n_x * request.n_y, rotation=False ) return CompletedRasterGridElem( - request=copy.deepcopy(request), - result=result, - centre_of_mass=None, + request=copy.deepcopy(request), result=result, centre_of_mass=None ) diff --git a/src/aare/daq/operations/face_detection/__init__.py b/src/aare/daq/operations/face_detection/__init__.py index 47bc7c2d..f43927cf 100644 --- a/src/aare/daq/operations/face_detection/__init__.py +++ b/src/aare/daq/operations/face_detection/__init__.py @@ -1,11 +1,4 @@ -from aare.daq.operations.face_detection.models import ( - FaceDetectionContext, - FaceDetectionResult, -) +from aare.daq.operations.face_detection.models import FaceDetectionContext, FaceDetectionResult from aare.daq.operations.face_detection.service import FaceDetectionService -__all__ = [ - "FaceDetectionContext", - "FaceDetectionResult", - "FaceDetectionService", -] \ No newline at end of file +__all__ = ["FaceDetectionContext", "FaceDetectionResult", "FaceDetectionService"] diff --git a/src/aare/daq/operations/face_detection/models.py b/src/aare/daq/operations/face_detection/models.py index a837c492..283c20c9 100644 --- a/src/aare/daq/operations/face_detection/models.py +++ b/src/aare/daq/operations/face_detection/models.py @@ -1,10 +1,7 @@ from dataclasses import dataclass from aare.daq.mlbox import MlBox -from aare.daq.operations.common.models import ( - BaseOperationContext, - BeamlineDependencies, -) +from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies @dataclass @@ -20,9 +17,7 @@ class FaceDetectionSettings: @dataclass -class FaceDetectionContext( - BaseOperationContext[FaceDetectionDependencies, FaceDetectionSettings] -): +class FaceDetectionContext(BaseOperationContext[FaceDetectionDependencies, FaceDetectionSettings]): pass @@ -31,4 +26,4 @@ class FaceDetectionResult: success: bool payload: dict error: Exception | None = None - comment: str | None = None \ No newline at end of file + comment: str | None = None diff --git a/src/aare/daq/operations/face_detection/service.py b/src/aare/daq/operations/face_detection/service.py index 5cda1d52..c162a654 100644 --- a/src/aare/daq/operations/face_detection/service.py +++ b/src/aare/daq/operations/face_detection/service.py @@ -6,10 +6,7 @@ from aarecommon.models.models import MLBoxModel, ZoomModeEnum import aare.daq.operations.face_detection.utils as fd from aare.daq.mlbox import MLBoxPredictionResult -from aare.daq.operations.face_detection.models import ( - FaceDetectionContext, - FaceDetectionResult, -) +from aare.daq.operations.face_detection.models import FaceDetectionContext, FaceDetectionResult class FaceDetectionService: @@ -20,9 +17,7 @@ class FaceDetectionService: def _progress_emitter(self): emitter = self.ctx.services.face_detection_progress if emitter is None: - raise RuntimeError( - "FaceDetectionService requires services.face_detection_progress" - ) + raise RuntimeError("FaceDetectionService requires services.face_detection_progress") return emitter def _log_warning(self, message: str) -> None: @@ -36,10 +31,7 @@ class FaceDetectionService: fallback(message) def _emit_running_progress( - self, - *, - angle: int, - boxes_face: dict[int, tuple[float, float, float, float]], + self, *, angle: int, boxes_face: dict[int, tuple[float, float, float, float]] ) -> None: self._progress_emitter().emit_progress( { @@ -52,12 +44,7 @@ class FaceDetectionService: ) def _emit_empty_result(self) -> dict: - payload = { - "running": False, - "samples": [], - "height_fit": {}, - "area_fit": {}, - } + payload = {"running": False, "samples": [], "height_fit": {}, "area_fit": {}} self._progress_emitter().emit_progress(payload) return payload @@ -87,9 +74,7 @@ class FaceDetectionService: steps = self.ctx.settings.steps if steps is None else steps step_size = self.ctx.settings.step_size if step_size is None else step_size face_min_ratio = ( - self.ctx.settings.face_min_ratio - if face_min_ratio is None - else face_min_ratio + self.ctx.settings.face_min_ratio if face_min_ratio is None else face_min_ratio ) try: self.ctx.deps.cfg.zoom_mode = ZoomModeEnum.LoopCenter @@ -119,9 +104,7 @@ class FaceDetectionService: ) prediction_result: MLBoxPredictionResult = self.ctx.deps.mlbox.predict( - preferred_class=(3, 0), - return_image=True, - return_bundle_meta=True, + preferred_class=(3, 0), return_image=True, return_bundle_meta=True ) model = prediction_result.box log_ml_bundle_meta( @@ -167,9 +150,7 @@ class FaceDetectionService: return FaceDetectionResult(success=True, payload=payload) total_detections = len(boxes_face) + len(boxes_loop) - face_ratio = ( - len(boxes_face) / total_detections if total_detections > 0 else 0.0 - ) + face_ratio = len(boxes_face) / total_detections if total_detections > 0 else 0.0 if boxes_face and face_ratio >= face_min_ratio: boxes = boxes_face @@ -183,13 +164,9 @@ class FaceDetectionService: ) else: boxes = boxes_face - self.logger.debug( - f"using loop_face boxes (only source, {len(boxes_face)} entries)" - ) + self.logger.debug(f"using loop_face boxes (only source, {len(boxes_face)} entries)") - best_fit_angle_area, area_params = fd.get_flat_face( - boxes, start_angle, end_angle, True - ) + best_fit_angle_area, area_params = fd.get_flat_face(boxes, start_angle, end_angle, True) best_fit_angle_height, height_params = fd.get_flat_face( boxes, start_angle, end_angle, False ) @@ -237,10 +214,7 @@ class FaceDetectionService: self.logger.error(f"error in face detection sequence {e}") payload = self._emit_empty_result() return FaceDetectionResult( - success=False, - payload=payload, - error=e, - comment="Face detection sequence failed", + success=False, payload=payload, error=e, comment="Face detection sequence failed" ) finally: self.ctx.deps.cfg.zoom_mode = ZoomModeEnum.User diff --git a/src/aare/daq/operations/face_detection/utils.py b/src/aare/daq/operations/face_detection/utils.py index b931cf26..fe127170 100644 --- a/src/aare/daq/operations/face_detection/utils.py +++ b/src/aare/daq/operations/face_detection/utils.py @@ -33,15 +33,11 @@ def prepare_samples( return samples -def cos_model( - theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float, C: float -): +def cos_model(theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float, C: float): return A + B * np.cos(C * np.deg2rad(theta_deg) - phi_rad) -def mad_filter( - samples: List[Tuple[float, float]], k: float = 3.5 -) -> List[Tuple[float, float]]: +def mad_filter(samples: List[Tuple[float, float]], k: float = 3.5) -> List[Tuple[float, float]]: if not samples: return samples ys = [y for _, y in samples] @@ -75,15 +71,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict: samples = mad_filter(samples, k=3.5) if len(samples) < 3: A = sum(y for _, y in samples) / max(1, len(samples)) - return { - "A": A, - "B": 1.0, - "phi_rad": 0.0, - "C": 1.0, - "rmse": None, - "mae": None, - "r2": None, - } + return {"A": A, "B": 1.0, "phi_rad": 0.0, "C": 1.0, "rmse": None, "mae": None, "r2": None} degs = np.array([d for d, _ in samples], dtype=float) ys = np.array([y for _, y in samples], dtype=float) @@ -119,15 +107,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict: B = max(0.0, B) yhat = cos_model(degs, A, B, phi, C) rmse, mae, r2 = fit_metrics(ys, yhat) - return { - "A": A, - "B": B, - "phi_rad": phi, - "C": C, - "rmse": rmse, - "mae": mae, - "r2": r2, - } + return {"A": A, "B": B, "phi_rad": phi, "C": C, "rmse": rmse, "mae": mae, "r2": r2} except Exception as e: # Fallback to initial @@ -150,9 +130,7 @@ def get_samples_out(boxes): for deg, box in boxes.items(): h = box_height_from_tuple(box) a = box_area_from_tuple(box) - samples_out.append( - {"angle_deg": float(deg), "height": float(h), "area": float(a)} - ) + samples_out.append({"angle_deg": float(deg), "height": float(h), "area": float(a)}) samples_out.sort(key=lambda x: x["angle_deg"]) return samples_out @@ -224,9 +202,7 @@ def get_flat_face( return best_fit_angle, parameters -def chose_best_angle( - boxes: dict[int, tuple[float, float, float, float]], fit_results -) -> int: +def chose_best_angle(boxes: dict[int, tuple[float, float, float, float]], fit_results) -> int: measured_angles = list(boxes.keys()) choose_best_fit(fit_results) candidates = [ @@ -236,10 +212,7 @@ def chose_best_angle( ] if measured_angles and candidates: chosen = min( - candidates, - key=lambda a: min( - abs(((a - m + 180) % 360) - 180) for m in measured_angles - ), + candidates, key=lambda a: min(abs(((a - m + 180) % 360) - 180) for m in measured_angles) ) else: chosen = candidates[0] if candidates else 0 diff --git a/src/aare/daq/operations/loop_centering/__init__.py b/src/aare/daq/operations/loop_centering/__init__.py index 1d82c710..2026c242 100644 --- a/src/aare/daq/operations/loop_centering/__init__.py +++ b/src/aare/daq/operations/loop_centering/__init__.py @@ -14,4 +14,4 @@ __all__ = [ "LoopCenteringSettings", "LoopCenteringService", "LoopCenteringAnalyzer", -] \ No newline at end of file +] diff --git a/src/aare/daq/operations/loop_centering/analyzer.py b/src/aare/daq/operations/loop_centering/analyzer.py index 56af4fee..d28d94a3 100644 --- a/src/aare/daq/operations/loop_centering/analyzer.py +++ b/src/aare/daq/operations/loop_centering/analyzer.py @@ -2,10 +2,7 @@ from aarecommon.config.logger_events import log_ml_bundle_meta from aarecommon.math.coordinate import Coordinate, SmargonCoordinate from aarecommon.models.models import MLBoxType -from aare.daq.operations.loop_centering.models import ( - AngleAnalysis, - LoopCenteringContext, -) +from aare.daq.operations.loop_centering.models import AngleAnalysis, LoopCenteringContext class LoopCenteringAnalyzer: @@ -17,11 +14,7 @@ class LoopCenteringAnalyzer: if not classes: return False - ignore_classes = { - MLBoxType.PIN.value, - MLBoxType.NEEDLE.value, - MLBoxType.ICE.value, - } + ignore_classes = {MLBoxType.PIN.value, MLBoxType.NEEDLE.value, MLBoxType.ICE.value} found = {int(cls) for cls in classes if cls is not None} return bool(found) and found.issubset(ignore_classes) @@ -78,18 +71,14 @@ class LoopCenteringAnalyzer: return calculated_target def _extract_prediction_target( - self, - *, - target_point: tuple[float, float] | None, + self, *, target_point: tuple[float, float] | None ) -> SmargonCoordinate | None: if target_point is None: return None geom = self.ctx.runtime.sample_geometry return SmargonCoordinate( - sh_mm=geom.picture_to_smargon( - Coordinate(x=target_point[0], y=target_point[1]) - ) + sh_mm=geom.picture_to_smargon(Coordinate(x=target_point[0], y=target_point[1])) ) def _interpret_ml_loop_centre_box( @@ -109,8 +98,7 @@ class LoopCenteringAnalyzer: pin = box best_box = self.ctx.deps.mlbox.get_preferred_class_box( - boxes, - (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN), + boxes, (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN) ) if best_box is None or best_box.box is None or best_box.cls is None: return None, None, classes @@ -155,11 +143,7 @@ class LoopCenteringAnalyzer: return SmargonCoordinate(sh_mm=coord), cls, classes def analyze_angle( - self, - *, - angle_deg: int, - zoom_value: float, - sample_id: int | None, + self, *, angle_deg: int, zoom_value: float, sample_id: int | None ) -> AngleAnalysis: if self.ctx.services.predictions is None: raise RuntimeError("LoopCenteringAnalyzer requires services.predictions") @@ -173,7 +157,7 @@ class LoopCenteringAnalyzer: ) predicted_target = self._extract_prediction_target( - target_point=prediction_result.target_point, + target_point=prediction_result.target_point ) calculated_target, selected_class, classes = self._interpret_ml_loop_centre_box( prediction_result.predictions @@ -189,7 +173,6 @@ class LoopCenteringAnalyzer: analysis.ignore_only = self.is_ignore_only_classes(analysis.classes) analysis.has_valid_target = self.has_valid_target_classes(analysis.classes) analysis.final_target = self.select_smargon_target( - calculated_target=analysis.calculated_target, - predicted_target=analysis.predicted_target, + calculated_target=analysis.calculated_target, predicted_target=analysis.predicted_target ) return analysis diff --git a/src/aare/daq/operations/loop_centering/models.py b/src/aare/daq/operations/loop_centering/models.py index 487c6bfd..fbc86702 100644 --- a/src/aare/daq/operations/loop_centering/models.py +++ b/src/aare/daq/operations/loop_centering/models.py @@ -3,10 +3,7 @@ from dataclasses import dataclass, field from aarecommon.math.coordinate import SmargonCoordinate from aare.daq.mlbox import MlBox -from aare.daq.operations.common.models import ( - BaseOperationContext, - BeamlineDependencies, -) +from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies @dataclass @@ -45,7 +42,5 @@ class LoopCenteringSettings: @dataclass -class LoopCenteringContext( - BaseOperationContext[LoopCenteringDependencies, LoopCenteringSettings] -): +class LoopCenteringContext(BaseOperationContext[LoopCenteringDependencies, LoopCenteringSettings]): pass diff --git a/src/aare/daq/operations/loop_centering/service.py b/src/aare/daq/operations/loop_centering/service.py index b19b8b70..e1baeca0 100644 --- a/src/aare/daq/operations/loop_centering/service.py +++ b/src/aare/daq/operations/loop_centering/service.py @@ -40,9 +40,7 @@ class LoopCenteringService: ) analysis = self.analyzer.analyze_angle( - angle_deg=angle, - zoom_value=zoom_value, - sample_id=sample_id, + angle_deg=angle, zoom_value=zoom_value, sample_id=sample_id ) if ( @@ -63,13 +61,10 @@ class LoopCenteringService: if sample_id is not None and trace_all_alc_moves: self.ctx.services.traces.append_smargon_trace( - sample_id=sample_id, - event=f"alc_move_zoom_{zoom_value:.0f}_angle_{angle}", + sample_id=sample_id, event=f"alc_move_zoom_{zoom_value:.0f}_angle_{angle}" ) else: - self.logger.debug( - f"Skipping move at angle {angle}; classes={analysis.classes}" - ) + self.logger.debug(f"Skipping move at angle {angle}; classes={analysis.classes}") if sample_id is not None and analysis.selected_class in ( MLBoxType.LOOP_ALL.value, @@ -77,9 +72,7 @@ class LoopCenteringService: MLBoxType.CRYSTAL.value, ): self.ctx.services.screenshots.save_to_db( - sample_id, - f"{sample_id}_{angle}_{zoom_value:.0f}", - wait_screenshot_sleep_sec, + sample_id, f"{sample_id}_{angle}_{zoom_value:.0f}", wait_screenshot_sleep_sec ) return analysis @@ -117,9 +110,7 @@ class LoopCenteringService: ) for attempt_number in range(1, max_attempts + 1): - self.logger.info( - f"Starting ALC attempt {attempt_number}/{max_attempts}" - ) + self.logger.info(f"Starting ALC attempt {attempt_number}/{max_attempts}") attempt = AttemptSummary(attempt_number=attempt_number) for angle in angles: @@ -133,9 +124,7 @@ class LoopCenteringService: attempt.first_pass.append(result) for c in result.classes: - found_classes_count[int(c)] = ( - found_classes_count.get(int(c), 0) + 1 - ) + found_classes_count[int(c)] = found_classes_count.get(int(c), 0) + 1 if attempt_number == 1: valid_seen_first_pass = any( @@ -159,9 +148,7 @@ class LoopCenteringService: attempt.correction_pass.append(result) for c in result.classes: - found_classes_count[int(c)] = ( - found_classes_count.get(int(c), 0) + 1 - ) + found_classes_count[int(c)] = found_classes_count.get(int(c), 0) + 1 attempts.append(attempt) @@ -219,8 +206,4 @@ class LoopCenteringService: self.logger.error(f"ALC exception: {alc_comment}") self.logger.error(traceback.format_exc()) self.logger.error(f"Error in loop centering: {e}") - return LoopCenteringResult( - success=False, - comment=alc_comment, - error=e, - ) + return LoopCenteringResult(success=False, comment=alc_comment, error=e) diff --git a/src/aare/daq/operations/mounting/__init__.py b/src/aare/daq/operations/mounting/__init__.py index ac163171..ff65a562 100644 --- a/src/aare/daq/operations/mounting/__init__.py +++ b/src/aare/daq/operations/mounting/__init__.py @@ -1,8 +1,4 @@ from aare.daq.operations.mounting.models import MountingContext, MountingResult from aare.daq.operations.mounting.service import MountingService -__all__ = [ - "MountingContext", - "MountingResult", - "MountingService", -] \ No newline at end of file +__all__ = ["MountingContext", "MountingResult", "MountingService"] diff --git a/src/aare/daq/operations/mounting/service.py b/src/aare/daq/operations/mounting/service.py index 614de690..13960895 100644 --- a/src/aare/daq/operations/mounting/service.py +++ b/src/aare/daq/operations/mounting/service.py @@ -43,9 +43,7 @@ class MountingService: self.logger.error( "Goniometer didn't reach position based on magnet position sensor readout" ) - raise Exception( - "Goniometer is not in position based on magnet position sensor readout" - ) + raise Exception("Goniometer is not in position based on magnet position sensor readout") def _handle_consecutive_mount_failure(self) -> None: count = self.ctx.deps.cfg.increment_mount_failure_streak() @@ -59,21 +57,16 @@ class MountingService: self.logger.exception(f"Failed to dry after mount failure: {e}") if count >= STOP_AFTER_FAIL_COUNT: - self.logger.error( - f"Mount failed {count} times in a row, stopping automation" - ) + self.logger.error(f"Mount failed {count} times in a row, stopping automation") self.logger.error("Unmounting sample and drying") try: self._unmount_current_sample(timeout=60.0) self.dry(park=True) except Exception as e: - self.logger.exception( - f"Failed to clean up after repeated mount failure: {e}" - ) + self.logger.exception(f"Failed to clean up after repeated mount failure: {e}") raise MountingFailed( - f"Mount failed {count} times in a row, stopping automation.", - critical=True, + f"Mount failed {count} times in a row, stopping automation.", critical=True ) def _mount_handler(self, target) -> None: @@ -135,9 +128,7 @@ class MountingService: """ pss = self.ctx.deps.devs.pss if not pss.is_prohibited(): - self.logger.error( - "Hutch is not in prohibited state; robot motion not permitted" - ) + self.logger.error("Hutch is not in prohibited state; robot motion not permitted") raise DoorSafetyError( "Door safety could not be activated: close the hutch doors and " "complete the safety search before mounting." diff --git a/src/aare/daq/operations/raster/__init__.py b/src/aare/daq/operations/raster/__init__.py index baf61d8f..5f87f03c 100644 --- a/src/aare/daq/operations/raster/__init__.py +++ b/src/aare/daq/operations/raster/__init__.py @@ -1,8 +1,4 @@ from aare.daq.operations.raster.models import RasterContext, RasterBoundingBoxResult from aare.daq.operations.raster.service import RasterService -__all__ = [ - "RasterBoundingBoxResult", - "RasterContext", - "RasterService", -] \ No newline at end of file +__all__ = ["RasterBoundingBoxResult", "RasterContext", "RasterService"] diff --git a/src/aare/daq/operations/raster/models.py b/src/aare/daq/operations/raster/models.py index e6fab589..2220649d 100644 --- a/src/aare/daq/operations/raster/models.py +++ b/src/aare/daq/operations/raster/models.py @@ -4,10 +4,7 @@ from typing import TYPE_CHECKING from aarecommon.models.raster_grid import RasterGridRequest from aare.daq.mlbox import MlBox -from aare.daq.operations.common.models import ( - BaseOperationContext, - BeamlineDependencies, -) +from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies if TYPE_CHECKING: from aare.daq.aaredb import AareWrapper diff --git a/src/aare/daq/operations/raster/service.py b/src/aare/daq/operations/raster/service.py index c99ee63d..ccda79a6 100644 --- a/src/aare/daq/operations/raster/service.py +++ b/src/aare/daq/operations/raster/service.py @@ -11,10 +11,7 @@ from aarecommon.config.logger_events import ( raster_request_log_context, sample_log_context, ) -from aarecommon.errors.exception_handler import ( - AutoRasterSampleSkipped, - RasterScanException, -) +from aarecommon.errors.exception_handler import AutoRasterSampleSkipped, RasterScanException from aarecommon.math.coordinate import AerotechCoordinate, Coordinate, SmargonCoordinate from aarecommon.math.find_xtal import ( compute_crystal_score_array, @@ -57,10 +54,7 @@ class RasterService: @staticmethod def _grid_image_id_from_centre_offset( - *, - x_mm: float, - y_mm: float, - request: RasterGridRequest, + *, x_mm: float, y_mm: float, request: RasterGridRequest ) -> int: if request.grid_size_mm.x <= 0 or request.grid_size_mm.y <= 0: raise ValueError("grid_size_mm must be positive") @@ -73,11 +67,7 @@ class RasterService: grid_x = min(max(1, grid_x), request.n_x) grid_y = min(max(1, grid_y), request.n_y) - return grid_to_image_id( - grid_x=grid_x, - grid_y=grid_y, - number_of_cols=request.n_x, - ) + return grid_to_image_id(grid_x=grid_x, grid_y=grid_y, number_of_cols=request.n_x) def _upload_raster_diffraction_preview( self, @@ -96,10 +86,7 @@ class RasterService: extra=merge_log_context( sample_log_context(self.ctx.sample), raster_request_log_context(request), - { - "diffraction_image_id": image_id, - "image_count": image_count, - }, + {"diffraction_image_id": image_id, "image_count": image_count}, ), ) return @@ -114,10 +101,7 @@ class RasterService: extra=merge_log_context( sample_log_context(self.ctx.sample), raster_request_log_context(request), - { - "diffraction_image_id": image_id, - "image_count": image_count, - }, + {"diffraction_image_id": image_id, "image_count": image_count}, ), exc_info=True, ) @@ -140,14 +124,10 @@ class RasterService: except Exception: pass - self.ctx.deps.aare.upload_jpg( - sample_id, filename, diffraction_image, message=comment - ) + self.ctx.deps.aare.upload_jpg(sample_id, filename, diffraction_image, message=comment) def ml_bounding_box( - self, - sample_id: int | None = None, - filename: str | None = None, + self, sample_id: int | None = None, filename: str | None = None ) -> RasterGridRequest | None: return get_ml_bounding_box( mlbox=self.ctx.deps.mlbox, @@ -162,9 +142,7 @@ class RasterService: ) def ml_raster_plan( - self, - sample_id: int | None = None, - filename: str | None = None, + self, sample_id: int | None = None, filename: str | None = None ) -> MLRasterPlan | None: return build_ml_raster_plan( mlbox=self.ctx.deps.mlbox, @@ -185,12 +163,7 @@ class RasterService: if width is None or height is None: return False x1, y1, x2, y2 = box - return ( - x1 <= margin - or y1 <= margin - or x2 >= width - margin - or y2 >= height - margin - ) + return x1 <= margin or y1 <= margin or x2 >= width - margin or y2 >= height - margin def _zoom_to_fit_box(self, plan: MLRasterPlan) -> tuple | None: """Pick the box that drives zoom-to-fit: loop_all if detected and not @@ -261,15 +234,11 @@ class RasterService: beam_x_pxl = geom.beam_location_pxl.x beam_y_pxl = geom.beam_location_pxl.y - line_scan_centre = geom.picture_to_smargon( - Coordinate(x=beam_x_pxl, y=beam_y_pxl) - ) + line_scan_centre = geom.picture_to_smargon(Coordinate(x=beam_x_pxl, y=beam_y_pxl)) n_y = default_n_y prediction_result: MLBoxPredictionResult = self.ctx.deps.mlbox.predict( - preferred_class=(3, 0), - return_image=False, - return_bundle_meta=True, + preferred_class=(3, 0), return_image=False, return_bundle_meta=True ) log_ml_bundle_meta( self.logger, @@ -285,7 +254,9 @@ class RasterService: prediction_type = getattr(prediction_box, "type", None) if prediction_type == MLBoxType.LOOP_FACE: - y_padding_fraction_each_side = local_contact_config.line_scan_loop_face_y_padding_fraction_each_side + y_padding_fraction_each_side = ( + local_contact_config.line_scan_loop_face_y_padding_fraction_each_side + ) elif prediction_type == MLBoxType.LOOP_ALL: y_padding_fraction_each_side = ( local_contact_config.line_scan_loop_all_y_padding_fraction_each_side @@ -303,9 +274,7 @@ class RasterService: y_retarget_threshold_mm = geom.beam_size_mm.y * 2.0 if y_delta_mm > y_retarget_threshold_mm: - line_scan_centre = geom.picture_to_smargon( - Coordinate(x=beam_x_pxl, y=target_y_pxl) - ) + line_scan_centre = geom.picture_to_smargon(Coordinate(x=beam_x_pxl, y=target_y_pxl)) self.logger.info( "Using ML target y for second auto-center raster", extra=merge_log_context( @@ -352,16 +321,10 @@ class RasterService: ), ) - if ( - prediction_box is not None - and prediction_box.box is not None - and grid_size_mm.y > 0 - ): + if prediction_box is not None and prediction_box.box is not None and grid_size_mm.y > 0: box_height_pxl = abs(prediction_box.box.bottom_y - prediction_box.box.top_y) padded_height_mm = ( - box_height_pxl - * geom.pixel_in_mm - * (1.0 + 2.0 * y_padding_fraction_each_side) + box_height_pxl * geom.pixel_in_mm * (1.0 + 2.0 * y_padding_fraction_each_side) ) n_y = max(1, int(ceil(padded_height_mm / grid_size_mm.y))) self.logger.info( @@ -396,10 +359,7 @@ class RasterService: ), ) - offset = Coordinate( - x=-grid_size_mm.x / 2.0, - y=-(n_y - 1) * grid_size_mm.y / 2.0, - ) + offset = Coordinate(x=-grid_size_mm.x / 2.0, y=-(n_y - 1) * grid_size_mm.y / 2.0) top_left = SmargonCoordinate( sh_mm=line_scan_centre + geom.smargon_nudge(offset), phi_deg=geom.smargon.phi_deg, @@ -427,9 +387,7 @@ class RasterService: return top_left, n_y def execute( - self, - request: RasterGridRequest, - wait_for_screenshot: float | None = None, + self, request: RasterGridRequest, wait_for_screenshot: float | None = None ) -> CompletedRasterGridElem: total_time = request.exp_time_s * request.n_x * request.n_y + request.n_y * 0.3 @@ -466,9 +424,7 @@ class RasterService: if not self.ctx.deps.cfg.simulated_detector: self.ctx.deps.jfjoch.wait_till_running(timeout=60.0) else: - self.logger.info( - "Simulated detector mode enabled; faking jfjoch intilalisation." - ) + self.logger.info("Simulated detector mode enabled; faking jfjoch intilalisation.") self.logger.debug( f"Starting grid scan with {request.n_x}x{request.n_y} points, exp time {request.exp_time_s}s" @@ -496,8 +452,7 @@ class RasterService: else: coord = self.ctx.deps.cfg.abr_meas_pos.at_mm self.ctx.deps.devs.aerotech_pos = AerotechCoordinate( - at_mm=coord, - omega_deg=self.ctx.deps.devs.aerotech_omega, + at_mm=coord, omega_deg=self.ctx.deps.devs.aerotech_omega ) self.ctx.deps.devs.aerotech.wait_till_done(timeout=60) @@ -505,9 +460,7 @@ class RasterService: y = None if self.ctx.deps.cfg.simulated_detector: - self.logger.info( - "Simulated detector mode enabled; using fake raster result." - ) + self.logger.info("Simulated detector mode enabled; using fake raster result.") scan_result = generate_no_beam_scan_result(request) com = raster_highest_score(scan_result.images) target_coor = com.get_com_mm(request) @@ -528,9 +481,7 @@ class RasterService: {"exp_time_s": request.exp_time_s}, ), ) - raise RasterScanException( - "JFJoch returned no ScanResult for raster" - ) + raise RasterScanException("JFJoch returned no ScanResult for raster") com = raster_highest_score(scan_result.images) if com is None: @@ -556,24 +507,16 @@ class RasterService: "centre_offset_z_mm": target_coor_offset.z, "grid_half_width_x_mm": x, "grid_half_height_y_mm": y, - "top_left_x_mm": getattr( - request.smargon_top_left.sh_mm, "x", None - ), - "top_left_y_mm": getattr( - request.smargon_top_left.sh_mm, "y", None - ), - "top_left_z_mm": getattr( - request.smargon_top_left.sh_mm, "z", None - ), + "top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None), + "top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None), + "top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None), }, ), ) else: self.logger.info("Calcualted COM is not None, procedding") target_coor = com.get_com_mm(request) - target_coor_offset = self.ctx.sample_geometry.smargon_nudge( - target_coor - ) + target_coor_offset = self.ctx.sample_geometry.smargon_nudge(target_coor) self.logger.info( "Calculated raster centre offset", extra=merge_log_context( @@ -585,22 +528,14 @@ class RasterService: "centre_offset_z_mm": target_coor_offset.z, "grid_half_width_x_mm": x, "grid_half_height_y_mm": y, - "top_left_x_mm": getattr( - request.smargon_top_left.sh_mm, "x", None - ), - "top_left_y_mm": getattr( - request.smargon_top_left.sh_mm, "y", None - ), - "top_left_z_mm": getattr( - request.smargon_top_left.sh_mm, "z", None - ), + "top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None), + "top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None), + "top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None), }, ), ) - self.logger.info( - f"moving Smargon to grid centre offset {target_coor_offset}" - ) + self.logger.info(f"moving Smargon to grid centre offset {target_coor_offset}") target_smargon = SmargonCoordinate( sh_mm=request.smargon_top_left.sh_mm + target_coor_offset, phi_deg=request.smargon_top_left.phi_deg, @@ -631,9 +566,7 @@ class RasterService: else None ) if sample_id: - self.logger.debug( - "moving to XtalSnapshot to take a screenshot of the sample" - ) + self.logger.debug("moving to XtalSnapshot to take a screenshot of the sample") if self.ctx.services.state is None: raise RuntimeError("RasterService requires services.state") self.ctx.services.state.set_state(BeamlineStateEnum.XtalSnapshot) @@ -642,8 +575,7 @@ class RasterService: time.sleep(wait_for_screenshot) self.ctx.services.screenshots.save_to_db( - sample_id, - f"{sample_id}_post_raster_{int(request.omega_deg)}deg", + sample_id, f"{sample_id}_post_raster_{int(request.omega_deg)}deg" ) if self.ctx.services.ingestion is not None: @@ -653,9 +585,7 @@ class RasterService: raster_request=request, geom=self.ctx.sample_geometry, com=None, - beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark( - self.ctx.deps.devs.zoom - ), + beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom), ) else: self.ctx.deps.aare.ingest_gridscan( @@ -664,9 +594,7 @@ class RasterService: raster_request=request, geom=self.ctx.sample_geometry, com=None, - beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark( - self.ctx.deps.devs.zoom - ), + beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom), ) if com is not None and com.max_image is not None: @@ -677,9 +605,7 @@ class RasterService: f"{self.ctx.sample.db_id}_diffraction_image_near_grid_scan" ) diffraction_image_id = self._grid_image_id_from_centre_offset( - x_mm=x, - y_mm=y, - request=request, + x_mm=x, y_mm=y, request=request ) self._upload_raster_diffraction_preview( @@ -708,10 +634,7 @@ class RasterService: extra=merge_log_context( sample_log_context(self.ctx.sample), raster_request_log_context(request), - { - "scan_result_is_none": scan_result is None, - "sample_id": sample_id, - }, + {"scan_result_is_none": scan_result is None, "sample_id": sample_id}, ), ) self.ctx.deps.cfg.crystal_size = get_xtal_size( @@ -719,16 +642,10 @@ class RasterService: compute_crystal_score_array(scan_result.images), r=request, ) - self.ctx.deps.cfg.last_best_res = get_best_res( - result_list=scan_result.images - ) - self.ctx.deps.cfg.last_best_b_factor = get_best_b_factor( - result_list=scan_result.images - ) + self.ctx.deps.cfg.last_best_res = get_best_res(result_list=scan_result.images) + self.ctx.deps.cfg.last_best_b_factor = get_best_b_factor(result_list=scan_result.images) return CompletedRasterGridElem( - request=copy.deepcopy(request), - result=scan_result, - centre_of_mass=com, + request=copy.deepcopy(request), result=scan_result, centre_of_mass=com ) except Exception as e: @@ -742,9 +659,7 @@ class RasterService: ) raise - def execute_auto_center( - self, request: RasterGridRequest - ) -> CompletedRasterGrid | None: + def execute_auto_center(self, request: RasterGridRequest) -> CompletedRasterGrid | None: sample = self.ctx.sample if sample is None: raise Exception("Sample must be mounted to auto center") @@ -787,17 +702,12 @@ class RasterService: "No ML bounding box found at primary angle during auto-center raster", extra=merge_log_context( sample_log_context(sample), - { - "omega_deg": geom.omega_deg, - "file_prefix": old_prefix, - }, + {"omega_deg": geom.omega_deg, "file_prefix": old_prefix}, ), ) self.ctx.deps.devs.aerotech_omega = geom.omega_deg + 90.0 time.sleep(0.2) - plan = self.ml_raster_plan( - sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg" - ) + plan = self.ml_raster_plan(sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg") if plan is None: self.logger.error( @@ -856,9 +766,7 @@ class RasterService: self.ctx.deps.jfjoch.measure_raster(grid, status) self.logger.info("detector initialised") else: - self.logger.info( - "Simulated detector mode enabled; using fake raster result." - ) + self.logger.info("Simulated detector mode enabled; using fake raster result.") if self.ctx.services.state is None: raise RuntimeError("RasterService requires services.state") @@ -896,18 +804,13 @@ class RasterService: grid.n_x = 1 grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg" geom = self.ctx.sample_geometry - grid.grid_size_mm = Coordinate( - x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25 - ) + grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25) grid.smargon_top_left, grid.n_y = self.auto_center_line_scan_top_left( omega_deg=grid.omega_deg, file_prefix=grid.file_prefix, grid_size_mm=grid.grid_size_mm, default_n_y=50, - y_retarget_threshold_mm=max( - geom.beam_size_mm.y * 2.0, - grid.grid_size_mm.y * 4.0, - ), + y_retarget_threshold_mm=max(geom.beam_size_mm.y * 2.0, grid.grid_size_mm.y * 4.0), ) self.logger.info( @@ -929,9 +832,7 @@ class RasterService: self.ctx.deps.jfjoch.measure_raster(grid, status) self.logger.info("detector initialised") else: - self.logger.info( - "Simulated detector mode enabled; using fake raster result." - ) + self.logger.info("Simulated detector mode enabled; using fake raster result.") if self.ctx.services.state is None: raise RuntimeError("RasterService requires services.state") @@ -940,8 +841,7 @@ class RasterService: self.logger.info( "Running second auto-center raster", extra=merge_log_context( - sample_log_context(self.ctx.sample), - raster_request_log_context(grid), + sample_log_context(self.ctx.sample), raster_request_log_context(grid) ), ) res2 = self.execute(grid) diff --git a/src/aare/daq/operations/rotation/models.py b/src/aare/daq/operations/rotation/models.py index 42f4bc6b..532cc5b3 100644 --- a/src/aare/daq/operations/rotation/models.py +++ b/src/aare/daq/operations/rotation/models.py @@ -1,10 +1,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from aare.daq.operations.common.models import ( - BaseOperationContext, - BeamlineDependencies, -) +from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies if TYPE_CHECKING: from aare.daq.aaredb import AareWrapper @@ -24,4 +21,4 @@ class RotationSettings: @dataclass class RotationContext(BaseOperationContext[RotationDependencies, RotationSettings]): - pass \ No newline at end of file + pass diff --git a/src/aare/daq/operations/rotation/service.py b/src/aare/daq/operations/rotation/service.py index d6dfdcce..aa63e104 100644 --- a/src/aare/daq/operations/rotation/service.py +++ b/src/aare/daq/operations/rotation/service.py @@ -20,11 +20,7 @@ class RotationService: self.logger.info("Simulated detector mode enabled; skipping JFJoch start.") return - self.ctx.deps.jfjoch.measure_rotation( - request, - self.ctx.status, - self.ctx.deps.cfg.xrf, - ) + self.ctx.deps.jfjoch.measure_rotation(request, self.ctx.status, self.ctx.deps.cfg.xrf) def _execute_scan(self, request: RotationScanRequest) -> CompletedRotationScan: omega_start = self.ctx.deps.devs.aerotech_omega @@ -66,9 +62,7 @@ class RotationService: if request.start is not None and request.end is not None: smargon_time_step = request.exp_time_s / float(request.steps) - pos_step = (request.end.sh_mm - request.start.sh_mm) * ( - 1.0 / float(request.steps) - ) + pos_step = (request.end.sh_mm - request.start.sh_mm) * (1.0 / float(request.steps)) for i in range(request.steps): self.ctx.deps.devs.smargon.target = SmargonCoordinate( @@ -76,22 +70,15 @@ class RotationService: ) time.sleep(smargon_time_step) - self.ctx.deps.devs.aerotech.wait_till_done( - timeout=int(round(total_time + 60, 0)) - ) + self.ctx.deps.devs.aerotech.wait_till_done(timeout=int(round(total_time + 60, 0))) self.ctx.deps.devs.aerotech_omega = omega_start if self.ctx.deps.cfg.simulated_detector: - self.logger.warning( - "Detector in simulation mode, returning fake zero rotation result." - ) + self.logger.warning("Detector in simulation mode, returning fake zero rotation result.") return build_fake_rotation_result(request, start_angle=float(omega_start)) scan_result = self.ctx.deps.jfjoch.wait_till_done(60) - return CompletedRotationScan( - request=copy.deepcopy(request), - result=scan_result, - ) + return CompletedRotationScan(request=copy.deepcopy(request), result=scan_result) def _ingest_screening_diffraction(self, sample, scan_result) -> None: """Pull each screening wedge's diffraction image from JFJoch (with spot @@ -110,9 +97,7 @@ class RotationService: ) except Exception: self.logger.warning( - "Screening diffraction image %s unavailable; skipping", - image_id, - exc_info=True, + "Screening diffraction image %s unavailable; skipping", image_id, exc_info=True ) continue @@ -141,11 +126,7 @@ class RotationService: self.ctx.services.datacollection.prepare(request) self._prepare_detector(request) - if ( - sample is not None - and sample.db_id is not None - and self.ctx.services.events is not None - ): + if sample is not None and sample.db_id is not None and self.ctx.services.events is not None: self.ctx.services.events.send(sample.db_id, SampleEventType.COLLECTING) if self.ctx.services.state is not None: @@ -159,16 +140,13 @@ class RotationService: if sample is not None and sample.db_id is not None: if self.ctx.services.screenshots is not None: self.ctx.services.screenshots.save_to_db( - sample.db_id, - self.ctx.settings.preview_filename, + sample.db_id, self.ctx.settings.preview_filename ) # For screening runs, ingest the per-wedge diffraction images. Done # before the COLLECTED event so they pin to the same sample event the # run is bound to (the COLLECTING event), matching the preview above. - self.logger.debug( - f"Ingesting screening diffraction images: {request.screening}" - ) + self.logger.debug(f"Ingesting screening diffraction images: {request.screening}") if request.screening and not self.ctx.deps.cfg.simulated_detector: self._ingest_screening_diffraction(sample, result.result) @@ -180,9 +158,7 @@ class RotationService: sample=sample, result=result.result, geom=self.ctx.sample_geometry, - beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark( - self.ctx.deps.devs.zoom - ), + beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom), ) return result diff --git a/src/aare/daq/operations/screenshot/service.py b/src/aare/daq/operations/screenshot/service.py index 18561a21..13d20dac 100644 --- a/src/aare/daq/operations/screenshot/service.py +++ b/src/aare/daq/operations/screenshot/service.py @@ -15,11 +15,7 @@ from aare.devices.mx_lib import clean_filename class NonCriticalRunner(Protocol): def __call__( - self, - action, - *, - description: str, - sample: SampleShortInfo | None = None, + self, action, *, description: str, sample: SampleShortInfo | None = None ) -> object | None: ... @@ -68,9 +64,7 @@ class ScreenshotService: self.logger.debug(f"saving screenshot {filename} from inference image") cv2.imwrite(f"{self.output_dir}/{filename}.jpg", bgr_image) - def save_to_db( - self, sample_id: int, filename: str, settle_time_s: float = 0.2 - ) -> None: + def save_to_db(self, sample_id: int, filename: str, settle_time_s: float = 0.2) -> None: time.sleep(settle_time_s) sample = self._current_sample() bgr_image = self._get_inference_image() @@ -78,18 +72,10 @@ class ScreenshotService: def _upload() -> None: self.aare.upload_image(sample_id, filename, bgr_image) - self.run_noncritical( - _upload, - description=f"screenshot upload '{filename}'", - sample=sample, - ) + self.run_noncritical(_upload, description=f"screenshot upload '{filename}'", sample=sample) def send_to_db( - self, - *, - filename: str | None = None, - message: str | None = None, - default_message: str, + self, *, filename: str | None = None, message: str | None = None, default_message: str ) -> None: sample = self._current_sample() if sample is None or sample.db_id is None or sample.db_id < 0: @@ -106,9 +92,7 @@ class ScreenshotService: "No active pgroup set; cannot save screenshot to photos directory." ) - photos_dir = ( - Path(self.photos_root) / pgroup / "raw" / "photos" / str(sample_id) - ) + photos_dir = Path(self.photos_root) / pgroup / "raw" / "photos" / str(sample_id) photos_dir.mkdir(parents=True, exist_ok=True) photo_path = photos_dir / f"{safe_filename}.jpeg" cv2.imwrite(str(photo_path), bgr_image) @@ -119,12 +103,6 @@ class ScreenshotService: final_message = (message or "").strip() or default_message def _upload() -> None: - self.aare.upload_image( - sample_id, upload_name, bgr_image, message=final_message - ) + self.aare.upload_image(sample_id, upload_name, bgr_image, message=final_message) - self.run_noncritical( - _upload, - description=f"send screenshot '{upload_name}'", - sample=sample, - ) + self.run_noncritical(_upload, description=f"send screenshot '{upload_name}'", sample=sample) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 451f5b2a..62900c80 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -77,10 +77,7 @@ _face_detection_state: dict = { } _face_detection_state_lock = asyncio.Lock() -_automation_progress_state: dict = { - "seq": 0, - "progress": None, -} +_automation_progress_state: dict = {"seq": 0, "progress": None} _automation_progress_state_lock = asyncio.Lock() @@ -172,8 +169,7 @@ def _validate_recovery_code(confirmation_code: str) -> None: if not hmac.compare_digest(provided, expected): logger.warning("Invalid recovery confirmation code.") raise HTTPException( - status_code=api_status.HTTP_403_FORBIDDEN, - detail="Invalid confirmation code.", + status_code=api_status.HTTP_403_FORBIDDEN, detail="Invalid confirmation code." ) @@ -200,10 +196,7 @@ def _push_face_detection_progress(payload: dict) -> None: global _face_detection_state try: next_seq = int(_face_detection_state.get("seq", 0)) + 1 - _face_detection_state = { - "seq": next_seq, - **payload, - } + _face_detection_state = {"seq": next_seq, **payload} except Exception as e: logger.warning(f"Failed to update face detection progress: {e}") @@ -346,18 +339,14 @@ async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel: full.sample = sample if in_ro and sample_view_allowed else None full.box = getattr(full, "box", None) if in_ro else None full.last_best_res = getattr(full, "last_best_res", None) if in_ro else None - full.last_best_b_factor = ( - getattr(full, "last_best_b_factor", None) if in_ro else None - ) + full.last_best_b_factor = getattr(full, "last_best_b_factor", None) if in_ro else None full.crystal_size = ( getattr(full, "crystal_size", CrystalSize(x=0, y=0, z=0)) if in_ro else CrystalSize(x=0, y=0, z=0) ) full.session = SessionStatus( - current_pgroup=cfg.pgroup, - session=cfg.session_state(data.session), - staff=data.staff, + current_pgroup=cfg.pgroup, session=cfg.session_state(data.session), staff=data.staff ) if data.staff: @@ -369,9 +358,7 @@ async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel: @app.get("/beamline/geometry") -async def sample_geometry( - token: str = Depends(oauth2_scheme), -) -> SampleGeometryModel: +async def sample_geometry(token: str = Depends(oauth2_scheme)) -> SampleGeometryModel: """ Get the sample geometry information. @@ -494,9 +481,7 @@ async def mono_pitch_scan(plot: bool = False, token: str = Depends(oauth2_scheme @app.put("/beamline/change_energy") -async def change_energy( - value: float, plot: bool = False, token: str = Depends(oauth2_scheme) -): +async def change_energy(value: float, plot: bool = False, token: str = Depends(oauth2_scheme)): """ Change monochromator energy. Staff only. @@ -534,9 +519,7 @@ async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)): @app.post("/beamline/tweak_abr_meas_pos") -async def tweak_abr_meas_pos( - val: AerotechCoordinate, token: str = Depends(oauth2_scheme) -): +async def tweak_abr_meas_pos(val: AerotechCoordinate, token: str = Depends(oauth2_scheme)): """ Tweak the Aerotech measurement position. Staff only. @@ -615,10 +598,7 @@ async def initialise_smargon(token: str = Depends(oauth2_scheme)) -> dict: data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.initialise_smargon() - return { - "ok": True, - "message": "Smargon initialised.", - } + return {"ok": True, "message": "Smargon initialised."} @app.post("/bec/load_user_macros") @@ -629,10 +609,7 @@ async def bec_load_user_macros(token: str = Depends(oauth2_scheme)) -> dict: data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.bec_load_user_macros() - return { - "ok": True, - "message": "BEC user macros loaded.", - } + return {"ok": True, "message": "BEC user macros loaded."} @app.get("/bec/user_macros") @@ -657,8 +634,7 @@ async def bec_list_all_devices(token: str = Depends(oauth2_scheme)) -> list: @app.post("/bec/reinitialise_planner_and_position_devices") async def bec_reinitialise_planner_and_position_devices( - method: str = "auto", - token: str = Depends(oauth2_scheme), + method: str = "auto", token: str = Depends(oauth2_scheme) ) -> dict: """ Reinitialise BEC planner and position devices. Staff only. @@ -689,10 +665,7 @@ async def bec_save_current_bs_pos(token: str = Depends(oauth2_scheme)) -> dict: data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.bec_save_current_bs_pos() - return { - "ok": True, - "message": "Saved current BEC beamstop work position.", - } + return {"ok": True, "message": "Saved current BEC beamstop work position."} @app.post("/bec/save_current_collimator_pos") @@ -703,26 +676,18 @@ async def bec_save_current_collimator_pos(token: str = Depends(oauth2_scheme)) - data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.bec_save_current_collimator_pos() - return { - "ok": True, - "message": "Saved current BEC collimator work position.", - } + return {"ok": True, "message": "Saved current BEC collimator work position."} @app.post("/bec/save_current_aerotech_position") -async def bec_save_current_aerotech_position( - token: str = Depends(oauth2_scheme), -) -> dict: +async def bec_save_current_aerotech_position(token: str = Depends(oauth2_scheme)) -> dict: """ Save the current BEC aerotech work position and reload device config. Staff only. """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.bec_save_current_aerotech_position() - return { - "ok": True, - "message": "Saved current BEC aerotech work position and reloaded devices.", - } + return {"ok": True, "message": "Saved current BEC aerotech work position and reloaded devices."} def initialise_aerotech(self): @@ -790,9 +755,7 @@ async def local_contact_links(token: str = Depends(oauth2_scheme)) -> dict: @app.post("/local_contact/simulate/{device}") async def local_contact_set_simulation( - device: str, - enabled: bool, - token: str = Depends(oauth2_scheme), + device: str, enabled: bool, token: str = Depends(oauth2_scheme) ) -> dict: """ Enable or disable runtime simulation for a backend device and restart its wrapper. Staff only. @@ -805,10 +768,7 @@ async def local_contact_set_simulation( @app.post("/local_contact/restart/{device}") -async def local_contact_restart_device( - device: str, - token: str = Depends(oauth2_scheme), -) -> dict: +async def local_contact_restart_device(device: str, token: str = Depends(oauth2_scheme)) -> dict: """ Restart a Local Contact backend wrapper. Staff only. """ @@ -837,9 +797,7 @@ async def local_contact_restart_device( @app.post("/local_contact/resync/detector_metadata") -async def local_contact_resync_detector_metadata( - token: str = Depends(oauth2_scheme), -) -> dict: +async def local_contact_resync_detector_metadata(token: str = Depends(oauth2_scheme)) -> dict: """ Refresh cached detector metadata and DTZ limits. Staff only. """ @@ -847,17 +805,11 @@ async def local_contact_resync_detector_metadata( auth.check_jwt_staff_only(data) payload = daq.refresh_detector_metadata_cache() - return { - "ok": True, - "message": "Hardware metadata cache resynced.", - "payload": payload, - } + return {"ok": True, "message": "Hardware metadata cache resynced.", "payload": payload} @app.get("/local_contact/config") -async def local_contact_config( - token: str = Depends(oauth2_scheme), -) -> LocalContactConfigModel: +async def local_contact_config(token: str = Depends(oauth2_scheme)) -> LocalContactConfigModel: """ Return Local Contact config values. Staff only. """ @@ -868,8 +820,7 @@ async def local_contact_config( @app.put("/local_contact/config") async def local_contact_set_config( - payload: LocalContactConfigModel, - token: str = Depends(oauth2_scheme), + payload: LocalContactConfigModel, token: str = Depends(oauth2_scheme) ) -> LocalContactConfigModel: """ Update Local Contact config values. Staff only. @@ -1054,9 +1005,7 @@ async def get_image(token: str = Depends(oauth2_scheme)): """ auth.check_jwt_ro(cfg, auth.parse_token(token)) - _, encoded_image = cv2.imencode( - ".jpg", daq.camera_image - ) # Encodes the image in JPEG format + _, encoded_image = cv2.imencode(".jpg", daq.camera_image) # Encodes the image in JPEG format image_bytes = io.BytesIO( encoded_image.tobytes() ) # Convert OpenCV byte format to a file-like object @@ -1108,10 +1057,7 @@ async def park_and_dry(token: str = Depends(oauth2_scheme)): auth.check_jwt_rw(cfg, auth.parse_token(token)) logger.debug("Executing unmount, dry and park") daq.park_and_dry(park=True, unmount=True) - return { - "ok": True, - "message": "TELL has been dried and parked", - } + return {"ok": True, "message": "TELL has been dried and parked"} @app.post("/tell/dry") @@ -1129,10 +1075,7 @@ async def tell_dry(token: str = Depends(oauth2_scheme)) -> dict: auth.check_jwt_rw(data) logger.debug("Executing dry and return to dewar") daq.park_and_dry(park=False, unmount=False) - return { - "ok": True, - "message": "TELL dry cycle completed.", - } + return {"ok": True, "message": "TELL dry cycle completed."} @app.post("/tell/toggle_blower") @@ -1149,16 +1092,11 @@ async def tell_toggle_blower(token: str = Depends(oauth2_scheme)) -> dict: data = auth.parse_token(token) auth.check_jwt_staff_only(data) daq.blower_control() - return { - "ok": True, - "message": "TELL blower toggled.", - } + return {"ok": True, "message": "TELL blower toggled."} @app.post("/sample/mount") -async def mount( - dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False -): +async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False): """ Mount a sample from the spreadsheet onto the goniometer. @@ -1244,10 +1182,7 @@ async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict: auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.sync_current_sample_from_tell(force=True) logger.info("TELL sample cache resynced via API request.") - return { - "ok": True, - "message": "TELL sample cache resynced.", - } + return {"ok": True, "message": "TELL sample cache resynced."} def get_spreadsheet(data: TokenData) -> SampleShortInfoList: @@ -1569,16 +1504,13 @@ async def take_over_beamline( _validate_recovery_code(payload.confirmation_code) auth.force_current_sesion(cfg, data) logger.warning( - "Beamline session forcefully taken over.", - extra={"session": getattr(data, "session", None)}, + "Beamline session forcefully taken over.", extra={"session": getattr(data, "session", None)} ) return "OK" @app.post("/state/free_beamline") -async def free_beamline( - payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme) -) -> str: +async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str: """ Clear the beamline busy flag. Staff only. @@ -1680,11 +1612,7 @@ async def recovery_unmount_sample( sample_mounted = _sample_is_mounted() if not sample_mounted: - return { - "ok": True, - "sample_mounted": False, - "message": "No sample appears to be mounted.", - } + return {"ok": True, "sample_mounted": False, "message": "No sample appears to be mounted."} prev_state = cfg.state daq.recovery_unmount_sample() @@ -1709,9 +1637,7 @@ async def recovery_unmount_sample( # Scans @app.post("/scan/raster") async def raster( - val: RasterGridRequest, - auto_center: bool = False, - token: str = Depends(oauth2_scheme), + val: RasterGridRequest, auto_center: bool = False, token: str = Depends(oauth2_scheme) ) -> CompletedRasterGrid: """ Execute a raster scan. @@ -1775,9 +1701,7 @@ async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): @app.post("/scan/smart_params") -async def set_smart_params( - p: SimpleScanParameters, token: str = Depends(oauth2_scheme) -) -> str: +async def set_smart_params(p: SimpleScanParameters, token: str = Depends(oauth2_scheme)) -> str: """ Set the 'smart' scan parameters. @@ -1825,9 +1749,7 @@ async def alc_center_loop(token: str = Depends(oauth2_scheme)) -> str: @app.post("/alc/ml_bounding_box") -async def alc_ml_bounding_box( - token: str = Depends(oauth2_scheme), -) -> RasterGridRequest | None: +async def alc_ml_bounding_box(token: str = Depends(oauth2_scheme)) -> RasterGridRequest | None: """ Request an ML-based bounding box for the sample. @@ -1859,13 +1781,7 @@ async def face_detection_run( logger.debug(f"Face detection run: {steps} steps, {step_size} step size") auth.check_jwt_rw(cfg, auth.parse_token(token)) _push_face_detection_progress( - { - "running": True, - "status": "starting", - "samples": [], - "height_fit": {}, - "area_fit": {}, - } + {"running": True, "status": "starting", "samples": [], "height_fit": {}, "area_fit": {}} ) result = daq.face_detection(steps=steps, step_size=step_size) return result @@ -2006,9 +1922,7 @@ async def get_gui_sessions(token: str = Depends(oauth2_scheme)) -> list[dict]: @app.post("/admin/gui_sessions/{session_id}/request_close") async def request_gui_close( - session_id: int, - grace_seconds: int = 60, - token: str = Depends(oauth2_scheme), + session_id: int, grace_seconds: int = 60, token: str = Depends(oauth2_scheme) ) -> dict: """ Staff-only request for a remote GUI to close gracefully. @@ -2017,28 +1931,18 @@ async def request_gui_close( auth.check_jwt_staff_only(data) payload = cfg.request_gui_close( - session=session_id, - requested_by=data.sub, - grace_seconds=grace_seconds, + session=session_id, requested_by=data.sub, grace_seconds=grace_seconds ) if payload is None: raise HTTPException( - status_code=api_status.HTTP_404_NOT_FOUND, - detail="GUI session not found.", + status_code=api_status.HTTP_404_NOT_FOUND, detail="GUI session not found." ) - return { - "ok": True, - "session": session_id, - "grace_seconds": grace_seconds, - } + return {"ok": True, "session": session_id, "grace_seconds": grace_seconds} @app.delete("/admin/gui_sessions/{session_id}") -async def force_remove_gui_session( - session_id: int, - token: str = Depends(oauth2_scheme), -) -> dict: +async def force_remove_gui_session(session_id: int, token: str = Depends(oauth2_scheme)) -> dict: """ Staff-only hard removal of a GUI session from Redis. @@ -2052,18 +1956,11 @@ async def force_remove_gui_session( cfg.end_active_session(session_id) cfg.remove_gui_session(session_id) - return { - "ok": True, - "session": session_id, - "message": "GUI session force removed.", - } + return {"ok": True, "session": session_id, "message": "GUI session force removed."} @app.post("/admin/gui_sessions/{session_id}/interaction") -async def update_gui_interaction( - session_id: int, - token: str = Depends(oauth2_scheme), -) -> dict: +async def update_gui_interaction(session_id: int, token: str = Depends(oauth2_scheme)) -> dict: """ GUI-side activity heartbeat. This is distinct from /status so idle timeout ignores status polling. @@ -2075,13 +1972,10 @@ async def update_gui_interaction( detail="Cannot update interaction for another session.", ) - payload = cfg.update_gui_interaction( - session=session_id, last_interaction_ts=time.time() - ) + payload = cfg.update_gui_interaction(session=session_id, last_interaction_ts=time.time()) if payload is None: raise HTTPException( - status_code=api_status.HTTP_404_NOT_FOUND, - detail="GUI session not found.", + status_code=api_status.HTTP_404_NOT_FOUND, detail="GUI session not found." ) return {"ok": True} @@ -2359,9 +2253,7 @@ async def put_settings(s: BeamlineSettingsModel, token: str = Depends(oauth2_sch @app.get("/beamline/cryo_settings") -async def get_cryo_settings( - token: str = Depends(oauth2_scheme), -) -> CryojetSettingsModel: +async def get_cryo_settings(token: str = Depends(oauth2_scheme)) -> CryojetSettingsModel: """ Get the current cryojet settings. Staff only. @@ -2377,9 +2269,7 @@ async def get_cryo_settings( @app.put("/beamline/cryo_settings") -async def put_cryo_settings( - s: CryojetSettingsModel, token: str = Depends(oauth2_scheme) -): +async def put_cryo_settings(s: CryojetSettingsModel, token: str = Depends(oauth2_scheme)): """ Update the cryojet settings. Staff only. @@ -2456,9 +2346,7 @@ async def fluorimeter_spectrum( @app.post("/fluorimeter/start") -async def fluorimeter_start( - erase: bool = False, token: str = Depends(oauth2_scheme) -) -> str: +async def fluorimeter_start(erase: bool = False, token: str = Depends(oauth2_scheme)) -> str: """ Start the fluorimeter measurement. @@ -2521,9 +2409,7 @@ async def fluorimeter_data(token: str = Depends(oauth2_scheme)) -> list[int] | N @app.get("/fluorimeter/background") -async def fluorimeter_background( - token: str = Depends(oauth2_scheme), -) -> list[int] | None: +async def fluorimeter_background(token: str = Depends(oauth2_scheme)) -> list[int] | None: """ Get the fluorimeter background data. @@ -2559,24 +2445,14 @@ async def fluorimeter_stream() -> AsyncGenerator[str, None]: data = ( d.tolist() if hasattr(d, "tolist") - else ( - list(d) - if isinstance(d, (tuple, list)) - else ([] if d is None else [d]) - ) + else (list(d) if isinstance(d, (tuple, list)) else ([] if d is None else [d])) ) bkg = ( b.tolist() if hasattr(b, "tolist") - else ( - list(b) - if isinstance(b, (tuple, list)) - else ([] if b is None else [b]) - ) - ) - logger.debug( - f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}" + else (list(b) if isinstance(b, (tuple, list)) else ([] if b is None else [b])) ) + logger.debug(f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}") payload = json.dumps( {"status": s, "data": data, "background": bkg}, separators=(",", ":") ) @@ -2622,9 +2498,7 @@ async def sse_fluorimeter(token: str = Depends(oauth2_scheme)): @app.post("/samcam/send_screenshot_db") async def send_screenshot_db( - filename: str | None = None, - message: str | None = None, - token: str = Depends(oauth2_scheme), + filename: str | None = None, message: str | None = None, token: str = Depends(oauth2_scheme) ) -> str: """ Capture a screenshot and send it to the database. @@ -2684,9 +2558,7 @@ async def maintenance(token: str = Depends(oauth2_scheme)) -> str: auth.check_jwt_staff(cfg, auth.parse_token(token)) cfg.state = BeamlineStateEnum.Maintenance logger.warning("Beamline state set to Maintenance via protected endpoint.") - raise MaintenanceStateException( - "Beamline was set to Maintenance. Automation must stop." - ) + raise MaintenanceStateException("Beamline was set to Maintenance. Automation must stop.") def main(): diff --git a/src/aare/daq/server_exception_handler.py b/src/aare/daq/server_exception_handler.py index bcaef31f..89678b69 100644 --- a/src/aare/daq/server_exception_handler.py +++ b/src/aare/daq/server_exception_handler.py @@ -86,10 +86,7 @@ def _extract_context(exc: Exception) -> dict[str, Any]: def _error_body( - exc: Exception, - *, - code_override: str | None = None, - critical_override: bool | None = None, + exc: Exception, *, code_override: str | None = None, critical_override: bool | None = None ) -> dict[str, Any]: """Build the unified error response body for any exception. @@ -125,9 +122,7 @@ def register_exception_handlers(app) -> None: """ @app.exception_handler(AutomationError) - async def _handle_automation_error( - request: Request, exc: AutomationError - ) -> JSONResponse: + async def _handle_automation_error(request: Request, exc: AutomationError) -> JSONResponse: body = _error_body(exc) log = logger.error if exc.critical else logger.warning log( @@ -159,10 +154,7 @@ def register_exception_handlers(app) -> None: **body["context"], }, ) - return JSONResponse( - status_code=api_status.HTTP_400_BAD_REQUEST, - content=body, - ) + return JSONResponse(status_code=api_status.HTTP_400_BAD_REQUEST, content=body) @app.exception_handler(AareAuthError) async def _handle_auth_error(request: Request, exc: AareAuthError) -> JSONResponse: @@ -189,26 +181,16 @@ def register_exception_handlers(app) -> None: "status_code": status, }, ) - return JSONResponse( - status_code=status, - content=body, - headers=getattr(exc, "headers", None), - ) + return JSONResponse(status_code=status, content=body, headers=getattr(exc, "headers", None)) @app.exception_handler(HTTPException) - async def _handle_http_exception( - request: Request, exc: HTTPException - ) -> JSONResponse: + async def _handle_http_exception(request: Request, exc: HTTPException) -> JSONResponse: """HTTPException isn't in the AareException hierarchy but FastAPI raises it internally (e.g. validation, route-not-found) and routes raise it manually. Adapt to the new body shape so clients see one schema.""" detail = exc.detail log = logger.error if exc.status_code >= 500 else logger.warning - log( - "HTTPException %s: %s", - exc.status_code, - detail, - ) + log("HTTPException %s: %s", exc.status_code, detail) # Preserve legacy callers that passed a {code, message} dict if isinstance(detail, dict) and "code" in detail and "message" in detail: legacy_code = str(detail.get("code")) @@ -220,9 +202,7 @@ def register_exception_handlers(app) -> None: "code": legacy_code, "exception_class": "HTTPException", "message": legacy_message, - "context": { - k: v for k, v in detail.items() if k not in ("code", "message") - }, + "context": {k: v for k, v in detail.items() if k not in ("code", "message")}, }, headers=exc.headers, ) @@ -245,8 +225,6 @@ def register_exception_handlers(app) -> None: return JSONResponse( status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR, content=_error_body( - exc, - code_override=str(AareErrorCode.INTERNAL_ERROR), - critical_override=True, + exc, code_override=str(AareErrorCode.INTERNAL_ERROR), critical_override=True ), ) diff --git a/src/aare/daq/spreadsheetupdater.py b/src/aare/daq/spreadsheetupdater.py index dda5acc5..f517286b 100644 --- a/src/aare/daq/spreadsheetupdater.py +++ b/src/aare/daq/spreadsheetupdater.py @@ -27,9 +27,7 @@ def set_spreadsheet_in_redis(spreadsheet): "[REDIS][DEBUG] Data to write:", json.dumps(spreadsheet, indent=4) ) # Pretty-print the data print("[REDIS][INFO] Writing spreadsheet to Redis...") - config.__client.set( - f"{config._BeamlineConfig__bl}:spreadsheet", json.dumps(spreadsheet) - ) + config.__client.set(f"{config._BeamlineConfig__bl}:spreadsheet", json.dumps(spreadsheet)) def on_message(ws, message): @@ -39,9 +37,7 @@ def on_message(ws, message): try: data = json.loads(message) - pucks_data = ( - data["samples"] if isinstance(data, dict) and "samples" in data else data - ) + pucks_data = data["samples"] if isinstance(data, dict) and "samples" in data else data pucks = [PuckWithTellPosition(**item) for item in pucks_data] print(f"INFO: Received pucks: {[p.puck_name for p in pucks]}") @@ -56,8 +52,7 @@ def on_message(ws, message): # Reference tool if isinstance(p.tell_position, str) and p.tell_position.startswith("X"): dewar_address = DewarAddress( - segment="X", - pos=int(p.tell_position[1:]) if len(p.tell_position) > 1 else 1, + segment="X", pos=int(p.tell_position[1:]) if len(p.tell_position) > 1 else 1 ) target_list = reference_short_infos @@ -69,9 +64,7 @@ def on_message(ws, message): target_list = normal_short_infos else: - print( - f"[WARN] Skipping unknown tell_position format: {p.tell_position}" - ) + print(f"[WARN] Skipping unknown tell_position format: {p.tell_position}") continue info = SampleShortInfo( diff --git a/src/aare/daq/tell_state_machine.py b/src/aare/daq/tell_state_machine.py index eb1eec90..2bbe432d 100644 --- a/src/aare/daq/tell_state_machine.py +++ b/src/aare/daq/tell_state_machine.py @@ -46,9 +46,7 @@ def _update_state( mount_success: bool | None = None, mount_error: str | None = None, ) -> TellStateModel: - updates = { - "last_update_ts": _utc_now_iso(), - } + updates = {"last_update_ts": _utc_now_iso()} if activity is not None: updates["activity"] = activity @@ -72,9 +70,7 @@ def _update_state( return state.model_copy(update=updates) -def _ready_state( - state: TellStateModel, event_name: str, event_value: str | None -) -> TellStateModel: +def _ready_state(state: TellStateModel, event_name: str, event_value: str | None) -> TellStateModel: return _update_state( state, activity=TellActivityEnum.IDLE, @@ -88,9 +84,7 @@ def _ready_state( def advance_tell_state( - state: TellStateModel, - event_name: str, - event_value: str | None, + state: TellStateModel, event_name: str, event_value: str | None ) -> TellStateModel: value = (event_value or "").strip() @@ -120,20 +114,14 @@ def advance_tell_state( if event_name == "shell": if value == "true": - return _update_state( - state, - event_name=event_name, - event_value=value, - ) + return _update_state(state, event_name=event_name, event_value=value) if value.startswith("mount("): sample_position = _parse_sample_position_from_mount_command(value) return _update_state( state, activity=TellActivityEnum.MOUNTING, - message=f"Mounting {sample_position}" - if sample_position - else "Mounting sample", + message=f"Mounting {sample_position}" if sample_position else "Mounting sample", event_name=event_name, event_value=value, operation="mount", @@ -201,11 +189,7 @@ def advance_tell_state( event_value=value, ) - return _update_state( - state, - event_name=event_name, - event_value=value, - ) + return _update_state(state, event_name=event_name, event_value=value) if value == "got result False": if state.operation == "mount" and state.phase in { @@ -260,11 +244,7 @@ def advance_tell_state( mount_error="", ) - return _update_state( - state, - event_name=event_name, - event_value=value, - ) + return _update_state(state, event_name=event_name, event_value=value) if event_name == "Motion Sync": if value == "Sample get from Gonio": @@ -304,9 +284,7 @@ def advance_tell_state( event_value=value, operation=next_operation, phase=next_phase, - mount_success=True - if state.operation == "unmount" - else state.mount_success, + mount_success=True if state.operation == "unmount" else state.mount_success, mount_error="" if state.operation == "unmount" else state.mount_error, ) @@ -366,15 +344,9 @@ def advance_tell_state( if value == "Busy": return _update_state( state, - message=state.message - if state.operation not in {None, "idle"} - else "Busy", + message=state.message if state.operation not in {None, "idle"} else "Busy", event_name=event_name, event_value=value, ) - return _update_state( - state, - event_name=event_name, - event_value=value, - ) + return _update_state(state, event_name=event_name, event_value=value) diff --git a/src/aare/daq/tellupdater.py b/src/aare/daq/tellupdater.py index 264aef7d..59975a63 100644 --- a/src/aare/daq/tellupdater.py +++ b/src/aare/daq/tellupdater.py @@ -12,10 +12,7 @@ import sseclient import websocket from aarecommon.config.beamline import mx_beamline from aarecommon.config.logger import setup_logger -from aarecommon.errors.exception_handler import ( - TellCommunicationError, - TellConnectionException, -) +from aarecommon.errors.exception_handler import TellCommunicationError, TellConnectionException from aarecommon.models.beamline import MXBeamline from aarecommon.models.tell import TellStateModel from aareDB.exceptions import ApiException @@ -42,16 +39,10 @@ last_pucks_sent = [] DEWAR_CONTENT_UPDATE_EVENT = "DewarContentUpdate" TRACKED_TELL_EVENT_VALUES = { - "Gripper detection": { - "Pin still in Gripper", - "No Pin in Gripper", - "Pin is lost", - }, + "Gripper detection": {"Pin still in Gripper", "No Pin in Gripper", "Pin is lost"} } -TRACKED_MOTION_EVENT_VALUES = { - "Motion Task": {"dry"}, -} +TRACKED_MOTION_EVENT_VALUES = {"Motion Task": {"dry"}} TRACKED_MOTION_SYNC_EVENTS = { "Motion Sync": { "Robot Clear after mount", @@ -60,7 +51,7 @@ TRACKED_MOTION_SYNC_EVENTS = { "Sample get from Gonio", "Sample put on Puck", "Sample get on Puck", - }, + } } TRACKED_STATE_EVENTS = {"state": {"Ready", "Busy"}} @@ -71,16 +62,8 @@ TELL_JOURNAL_PREFIX = "[TELL][JOURNAL]" TELL_EVENTS_REDIS_KEY_SUFFIX = "tell_events" TELL_STATE_REDIS_KEY_SUFFIX = "tell_state" SSE_RECONNECT_DELAY_S = 5 -SSE_RECOVERABLE_EXCEPTIONS = ( - RequestException, - OSError, - ssl.SSLError, -) -WS_RECOVERABLE_EXCEPTIONS = ( - OSError, - ssl.SSLError, - websocket.WebSocketException, -) +SSE_RECOVERABLE_EXCEPTIONS = (RequestException, OSError, ssl.SSLError) +WS_RECOVERABLE_EXCEPTIONS = (OSError, ssl.SSLError, websocket.WebSocketException) class TellSsePayload(BaseModel): @@ -92,13 +75,7 @@ class TellSsePayload(BaseModel): @property def normalized_value(self) -> str | None: - for candidate in ( - self.value, - self.eventValue, - self.data, - self.message, - self.state, - ): + for candidate in (self.value, self.eventValue, self.data, self.message, self.state): if candidate is None: continue stripped = candidate.strip() @@ -113,11 +90,7 @@ class TellEventRecord(BaseModel): event: str def redis_dict(self) -> dict[str, str]: - return { - "timestamp": self.timestamp, - "class": self.class_, - "event": self.event, - } + return {"timestamp": self.timestamp, "class": self.class_, "event": self.event} def _get_redis_context() -> tuple[Any | None, str | None]: @@ -128,9 +101,7 @@ def _get_redis_context() -> tuple[Any | None, str | None]: redis_client = getattr(config, "_BeamlineConfig__client", None) beamline_key = getattr(config, "_BeamlineConfig__bl", None) if redis_client is None or beamline_key is None: - logger.error( - "[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write" - ) + logger.error("[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write") return None, None return cast(Any, redis_client), str(beamline_key) @@ -200,26 +171,16 @@ def extract_tracked_tell_event(event_name, event_data): def set_tell_events_in_redis(events: list[dict[str, str]]) -> None: - _set_json_in_redis( - TELL_EVENTS_REDIS_KEY_SUFFIX, - events, - log_label="TELL events", - ) + _set_json_in_redis(TELL_EVENTS_REDIS_KEY_SUFFIX, events, log_label="TELL events") def set_tell_state_in_redis(state: TellStateModel) -> None: - _set_json_in_redis( - TELL_STATE_REDIS_KEY_SUFFIX, - state.model_dump(), - log_label="TELL state", - ) + _set_json_in_redis(TELL_STATE_REDIS_KEY_SUFFIX, state.model_dump(), log_label="TELL state") def record_tell_event(event_name, event_value): event_record = TellEventRecord( - timestamp=datetime.now(timezone.utc).isoformat(), - class_=event_name, - event=event_value, + timestamp=datetime.now(timezone.utc).isoformat(), class_=event_name, event=event_value ) latest_tell_events[event_name] = event_value @@ -252,9 +213,7 @@ def listen_to_sse(): # mTLS path # ca_root = "/etc/ssl/certs/secrets/mx-db-01_Full_Chain_CA.pem" ca_root = "/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem" - response = requests.get( - sse_url, stream=True, cert=cert_pair, verify=ca_root - ) + response = requests.get(sse_url, stream=True, cert=cert_pair, verify=ca_root) response.raise_for_status() client = sseclient.SSEClient(response) else: @@ -271,9 +230,7 @@ def listen_to_sse(): # print(f"event = {event.event} with data: {event.data}") on_sse_event(event) except SSE_RECOVERABLE_EXCEPTIONS: - logger.exception( - "[SSE] Recoverable failure while consuming TELL event stream" - ) + logger.exception("[SSE] Recoverable failure while consuming TELL event stream") logger.info(f"[SSE] Reconnecting in {SSE_RECONNECT_DELAY_S} seconds") time.sleep(SSE_RECONNECT_DELAY_S) @@ -326,11 +283,7 @@ def on_sse_event(event): logger.debug(f"[EVENT] SSE event={event.event} data={event.data!r}") normalized_data = _normalize_event_data(event.data) - current_tell_state = advance_tell_state( - current_tell_state, - event.event, - normalized_data, - ) + current_tell_state = advance_tell_state(current_tell_state, event.event, normalized_data) set_tell_state_in_redis(current_tell_state) tracked_event = extract_tracked_tell_event(event.event, normalized_data) diff --git a/src/aare/daq/workflows.py b/src/aare/daq/workflows.py index 485b3785..0b9a54ca 100644 --- a/src/aare/daq/workflows.py +++ b/src/aare/daq/workflows.py @@ -13,9 +13,7 @@ logger = setup_logger("aareDAQ") SAFE_POSITION = 600 -def _move_detector_to_safe_position_if_needed( - devs: BeamlineDevices, cfg: BeamlineConfig -): +def _move_detector_to_safe_position_if_needed(devs: BeamlineDevices, cfg: BeamlineConfig): safe_position = cfg.dtz_safe_position current_dtz = devs.dtz @@ -157,9 +155,7 @@ def sa2dc(devs: BeamlineDevices, cfg: BeamlineConfig): start = time.perf_counter() det_z_pos = cfg.dtz status = devs.bec_worker.det_z(value=det_z_pos) - logger.info( - f"start detector move to {cfg.dtz} at {time.perf_counter() - start:.2f}" - ) + logger.info(f"start detector move to {cfg.dtz} at {time.perf_counter() - start:.2f}") devs.samcam_auto(AutoEnum.AUTO) logger.info(f"sam cam auto exposure in {time.perf_counter() - start:.2f}") devs.bec_worker.move_to(BeamlineState.DATA_COLLECTION) @@ -264,9 +260,7 @@ def common2dh(devs: BeamlineDevices, cfg: BeamlineConfig): devs.samcam_auto(AutoEnum.ONCE) if devs.tell.is_position("pPark") and devs.tell.get_mounted_sample() is None: - logger.info( - "TELL already in pPark; skipping dry/park preparation for dewar transfer" - ) + logger.info("TELL already in pPark; skipping dry/park preparation for dewar transfer") return if devs.tell.get_mounted_sample() is not None: diff --git a/src/aare/devices/aerotech.py b/src/aare/devices/aerotech.py index cbd073ff..9209fe69 100644 --- a/src/aare/devices/aerotech.py +++ b/src/aare/devices/aerotech.py @@ -49,10 +49,7 @@ class AerotechController(object): self.__api = DefaultApi(self.__client) def __make_aerotech_target( - self, - coord: AerotechCoordinate, - wait: bool = False, - incremental: bool = False, + self, coord: AerotechCoordinate, wait: bool = False, incremental: bool = False ) -> Target: at_mm = coord.at_mm @@ -96,12 +93,7 @@ class AerotechController(object): def get_position(self) -> AerotechCoordinate: status = self.status() return AerotechCoordinate( - at_mm=Coordinate( - x=status.x.pos, - y=status.y.pos, - z=status.z.pos, - ), - omega_deg=status.u.pos, + at_mm=Coordinate(x=status.x.pos, y=status.y.pos, z=status.z.pos), omega_deg=status.u.pos ) def status(self) -> Status: @@ -162,10 +154,7 @@ class AerotechController(object): return self.__api.home_post() except Exception as e: raise AerotechCommunicationError( - "Aerotech home failed", - endpoint="home_post", - base_url=self.__base, - operation="POST", + "Aerotech home failed", endpoint="home_post", base_url=self.__base, operation="POST" ) from e def wait_till_done(self, timeout=60): @@ -179,19 +168,11 @@ class AerotechController(object): operation="POST", ) from e - def position( - self, - target: AerotechCoordinate, - /, - wait: bool = True, - incremental: bool = False, - ): + def position(self, target: AerotechCoordinate, /, wait: bool = True, incremental: bool = False): if self.__simulated: return self.__pos - payload = self.__make_aerotech_target( - target, wait=wait, incremental=incremental - ) + payload = self.__make_aerotech_target(target, wait=wait, incremental=incremental) try: return self.__api.position_post(payload) except Exception as e: diff --git a/src/aare/devices/area_detector.py b/src/aare/devices/area_detector.py index 6074cfa3..e9c9be0a 100644 --- a/src/aare/devices/area_detector.py +++ b/src/aare/devices/area_detector.py @@ -4,11 +4,13 @@ from enum import Enum import epics import numpy as np + class AutoEnum(Enum): MANUAL = 0 ONCE = 1 AUTO = 2 + @dataclasses.dataclass class AutoExposureSettings: aoi_offset_x: int = 500 @@ -18,6 +20,7 @@ class AutoExposureSettings: gain_max: float = 5.000 exp_max: float = 30000.000 + class epicsAD(object): def __init__(self, prefix, cam="cam1:", image="image1:"): self.img = None @@ -70,8 +73,8 @@ class epicsAD(object): self.gain_max.put(settings.gain_max) self.exp_max.put(settings.exp_max) self.set_auto(AutoEnum.ONCE) - #TODO add a white_balance button to GUI or set it so it happens with backlight in and on - #self.auto_white_balance.put(1) + # TODO add a white_balance button to GUI or set it so it happens with backlight in and on + # self.auto_white_balance.put(1) self.acquire.put(1) def get_image(self, gray=True) -> np.ndarray: @@ -117,4 +120,4 @@ class epicsAD(object): self.acquire.put(0, wait=True) self.gain_mode.put(state.value) # automatic gain control self.expo_mode.put(state.value) # automatic exposure control - self.acquire.put(1) \ No newline at end of file + self.acquire.put(1) diff --git a/src/aare/devices/bec_worker.py b/src/aare/devices/bec_worker.py index c30f36bf..fc2322ab 100644 --- a/src/aare/devices/bec_worker.py +++ b/src/aare/devices/bec_worker.py @@ -83,9 +83,7 @@ class BECClientWorker: service_config.config["log_writer"]["base_path"] = "/tmp/logs" # service_config.config["user_macros"]["base_path"]=f'/sls/{BEAMLINE}/config/bec/production/pxiii_bec/pxiii_bec' # print(service_config.config) - self.client = BECIPythonClient( - config=service_config, mode=OperationMode.Procedure - ) + self.client = BECIPythonClient(config=service_config, mode=OperationMode.Procedure) self.client.start() # self.client.config.update_session_with_file("/sls/x10sa/config/bec/production/bec/bec_lib/bec_lib/config_helper.py") self.dev = self.client.device_manager.devices @@ -164,11 +162,7 @@ class BECClientWorker: exception=exc, ) from exc - raise BECCommunicationError( - message, - operation=operation, - exception=exc, - ) from exc + raise BECCommunicationError(message, operation=operation, exception=exc) from exc def __set_scilog_tags(self, tags: Optional[List[str]] = None): try: @@ -243,9 +237,7 @@ class BECClientWorker: except Exception as e: self._raise_bec_error(e, operation=f"run_macro:{macro_name}") - def run_macro_blocked( - self, macro_name: str, *args, queue: str = "default", **kwargs - ): + def run_macro_blocked(self, macro_name: str, *args, queue: str = "default", **kwargs): if self.simulated: logger.debug(f"Simulating macro {macro_name}") return None @@ -308,9 +300,7 @@ class BECClientWorker: return [] try: self.__list_all_macros() - raw_macros = [ - name for name, _ in self.client.macros._update_handler.macros.items() - ] + raw_macros = [name for name, _ in self.client.macros._update_handler.macros.items()] if raw_macros is None: logger.warning("BEC returned no user macros; treating as empty list") return [] @@ -353,23 +343,18 @@ class BECClientWorker: List of position device names after reinitialisation. """ if self.simulated: - logger.debug( - f"Simulating reinitialise_planner_and_position_devices(method={method})" - ) + logger.debug(f"Simulating reinitialise_planner_and_position_devices(method={method})") return [] try: self.client.config.update_session_with_file( f"/sls/{self.beamline}/config/bec/production/{self._beamline_name}_bec/{self._beamline_name}_bec/device_configs/{self._beamline_name}-devices.yaml" ) self.__init_beamline_environment() - logger.info( - f"Reinitialised BEC planner and position devices using method={method}" - ) + logger.info(f"Reinitialised BEC planner and position devices using method={method}") return self.list_position_devices() except Exception as e: self._raise_bec_error( - e, - operation=f"reinitialise_planner_and_position_devices:{method}", + e, operation=f"reinitialise_planner_and_position_devices:{method}" ) def shutdown_client(self): @@ -379,17 +364,11 @@ class BECClientWorker: try: mono_pitch_scan(plot) except Exception as e: - self._raise_bec_error( - e, operation="mono_pitch_scan", tags=["mono_pitch_scan"] - ) + self._raise_bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"]) if self.beamline is MXBeamline.X06DA: - addtional_text = [ - f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}" - ] + addtional_text = [f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}"] else: - addtional_text = [ - f"New dcm_theta2 position: {self.dev.dccm_theta2.position:5f}" - ] + addtional_text = [f"New dcm_theta2 position: {self.dev.dccm_theta2.position:5f}"] self.scilog_msg( message="Mono pitch scan completed", bold=True, @@ -407,16 +386,13 @@ class BECClientWorker: def change_energy(self, value: float | int, plot: bool = False): current_energy = self.check_current_energy() logger.info(f"Current energy: {current_energy:.1f} eV") - logger.info( - f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV" - ) + logger.info(f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV") try: bl_energy(value, move_gap=False, mono_scan=True, plot=plot) except Exception as e: self._raise_bec_error( e, - operation=f"Requested energy change from:" - f"{current_energy:.1f} to {value} eV", + operation=f"Requested energy change from:{current_energy:.1f} to {value} eV", tags=["energy_change"], ) @@ -462,9 +438,7 @@ class BECClientWorker: status.wait(timeout=timeout) return status except Exception as e: - self._raise_bec_error( - e, operation=f"scans.mv:det_z:{value}", tags=["det_z"] - ) + self._raise_bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"]) def get_det_y(self): try: @@ -480,9 +454,7 @@ class BECClientWorker: status.wait(timeout=timeout) return status except Exception as e: - self._raise_bec_error( - e, operation=f"scans.mv:det_y:{value}", tags=["det_z"] - ) + self._raise_bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"]) @property def backlight_brightness(self) -> BrightnessEnum: @@ -506,9 +478,7 @@ class BECClientWorker: try: self.__backlight_brightness.move(value) except Exception as e: - self._raise_bec_error( - e, operation=f"backlight_brightness:{value}", tags=["backlight"] - ) + self._raise_bec_error(e, operation=f"backlight_brightness:{value}", tags=["backlight"]) raise def get_backlight_pos(self) -> BrightnessEnum: diff --git a/src/aare/devices/enum_pv.py b/src/aare/devices/enum_pv.py index 118fe26f..f8219696 100755 --- a/src/aare/devices/enum_pv.py +++ b/src/aare/devices/enum_pv.py @@ -3,6 +3,7 @@ from typing import Any from aare.devices.set_get_pv import SetGetPV, MoveResult + class EnumPV(SetGetPV): def __init__(self, name: str, setpv: str, getpv: str, **kwargs): super().__init__(name, setpv, getpv, **kwargs) @@ -32,4 +33,4 @@ class EnumPV(SetGetPV): return MoveResult(target=s, name=s) raise ValueError(f"'{x}' not in {list(self.setpoint_pv.enum_strs)}") - raise TypeError(f"Unsupported enum command type: {type(x).__name__}") \ No newline at end of file + raise TypeError(f"Unsupported enum command type: {type(x).__name__}") diff --git a/src/aare/devices/filter_transmission.py b/src/aare/devices/filter_transmission.py index 77db425f..36eb9140 100644 --- a/src/aare/devices/filter_transmission.py +++ b/src/aare/devices/filter_transmission.py @@ -50,6 +50,4 @@ class FilterTransmission: timeisup = time.time() > timeout poll(0.1) if timeisup and self._fail_on_timeout: - raise RuntimeError( - "timeout waiting for filters to achieve requested transmission." - ) + raise RuntimeError("timeout waiting for filters to achieve requested transmission.") diff --git a/src/aare/devices/fluorimeter.py b/src/aare/devices/fluorimeter.py index 9e9e8190..21e1b32f 100644 --- a/src/aare/devices/fluorimeter.py +++ b/src/aare/devices/fluorimeter.py @@ -13,9 +13,7 @@ class Fluorimeter(object): self.__start = PV(f"{BEAMLINE}-ES-SiD:mca1Start") # 0 done ,1 start self.__stop = PV(f"{BEAMLINE}-ES-SiD:mca1Stop") # 0 done ,1 stop - self.__erase_and_start = PV( - f"{BEAMLINE}-ES-SiD:mca1EraseStart" - ) # 0 done,1 start + self.__erase_and_start = PV(f"{BEAMLINE}-ES-SiD:mca1EraseStart") # 0 done,1 start self.__erase = PV(f"{BEAMLINE}-ES-SiD:mca1Erase") # 0 done,1 erase self.__preset_mode = PV(f"{BEAMLINE}-ES-SiD:dxp1:PresetMode") # set mode @@ -25,9 +23,7 @@ class Fluorimeter(object): self.__live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PLTM") # float self.__elapsed_real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ERTM") # float self.__elapsed_live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ELTM") # float - self.__elapsed_trigger_live_time = PV( - f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime" - ) + self.__elapsed_trigger_live_time = PV(f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime") self.__instant_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.IDTIM") self.__average_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.DTIM") diff --git a/src/aare/devices/jfjoch.py b/src/aare/devices/jfjoch.py index 683bbfed..5b5cd05d 100644 --- a/src/aare/devices/jfjoch.py +++ b/src/aare/devices/jfjoch.py @@ -30,9 +30,7 @@ class JFJochWrapper: self.__api = None return - self.__client = jfjoch_client.ApiClient( - jfjoch_client.Configuration(host=self.__url) - ) + self.__client = jfjoch_client.ApiClient(jfjoch_client.Configuration(host=self.__url)) self.__api = jfjoch_client.DefaultApi(self.__client) self.cancel() # if not self.is_idle(): @@ -52,12 +50,7 @@ class JFJochWrapper: return None def _raise_jfjoch_error( - self, - message: str, - *, - error: Exception, - operation: str, - endpoint: str, + self, message: str, *, error: Exception, operation: str, endpoint: str ) -> None: raise JFJochCommunicationError( message, @@ -72,10 +65,7 @@ class JFJochWrapper: self.__api.initialize_post() except Exception as e: self._raise_jfjoch_error( - "JFJoch initialize failed", - error=e, - operation="POST", - endpoint="initialize_post", + "JFJoch initialize failed", error=e, operation="POST", endpoint="initialize_post" ) def cancel(self): @@ -83,10 +73,7 @@ class JFJochWrapper: self.__api.cancel_post() except Exception as e: self._raise_jfjoch_error( - "JFJoch cancel failed", - error=e, - operation="POST", - endpoint="cancel_post", + "JFJoch cancel failed", error=e, operation="POST", endpoint="cancel_post" ) def is_idle(self) -> bool: @@ -213,9 +200,7 @@ class JFJochWrapper: f: FluorescenceSpectrumOutputModel | None = None, async_start: bool = True, ): - dataset_settings = self.__format_dataset_settings( - r, s, f, async_start=async_start - ) + dataset_settings = self.__format_dataset_settings(r, s, f, async_start=async_start) try: self.__api.start_post(dataset_settings=dataset_settings) except Exception as e: @@ -238,18 +223,14 @@ class JFJochWrapper: else: self.__start_scan(ScanTypeEnum.ROTATION, r, s, f, async_start=async_start) - def measure_raster( - self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True - ): + def measure_raster(self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True): self.__start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start) def wait_till_running(self, timeout: int | float = 60): if self.__simulated: return None try: - self.__api.wait_until_running_post_with_http_info( - timeout=math.ceil(timeout) - ) + self.__api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout)) return True except Exception as e: self._raise_jfjoch_error( @@ -259,9 +240,7 @@ class JFJochWrapper: endpoint="wait_until_running_post", ) - def wait_till_done( - self, timeout: int | float - ) -> jfjoch_client.models.ScanResult | None: + def wait_till_done(self, timeout: int | float) -> jfjoch_client.models.ScanResult | None: if self.__simulated: return None try: @@ -385,9 +364,7 @@ if __name__ == "__main__": pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0), aerotech_meas=Coordinate(x=0, y=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0 - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0), omega_deg=10.5, beam_size_mm=Coordinate(x=0.02, y=0.01), ), diff --git a/src/aare/devices/mx_lib.py b/src/aare/devices/mx_lib.py index 79af94b5..09bf2eab 100644 --- a/src/aare/devices/mx_lib.py +++ b/src/aare/devices/mx_lib.py @@ -4,6 +4,7 @@ from typing import Callable, Union, Any from epics import PV, Motor, poll + def wait_for_movement_to_finish(*motors): """ Wait for all {motors} passed in argument to finish movement. @@ -38,8 +39,15 @@ class ValueWaitTimeout(Exception): pass -def pv_wait(pv: PV | Motor, value: Any, *, timeout: float = 60.0, - polling: float = 0.1, tolerance: float | None=None, verbose: bool =False): +def pv_wait( + pv: PV | Motor, + value: Any, + *, + timeout: float = 60.0, + polling: float = 0.1, + tolerance: float | None = None, + verbose: bool = False, +): """wait until an epics.PV reaches a value pv: epics.PV | epics.Motor the PV on which you want to wait on @@ -62,14 +70,16 @@ def pv_wait(pv: PV | Motor, value: Any, *, timeout: float = 60.0, tolerance = m.get("RDBD") if tolerance is None: print(f"WARNING: motor {m._prefix[:-1]} has no RDBD, using 0.001") - tolerance = 0.001 #FIXME + tolerance = 0.001 # FIXME if target is None: target = m.drive diff = abs(target - m.readback) return m.done_moving and diff < tolerance - wait_motor_position(pv, checker, target=value, tolerance=tolerance, timeout=timeout, polling=polling) + wait_motor_position( + pv, checker, target=value, tolerance=tolerance, timeout=timeout, polling=polling + ) elif is_epics_type(pv, "double") or is_epics_type(pv, "time_double"): wait_float_condition(pv, value, tolerance=tolerance, timeout=timeout, polling=polling) @@ -91,7 +101,9 @@ def is_epics_type(pv: PV, pv_type: str) -> bool: return pv_type == pv.type -def wait_string_condition(pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1): +def wait_string_condition( + pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1 +): """wait until an epics.PV of type string reaches target :pv: epics.PV PV should be of type string @@ -116,11 +128,19 @@ def wait_string_condition(pv: PV, target: Union[str, re.Pattern], *, timeout: fl poll(polling) if time.time() > tout: - raise TimeoutError(f"timeout waiting for string {pv.pvname} == {target}; actual value == {pv.char_value}") + raise TimeoutError( + f"timeout waiting for string {pv.pvname} == {target}; actual value == {pv.char_value}" + ) -def wait_float_condition(pv: PV, value:float, *, timeout: float = 60.0, - polling: float = 0.1, tolerance: float| None = None): +def wait_float_condition( + pv: PV, + value: float, + *, + timeout: float = 60.0, + polling: float = 0.1, + tolerance: float | None = None, +): """wait until an epics.PV of type double reaches value pv: epics.PV the PV enum on which you want to wait on @@ -168,7 +188,9 @@ def wait_float_condition(pv: PV, value:float, *, timeout: float = 60.0, raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value}") -def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0, polling: float = 0.1, **kwargs): +def wait_motor_position( + motor: Motor, tester: Callable, *, timeout: float = 50.0, polling: float = 0.1, **kwargs +): """wait until an epics.Motor reaches a condition motor: epics.Motor the motor on which you want to wait on @@ -202,10 +224,14 @@ def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0 poll(polling) if time.time() > tout: - raise TimeoutError(f"timeout waiting for a condition on {motor} {motor.drive} != {motor.readback}") + raise TimeoutError( + f"timeout waiting for a condition on {motor} {motor.drive} != {motor.readback}" + ) -def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1): +def wait_enum_condition( + pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1 +): """wait until an epics.PV enum reaches value pv: epics.PV the PV enum on which you want to wait on @@ -244,12 +270,15 @@ def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout: poll(polling) if time.time() > tout: - raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value}," - f"current value is {pv.get(as_string=True)}") + raise TimeoutError( + f"timeout waiting for enum {pv.pvname} == {value}," + f"current value is {pv.get(as_string=True)}" + ) + def clean_filename(filename: str) -> str: cleaned = re.sub(r"[^A-Za-z0-9._-]", "_", filename.strip()) cleaned = cleaned.strip("._-") if not cleaned: raise ValueError("Filename is empty after sanitization.") - return cleaned \ No newline at end of file + return cleaned diff --git a/src/aare/devices/my_motor.py b/src/aare/devices/my_motor.py index b7ccc018..7878f38c 100644 --- a/src/aare/devices/my_motor.py +++ b/src/aare/devices/my_motor.py @@ -5,18 +5,19 @@ from epics import Motor class MyMotor(Motor): """Wrapper for the EPICS motor PV.""" + def __init__(self, name, timeout=5.0): super().__init__(name.upper(), timeout=timeout) @property def speed(self): """Gets the current motor.slew_speed value""" - return self.get('VELO') + return self.get("VELO") @speed.setter def speed(self, v): """Sets the motor slew speed""" - self.put('VELO', v) + self.put("VELO", v) @property def position(self): @@ -40,7 +41,7 @@ class MyMotor(Motor): @property def moving(self): """Returns True if the motor is moving""" - return bool(self.get('MOVN')) + return bool(self.get("MOVN")) @property def units(self): @@ -50,14 +51,14 @@ class MyMotor(Motor): @property def limits(self): """Returns (low_limit, high_limit)""" - return self.get('HLM'), self.get('LLM') + return self.get("HLM"), self.get("LLM") @limits.setter def limits(self, limits): """Sets (low_limit, high_limit)""" low, high = limits - self.put('LLM', low) - self.put('HLM', high) + self.put("LLM", low) + self.put("HLM", high) def move_motor(self, val, relative=False, wait=False, timeout=300.0): """ @@ -70,12 +71,12 @@ class MyMotor(Motor): return self.move(val, relative=relative, wait=wait, timeout=timeout) - def home(self, direction='forward', wait=False): + def home(self, direction="forward", wait=False): """ Homes the motor. :param direction: 'forward' or 'reverse' """ - field = 'HOMF' if direction == 'forward' else 'HOMR' + field = "HOMF" if direction == "forward" else "HOMR" self.put(field, 1) if wait: self.wait_for_stop() @@ -95,6 +96,7 @@ class MyMotor(Motor): Asynchronous wait until the motor stops moving. """ import asyncio + start_time = time.time() while self.moving: await asyncio.sleep(poll_rate) diff --git a/src/aare/devices/set_get_pv.py b/src/aare/devices/set_get_pv.py index c3784fe2..355ff857 100644 --- a/src/aare/devices/set_get_pv.py +++ b/src/aare/devices/set_get_pv.py @@ -12,6 +12,7 @@ ResolverValue = Union[ tuple[Callable[..., RawValue], tuple[Any, ...]], # (func, args) pattern you already use ] + @dataclass class MoveResult: target: RawValue @@ -19,7 +20,15 @@ class MoveResult: class SetGetPV: - def __init__(self, name: str, setpv: str, getpv: str, *, timeout: float = 60.0, tolerance: float | None = None): + def __init__( + self, + name: str, + setpv: str, + getpv: str, + *, + timeout: float = 60.0, + tolerance: float | None = None, + ): self.name = name self.setpoint_pv = PV(setpv) self.readback_pv = PV(getpv) @@ -46,10 +55,18 @@ class SetGetPV: def wait(self, *, timeout: float | None = None): if self._last_target is None: return - pv_wait(self.readback_pv, self._last_target, timeout=timeout or self.default_timeout, tolerance=self.tolerance) + pv_wait( + self.readback_pv, + self._last_target, + timeout=timeout or self.default_timeout, + tolerance=self.tolerance, + ) + class PredefinedPV(SetGetPV): - def __init__(self, name: str, setpv: str, getpv: str, predefs: Mapping[str, ResolverValue], **kwargs): + def __init__( + self, name: str, setpv: str, getpv: str, predefs: Mapping[str, ResolverValue], **kwargs + ): super().__init__(name, setpv, getpv, **kwargs) self._predefs = dict(predefs) diff --git a/src/aare/devices/smargon.py b/src/aare/devices/smargon.py index 3465c113..3927066a 100644 --- a/src/aare/devices/smargon.py +++ b/src/aare/devices/smargon.py @@ -16,22 +16,16 @@ class SmargonMode(Enum): class Smargon(object): - SMARGON_HOME = SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0 - ) + SMARGON_HOME = SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0) AERO_HOME = AerotechCoordinate(x=0, y=0, z=0, omega=0) def __init__(self, bl: MXBeamline): if bl == MXBeamline.X06DA: self.__simulated = False - self.__base = cfg_get( - "daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000" - ) + self.__base = cfg_get("daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000") elif bl == MXBeamline.X10SA: self.__simulated = False - self.__base = cfg_get( - "daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000" - ) + self.__base = cfg_get("daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000") elif bl == MXBeamline.X06SA: raise NotImplementedError("Not implemented smargon url for X06SA") elif bl == MXBeamline.SIMULATED: @@ -158,8 +152,7 @@ class Smargon(object): acs = self.gonget("readbackAEROTECH") return AerotechCoordinate( - at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), - omega_deg=acs["GMU"], + at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), omega_deg=acs["GMU"] ) @property @@ -199,8 +192,7 @@ class Smargon(object): acs = self.gonget("targetAEROTECH") # targetAEROTECH, #targetOMEGA return AerotechCoordinate( - at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), - omega_deg=acs["GMU"], + at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), omega_deg=acs["GMU"] ) @target_aerotech.setter diff --git a/src/aare/devices/tell_backend.py b/src/aare/devices/tell_backend.py index 4cccfa35..1ce95ff6 100644 --- a/src/aare/devices/tell_backend.py +++ b/src/aare/devices/tell_backend.py @@ -49,9 +49,7 @@ class TellBackend(Protocol): def start_eval(self, expr: str) -> int: ... - def run( - self, path: str, pars: list[str] | None = None, background: bool = False - ) -> None: ... + def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None: ... def abort(self) -> None: ... @@ -88,9 +86,7 @@ class PShellTellBackend: def _resolve_url(bl: MXBeamline) -> str: beamline = bl.value.lower() if bl == MXBeamline.X06DA: - return cfg_get( - "daq.hardware.tell_url", f"http://{beamline}-tell.psi.ch:22222" - ) + return cfg_get("daq.hardware.tell_url", f"http://{beamline}-tell.psi.ch:22222") if bl == MXBeamline.X10SA: return cfg_get("daq.hardware.tell_url", "http://PC17488:22222") if bl == MXBeamline.X06SA: @@ -124,11 +120,7 @@ class PShellTellBackend: if e.response is not None and e.response.text: msg = e.response.text.strip() raise TellCommunicationError( - msg, - base_url=self._url, - endpoint=endpoint, - operation=operation, - critical=True, + msg, base_url=self._url, endpoint=endpoint, operation=operation, critical=True ) from e def eval(self, expr: str): @@ -142,9 +134,7 @@ class PShellTellBackend: def start_eval(self, expr: str) -> int: return self._pshell.start_eval(expr) - def run( - self, path: str, pars: list[str] | None = None, background: bool = False - ) -> None: + def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None: self._pshell.run(path, pars=pars, background=background) def abort(self) -> None: @@ -159,10 +149,7 @@ class SimTellBackend: self._mounted_sample = "" self._settings: dict[str, str] = {"mounted_sample_position": ""} self._results: dict[int, dict[str, Any]] = {} - self._robot_status: dict[str, Any] = { - "powered": True, - "pos": POSITION_PARK, - } + self._robot_status: dict[str, Any] = {"powered": True, "pos": POSITION_PARK} self._current_mA = 30.0 self._pin_offset = 0.0 self._detected_pucks: list[dict[str, Any]] = [] @@ -259,9 +246,7 @@ class SimTellBackend: return str(self._current_mA) if expr.startswith("smart_magnet.set_current("): - match = re.match( - r"smart_magnet\.set_current\(([-+]?\d+(?:\.\d+)?)\)&", expr - ) + match = re.match(r"smart_magnet\.set_current\(([-+]?\d+(?:\.\d+)?)\)&", expr) if match: self._current_mA = float(match.group(1)) return None @@ -322,9 +307,7 @@ class SimTellBackend: self._set_ready_soon() return cmd_id - def run( - self, path: str, pars: list[str] | None = None, background: bool = False - ) -> None: + def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None: _ = background if path == "data/set_samples_info" and pars: @@ -349,9 +332,7 @@ class SimTellBackend: class LazyTellBackend: - def __init__( - self, factory: Callable[[], TellBackend], *, retry_interval_s: float = 2.0 - ): + def __init__(self, factory: Callable[[], TellBackend], *, retry_interval_s: float = 2.0): self._factory = factory self._backend: TellBackend | None = None self._retry_interval_s = float(retry_interval_s) @@ -363,10 +344,7 @@ class LazyTellBackend: return self._backend now = time.monotonic() - if ( - now - self._last_attempt_ts < self._retry_interval_s - and self._last_error is not None - ): + if now - self._last_attempt_ts < self._retry_interval_s and self._last_error is not None: raise self._last_error self._last_attempt_ts = now @@ -378,10 +356,7 @@ class LazyTellBackend: self._last_error = e raise except Exception as e: - wrapped = TellCommunicationError( - "TELL connection failed", - operation="CONNECT", - ) + wrapped = TellCommunicationError("TELL connection failed", operation="CONNECT") self._last_error = wrapped raise wrapped from e @@ -410,9 +385,7 @@ class LazyTellBackend: def start_eval(self, expr: str) -> int: return self._get_backend().start_eval(expr) - def run( - self, path: str, pars: list[str] | None = None, background: bool = False - ) -> None: + def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None: self._get_backend().run(path, pars=pars, background=background) def abort(self) -> None: diff --git a/src/aare/devices/tell_client.py b/src/aare/devices/tell_client.py index c9086bab..89c3fbc8 100755 --- a/src/aare/devices/tell_client.py +++ b/src/aare/devices/tell_client.py @@ -14,11 +14,7 @@ from aarecommon.errors.exception_handler import ( TellConnectionException, ) from aarecommon.models.beamline import MXBeamline -from aarecommon.models.models import ( - DewarAddress, - PuckLoadedInfo, - SampleDewarAddress, -) +from aarecommon.models.models import DewarAddress, PuckLoadedInfo, SampleDewarAddress from aareDB import PuckWithTellPosition from aare.devices.tell_backend import ( @@ -150,8 +146,7 @@ class TellClient: if reasons: raise TellCommunicationError( - "Mount can't start: " + "; ".join(reasons), - operation="mount_precheck", + "Mount can't start: " + "; ".join(reasons), operation="mount_precheck" ) def validate_door_closed(self) -> None: @@ -162,13 +157,11 @@ class TellClient: door_closed = self.is_door_closed() except Exception as e: raise TellCommunicationError( - f"Mount can't start: failed to check door status: {e}", - operation="mount_precheck", + f"Mount can't start: failed to check door status: {e}", operation="mount_precheck" ) if not door_closed: raise TellCommunicationError( - "Mount can't start: TELL doors are open", - operation="mount_precheck", + "Mount can't start: TELL doors are open", operation="mount_precheck" ) def set_samples_info(self, info: List[PuckWithTellPosition]): @@ -351,9 +344,7 @@ class TellClient: logger.info(f"{TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT.value}") return TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT else: - logger.info( - f"Unexpected event: {event} occurred with value: {value}" - ) + logger.info(f"Unexpected event: {event} occurred with value: {value}") logger.info("Checking command completed okay anyway") self.check_command_ok( timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: " @@ -361,9 +352,7 @@ class TellClient: return TellEventValueEnum.UNKNOWN elif wait and segment == "X": logger.info("Loading an auxiliary puck") - self.check_command_ok( - timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: " - ) + self.check_command_ok(timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: ") logger.info("post waiting") return TellEventValueEnum.SUCCESS except TellCommunicationError: @@ -371,8 +360,7 @@ class TellClient: except Exception as e: logger.error(f"Exception occurred: {e}") raise TellCommunicationError( - message=f"Error during mount {segment}{puck}-{sample}: {e}", - critical=True, + message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True ) def unmount(self, force=False, wait=False, timeout=360.0): @@ -476,10 +464,9 @@ class TellClient: PuckLoadedInfo( puck_name=i["puckBarcode"], location=DewarAddress( - segment=puck_address[0], - pos=int(puck_address[1]), + segment=puck_address[0], pos=int(puck_address[1]) ), - ), + ) ) return output @@ -543,18 +530,14 @@ class TellClient: self.backend.eval("smart_magnet.set_supress(True)&") self.backend.eval("smart_magnet.state&") if self.get_mounted_sample() is None: - logger.warning( - "Check mount: A manually mounted sample is detected." - ) + logger.warning("Check mount: A manually mounted sample is detected.") logger.warning("Remove before mounting with the robot.") raise ManualMountException return True elif state == "Ready": logger.debug("No sample detected, ready to mount") if self.get_mounted_sample(): - logger.error( - "Check mount: No sample detected, but robot thinks is mounted" - ) + logger.error("Check mount: No sample detected, but robot thinks is mounted") raise SmartMagnetFaultException return False elif state == "Paused": @@ -573,10 +556,7 @@ def make_tell_client(bl: MXBeamline) -> TellClient: if bl == MXBeamline.SIMULATED: backend = SimTellBackend() else: - backend = LazyTellBackend( - factory=lambda: PShellTellBackend(bl), - retry_interval_s=2.0, - ) + backend = LazyTellBackend(factory=lambda: PShellTellBackend(bl), retry_interval_s=2.0) return TellClient(bl, backend=backend) diff --git a/src/aare/devices/workflow_tools.py b/src/aare/devices/workflow_tools.py index c4ae57e9..bf3352ac 100755 --- a/src/aare/devices/workflow_tools.py +++ b/src/aare/devices/workflow_tools.py @@ -14,7 +14,9 @@ def wait_position(motor, target, tolerance=None, timeout=60.0): elif isinstance(position, (bytes, str)): tst = "'%s' == '%s'" if isinstance(position, bytes): - ltst = lambda x, y, z: x.lower() == y.lower().encode() if isinstance(y, str) else x.lower() == y.lower() + ltst = lambda x, y, z: ( + x.lower() == y.lower().encode() if isinstance(y, str) else x.lower() == y.lower() + ) else: ltst = lambda x, y, z: x.lower() == y.lower() else: @@ -29,7 +31,10 @@ def wait_position(motor, target, tolerance=None, timeout=60.0): n = n + 1 if n > 20: n = 0 - print("waiting_position test: %s (%s, %s, %s)" % (tst, str(motor.readback), str(target), str(tolerance))) + print( + "waiting_position test: %s (%s, %s, %s)" + % (tst, str(motor.readback), str(target), str(tolerance)) + ) timeisup = timeout < time.time() condition = ltst(motor.readback, target, tolerance) diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 7355a9a1..5b7a9522 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -12,9 +12,7 @@ logger = setup_logger("aareGUI") def auth(base_url: str | None, cert_path: str | None) -> str: curr_user = get_user() if base_url is None: - token_data = TokenData( - sub=curr_user, staff=True, session=15, pgroups=["p16371", "p22233"] - ) + token_data = TokenData(sub=curr_user, staff=True, session=15, pgroups=["p16371", "p22233"]) return jwt.encode(token_data.model_dump(), "ABC123") # Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server. diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index ac5189be..7274c6d2 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -47,12 +47,8 @@ def main(): # TODO if zmq and pred stream come from same source, do not need images from both streams, can combine match mx_beamline(): case MXBeamline.X06DA: - default_url = cfg_get( - "gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch" - ) - default_cert_path = cfg_get( - "gui.daq.cert_path", "/sls/x06da/misc/.cert/6d.crt" - ) + default_url = cfg_get("gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06da/misc/.cert/6d.crt") default_zmq_addr = cfg_get( "gui.cameras.sample_camera_zmq_url", "tcp://x06da-pserv-01:9089" ) @@ -67,12 +63,8 @@ def main(): ) default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 3)) case MXBeamline.X10SA: - default_url = cfg_get( - "gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch" - ) - default_cert_path = cfg_get( - "gui.daq.cert_path", "/sls/x10sa/misc/.cert/10s.crt" - ) + default_url = cfg_get("gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x10sa/misc/.cert/10s.crt") default_zmq_addr = cfg_get( "gui.cameras.sample_camera_zmq_url", "tcp://x10sa-spark-01:9091" ) @@ -87,17 +79,11 @@ def main(): ) default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) case MXBeamline.X06SA: - default_url = cfg_get( - "gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch" - ) - default_cert_path = cfg_get( - "gui.daq.cert_path", "/sls/x06sa/misc/.cert/6s.crt" - ) + default_url = cfg_get("gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch") + default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06sa/misc/.cert/6s.crt") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "") - default_beamline_cam_addr = cfg_get( - "gui.cameras.beamline_camera_url", "" - ) + default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "") default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "") default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) case _: @@ -105,16 +91,12 @@ def main(): default_cert_path = cfg_get("gui.daq.cert_path", "") default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "") default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "") - default_beamline_cam_addr = cfg_get( - "gui.cameras.beamline_camera_url", "" - ) + default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "") default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "") default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1)) # Add custom options as needed - urlOption = QCommandLineOption( - ["u", "aaredaq-url"], "Base AareDAQ URL", "url", default_url - ) + urlOption = QCommandLineOption(["u", "aaredaq-url"], "Base AareDAQ URL", "url", default_url) parser.addOption(urlOption) certPath = QCommandLineOption( @@ -126,9 +108,7 @@ def main(): parser.addOption(certPath) defaultImage = QCommandLineOption( - ["i", "image"], - "Default image to display in absence of the ZMQ stream", - "image", + ["i", "image"], "Default image to display in absence of the ZMQ stream", "image" ) parser.addOption(defaultImage) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index fb37068a..a16cf3bd 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -232,10 +232,7 @@ class MainWindow(QMainWindow): self.left_column_layout.setSpacing(8) self.data_collection = DataCollectionSettings( - s=geom, - parent=self.left_column, - raster_mgr=self.raster, - diffraction=diffraction, + s=geom, parent=self.left_column, raster_mgr=self.raster, diffraction=diffraction ) self.loop_centering = LoopCenteringPanel(parent=self.left_column) @@ -271,10 +268,7 @@ class MainWindow(QMainWindow): self.video_tab = QTabWidget(parent=top_widget) self.sample_camera = SampleCameraImageLabel( - geom=geom, - raster=self.raster, - parent=top_widget, - default_image=default_image, + geom=geom, raster=self.raster, parent=top_widget, default_image=default_image ) self.beamline_view = VideoGraphicsView() @@ -284,9 +278,7 @@ class MainWindow(QMainWindow): self.beamline_view_panel.refresh_requested.connect(self.refresh_axis_cameras) self.gonio_view = VideoGraphicsView() - self.gonio_view_panel = AxisVideoPanel( - "Gonio camera", self.gonio_view, parent=top_widget - ) + self.gonio_view_panel = AxisVideoPanel("Gonio camera", self.gonio_view, parent=top_widget) self.gonio_view_panel.refresh_requested.connect(self.refresh_axis_cameras) self.beamline_view_container = QWidget(parent=top_widget) @@ -300,13 +292,9 @@ class MainWindow(QMainWindow): self.beamline_view_layout.addWidget(self.beamline_view_2_combined) self.beamline_combined_panel = AxisVideoPanel( - "Beamline combined view", - self.beamline_view_container, - parent=top_widget, - ) - self.beamline_combined_panel.refresh_requested.connect( - self.refresh_axis_cameras + "Beamline combined view", self.beamline_view_container, parent=top_widget ) + self.beamline_combined_panel.refresh_requested.connect(self.refresh_axis_cameras) self.video_tab.addTab(self.sample_camera, "Sample camera") self.video_tab.addTab(self.gonio_view_panel, "Gonio camera") @@ -321,10 +309,7 @@ class MainWindow(QMainWindow): # self.video_tab.addTab(self.secondary_beamline_view_panel, "Secondary view") self.compact_sample_camera = SampleCameraImageLabel( - geom=geom, - raster=self.raster, - parent=root_widget, - default_image=default_image, + geom=geom, raster=self.raster, parent=root_widget, default_image=default_image ) self.compact_automation_panel = CompactAutomationPanel( self.compact_sample_camera, parent=root_widget @@ -339,14 +324,10 @@ class MainWindow(QMainWindow): # ── Portrait mode page ────────────────────────────────────────── self.portrait_sample_camera = SampleCameraImageLabel( - geom=geom, - raster=self.raster, - parent=root_widget, - default_image=default_image, + geom=geom, raster=self.raster, parent=root_widget, default_image=default_image ) self.portrait_mode_panel = PortraitModePanel( - sample_camera_widget=self.portrait_sample_camera, - parent=root_widget, + sample_camera_widget=self.portrait_sample_camera, parent=root_widget ) self.portrait_mode_page = QWidget(parent=root_widget) @@ -354,12 +335,9 @@ class MainWindow(QMainWindow): portrait_page_layout = QHBoxLayout(self.portrait_mode_page) portrait_page_layout.setContentsMargins(0, 0, 0, 0) portrait_page_layout.setSpacing(0) - self.portrait_mode_page.setFixedWidth( - self.portrait_mode_panel.PORTRAIT_WIDTH + 24 - ) + self.portrait_mode_page.setFixedWidth(self.portrait_mode_panel.PORTRAIT_WIDTH + 24) portrait_page_layout.addWidget( - self.portrait_mode_panel, - alignment=Qt.AlignmentFlag.AlignHCenter, + self.portrait_mode_panel, alignment=Qt.AlignmentFlag.AlignHCenter ) top_widget_layout.addWidget(self.video_tab) @@ -381,29 +359,21 @@ class MainWindow(QMainWindow): self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[])) self.job_list_panel = SampleQueuePanel(show_user=self.__decoded_token.staff) - self.compact_automation_panel.play_pause_clicked.connect( - self.job_list_panel.run - ) - self.compact_automation_panel.skip_clicked.connect( - self.job_list_panel.skip_current_sample - ) + self.compact_automation_panel.play_pause_clicked.connect(self.job_list_panel.run) + self.compact_automation_panel.skip_clicked.connect(self.job_list_panel.skip_current_sample) self.compact_automation_panel.step_through_toggled.connect( self.job_list_panel.set_step_through ) self.compact_automation_panel.show_full_view_requested.connect( self._return_from_compact_automation_view ) - self.compact_automation_panel.annotation_selected.connect( - self._handle_compact_annotation - ) + self.compact_automation_panel.annotation_selected.connect(self._handle_compact_annotation) self.tell_samples_dock = QDockWidget("Sample List", self) self.tell_samples_dock.setObjectName("tell_samples_dock") self.tell_samples_dock.setWidget(self.tell_samples) self.tell_samples_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) - self.addDockWidget( - Qt.DockWidgetArea.BottomDockWidgetArea, self.tell_samples_dock - ) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.tell_samples_dock) self.ref_tools_dock = QDockWidget("Reference Tools", self) self.ref_tools_dock.setObjectName("ref_tools_dock") @@ -430,20 +400,14 @@ class MainWindow(QMainWindow): self.manual_sample_dock.setObjectName("manual_sample_dock") self.manual_sample_dock.setWidget(self.manual_sample_panel) self.manual_sample_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) - self.addDockWidget( - Qt.DockWidgetArea.BottomDockWidgetArea, self.manual_sample_dock - ) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.manual_sample_dock) self.automation_progress_panel = AutomationProgressWidget() self.automation_progress_dock = QDockWidget("Automation progress", self) self.automation_progress_dock.setObjectName("automation_progress_dock") self.automation_progress_dock.setWidget(self.automation_progress_panel) - self.automation_progress_dock.setAllowedAreas( - Qt.DockWidgetArea.BottomDockWidgetArea - ) - self.addDockWidget( - Qt.DockWidgetArea.BottomDockWidgetArea, self.automation_progress_dock - ) + self.automation_progress_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.automation_progress_dock) self.face_panel = FaceDetectionPanel() self.face_panel_dock = QDockWidget("Face detection", self) @@ -464,9 +428,7 @@ class MainWindow(QMainWindow): | Qt.DockWidgetArea.BottomDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea ) - self.addDockWidget( - Qt.DockWidgetArea.BottomDockWidgetArea, self.fluor_panel_dock - ) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.fluor_panel_dock) self.fluor_panel_dock.hide() self.log_dock = LogDock("Console Log", self) @@ -487,9 +449,7 @@ class MainWindow(QMainWindow): self.job_list_panel.automation_running_changed.connect( self.automation_progress_panel.set_running ) - self.job_list_panel.automation_running_changed.connect( - self._on_automation_running_changed - ) + self.job_list_panel.automation_running_changed.connect(self._on_automation_running_changed) self.job_list_panel.automation_running_changed.connect( self.compact_automation_panel.set_running ) @@ -499,28 +459,20 @@ class MainWindow(QMainWindow): self.job_list_panel.samples_in_queue_changed.connect( self.compact_automation_panel.set_samples_in_queue ) - self.job_list_panel.samples_in_queue_changed.connect( - self._refresh_compact_queue_preview - ) + self.job_list_panel.samples_in_queue_changed.connect(self._refresh_compact_queue_preview) # Portrait mode: queue size + running state + preview - self.job_list_panel.automation_running_changed.connect( - self.portrait_mode_panel.set_running - ) + self.job_list_panel.automation_running_changed.connect(self.portrait_mode_panel.set_running) self.job_list_panel.samples_in_queue_changed.connect( self.portrait_mode_panel.set_samples_in_queue ) - self.job_list_panel.samples_in_queue_changed.connect( - self._refresh_portrait_queue_preview - ) + self.job_list_panel.samples_in_queue_changed.connect(self._refresh_portrait_queue_preview) self.automation_progress_panel.set_samples_in_queue( len(self.job_list_panel.table_model.samples) ) self.automation_progress_panel.set_running(self.job_list_panel.is_running()) self.compact_automation_panel.set_running(self.job_list_panel.is_running()) - self.compact_automation_panel.set_step_through( - self.job_list_panel.is_step_through() - ) + self.compact_automation_panel.set_step_through(self.job_list_panel.is_step_through()) self.compact_automation_panel.set_samples_in_queue( len(self.job_list_panel.table_model.samples) ) @@ -536,9 +488,7 @@ class MainWindow(QMainWindow): | Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea ) - self.addDockWidget( - Qt.DockWidgetArea.RightDockWidgetArea, self.smargon_trace_dock - ) + self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.smargon_trace_dock) self.smargon_trace_dock.hide() # Target stability panel @@ -551,9 +501,7 @@ class MainWindow(QMainWindow): | Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea ) - self.addDockWidget( - Qt.DockWidgetArea.RightDockWidgetArea, self.target_stability_dock - ) + self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.target_stability_dock) self.target_stability_dock.hide() # Prediction Metrics Panel @@ -566,9 +514,7 @@ class MainWindow(QMainWindow): | Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea ) - self.addDockWidget( - Qt.DockWidgetArea.RightDockWidgetArea, self.prediction_metrics_dock - ) + self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.prediction_metrics_dock) self.prediction_metrics_dock.hide() self.content_stack.addWidget(top_widget) @@ -617,22 +563,15 @@ class MainWindow(QMainWindow): self._remote_close_timer.timeout.connect(self._check_remote_close_deadline) self._axis_camera_refresh_timer = QTimer(self) - self._axis_camera_refresh_timer.setInterval( - self._axis_camera_refresh_interval_ms - ) + self._axis_camera_refresh_timer.setInterval(self._axis_camera_refresh_interval_ms) self._axis_camera_refresh_timer.timeout.connect(self.refresh_axis_cameras) self._axis_camera_refresh_timer.start() self.portrait_mode_panel.wire_to_main_window( - job_list_panel=self.job_list_panel, - tell_samples=self.tell_samples, - ) - self.portrait_mode_panel._back_btn.clicked.connect( - self._return_from_portrait_mode - ) - self.portrait_mode_panel.grab_session_requested.connect( - self.status_bar.request_baton + job_list_panel=self.job_list_panel, tell_samples=self.tell_samples ) + self.portrait_mode_panel._back_btn.clicked.connect(self._return_from_portrait_mode) + self.portrait_mode_panel.grab_session_requested.connect(self.status_bar.request_baton) # Route alert banner signals through portrait-aware interceptors self.daq.polled_devices_status.connect(self._portrait_alert_primary) @@ -643,9 +582,7 @@ class MainWindow(QMainWindow): self.daq.baton_request_result.connect(self._on_baton_request_result) self.daq.baton_response_result.connect(self._on_baton_response_result) self.daq.baton_timeout_checked.connect(self._on_baton_timeout_checked) - self.daq.automation_progress.connect( - self.automation_progress_panel.set_progress - ) + self.daq.automation_progress.connect(self.automation_progress_panel.set_progress) self.daq.automation_progress.connect(self.compact_automation_panel.set_progress) self.daq.automation_progress.connect(self.portrait_mode_panel.set_progress) @@ -681,12 +618,8 @@ class MainWindow(QMainWindow): self.beamline.illumination_panel.back_light.connect(self.daq.back_light) if self.__decoded_token.staff: - self.beamline.monochromator_panel.mono_pitch_scan.connect( - self.daq.mono_pitch_scan - ) - self.beamline.monochromator_panel.change_energy.connect( - self.daq.change_energy - ) + self.beamline.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan) + self.beamline.monochromator_panel.change_energy.connect(self.daq.change_energy) self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak) self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save) self.beamline.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas) @@ -697,12 +630,8 @@ class MainWindow(QMainWindow): self.beamline.beam_size.beam_size.connect(self.daq.beam_size_mm) self.sample_camera.load_image.connect(self.raster.load_image) - self.sample_camera.switch_raster_grid.connect( - self.data_collection.switch_to_raster - ) - self.beamline.samcam.show_detections_changed.connect( - self.sample_camera.set_show_detections - ) + self.sample_camera.switch_raster_grid.connect(self.data_collection.switch_to_raster) + self.beamline.samcam.show_detections_changed.connect(self.sample_camera.set_show_detections) self.beamline.samcam.show_detection_polygons_changed.connect( self.sample_camera.set_show_detection_polygons ) @@ -718,40 +647,24 @@ class MainWindow(QMainWindow): self.beamline.samcam.compact_overlay_legend_changed.connect( self.sample_camera.set_compact_overlay_legend ) - self.beamline.samcam.target_color_changed.connect( - self.sample_camera.set_target_color - ) + self.beamline.samcam.target_color_changed.connect(self.sample_camera.set_target_color) self._restore_samcam_overlay_settings() sample_feed_addr = pred_zmq_addr or zmq_addr if sample_feed_addr is not None: logger.debug(f"Starting prediction subscriber thread {sample_feed_addr}") - self.prediction_thread = PredictionSubscriber( - pred_zmq_url=sample_feed_addr, topic=b"" - ) + self.prediction_thread = PredictionSubscriber(pred_zmq_url=sample_feed_addr, topic=b"") self.prediction_thread.image.connect(self.sample_camera.update_pixmap) - self.prediction_thread.image.connect( - self.compact_sample_camera.update_pixmap - ) - self.prediction_thread.image.connect( - self.portrait_sample_camera.update_pixmap - ) - self.prediction_thread.prediction.connect( - self.sample_camera.update_detections - ) - self.prediction_thread.prediction.connect( - self.compact_sample_camera.update_detections - ) - self.prediction_thread.prediction.connect( - self.portrait_sample_camera.update_detections - ) + self.prediction_thread.image.connect(self.compact_sample_camera.update_pixmap) + self.prediction_thread.image.connect(self.portrait_sample_camera.update_pixmap) + self.prediction_thread.prediction.connect(self.sample_camera.update_detections) + self.prediction_thread.prediction.connect(self.compact_sample_camera.update_detections) + self.prediction_thread.prediction.connect(self.portrait_sample_camera.update_detections) self.prediction_thread.prediction.connect( self.prediction_metrics_panel.update_from_prediction ) - self.prediction_thread.target_point.connect( - self.sample_camera.update_target_point - ) + self.prediction_thread.target_point.connect(self.sample_camera.update_target_point) self.prediction_thread.target_point.connect( self.compact_sample_camera.update_target_point ) @@ -761,12 +674,8 @@ class MainWindow(QMainWindow): self.prediction_thread.target_point.connect( self.target_stability_panel.update_target_point ) - self.prediction_thread.focus_measure.connect( - self.status_bar.update_sharpness - ) - self.prediction_thread.fps_measure.connect( - self.status_bar.update_samcam_fps - ) + self.prediction_thread.focus_measure.connect(self.status_bar.update_sharpness) + self.prediction_thread.fps_measure.connect(self.status_bar.update_samcam_fps) self.prediction_thread.camera_availability_changed.connect( self.sample_camera.set_camera_available ) @@ -786,9 +695,7 @@ class MainWindow(QMainWindow): self.sample_camera.set_camera_available(False) self.compact_sample_camera.set_camera_available(False) self.portrait_sample_camera.set_camera_available(False) - self._show_samcam_feed_banner( - "Sample camera feed unavailable: no stream configured" - ) + self._show_samcam_feed_banner("Sample camera feed unavailable: no stream configured") # # self.data_collection.helical.helical_scan.connect(self.worker.helical_scan) @@ -801,13 +708,9 @@ class MainWindow(QMainWindow): self.sample_camera.evaluate_grid.connect(self.raster.run_grid_scan) self.data_collection.raster.evaluate_grid.connect(self.raster.run_grid_scan) - self.data_collection.raster.evaluate_grid_auto.connect( - self.raster.run_grid_scan_auto - ) + self.data_collection.raster.evaluate_grid_auto.connect(self.raster.run_grid_scan_auto) - self.sample_camera.clear_evaluated_grids.connect( - self.raster.clear_completed_grids - ) + self.sample_camera.clear_evaluated_grids.connect(self.raster.clear_completed_grids) self.sample_camera.clear_grid.connect(self.raster.clear_active_grid) self.daq.run_number_incremented.connect( self.data_collection.file_path_panel.increment_run_number @@ -825,28 +728,18 @@ class MainWindow(QMainWindow): self.ref_tools_panel.mount.connect(self._on_manual_mount_requested) self.ref_tools_panel.unmount.connect(self._on_manual_unmount_requested) - self.data_collection.raster.grid_size_updated.connect( - self.raster.update_grid_size - ) - self.data_collection.raster.exp_time_updated.connect( - self.raster.update_exposure_time - ) - self.data_collection.raster.transmission_updated.connect( - self.raster.update_transmission - ) + self.data_collection.raster.grid_size_updated.connect(self.raster.update_grid_size) + self.data_collection.raster.exp_time_updated.connect(self.raster.update_exposure_time) + self.data_collection.raster.transmission_updated.connect(self.raster.update_transmission) self.data_collection.raster.dtz_updated.connect(self.raster.update_dtz) self.data_collection.raster.grid_metric_updated.connect(self.raster.metric) - self.data_collection.raster.raster_alpha_changed.connect( - self.sample_camera.raster_alpha - ) + self.data_collection.raster.raster_alpha_changed.connect(self.sample_camera.raster_alpha) self.data_collection.cancel.connect(self.daq.cancel) self.raster.grid_scan.connect(self.daq.raster_scan) self.raster.grid_scan_auto.connect(self.daq.raster_scan_auto) self.data_collection.screening.rotation_scan.connect(self.daq.standard_scan) - self.data_collection.simple.rotation_scan.connect( - self._on_simple_rotation_requested - ) + self.data_collection.simple.rotation_scan.connect(self._on_simple_rotation_requested) self.data_collection.simple.parameters_changed.connect(self.daq.smart_params) self.raster.grid_scan_size_changed.connect( @@ -879,28 +772,20 @@ class MainWindow(QMainWindow): self.beamline_state_panel.sample_exchange.connect(self.daq.sample_exchange) self.beamline_state_panel.sample_alignment.connect(self.daq.sample_alignment) self.beamline_state_panel.beam_location.connect(self.daq.beam_location) - self.beamline_state_panel.beamstop_alignment.connect( - self.daq.beamstop_alignment - ) + self.beamline_state_panel.beamstop_alignment.connect(self.daq.beamstop_alignment) self.beamline_state_panel.flux_measurement.connect(self.daq.flux_measurement) self.beamline_state_panel.data_collection.connect(self.daq.data_collection) self.beamline_state_panel.xtal_snapshot.connect(self.daq.xtal_snapshot) self.beamline_state_panel.xray_fluorescence.connect(self.daq.xray_fluorescence) - self.beamline_state_panel.robot_sample_exchange.connect( - self.daq.robot_sample_exchange - ) + self.beamline_state_panel.robot_sample_exchange.connect(self.daq.robot_sample_exchange) self.rotation.file_ready.connect(self.viewer.load_image) self.raster.image_selected.connect(self.viewer.load_image) self.raster.viewer_track_online.connect(self.viewer.load_online) - self.data_collection.screening.viewer_track_online.connect( - self.viewer.load_online - ) + self.data_collection.screening.viewer_track_online.connect(self.viewer.load_online) self.job_list_panel.viewer_track_online.connect(self.viewer.load_online) - self.sample_logic.sample_changed.connect( - self.data_collection.file_path_panel.update_sample - ) + self.sample_logic.sample_changed.connect(self.data_collection.file_path_panel.update_sample) self.daq.update.connect(self.beamline.omega_panel.update_daq_status) self.daq.update.connect(self.beamline.smargon_panel.update_daq_status) @@ -945,9 +830,7 @@ class MainWindow(QMainWindow): self.daq.raster_scan_completed.connect(self.raster.grid_scan_completed) self.daq.automated_scan_done.connect(self.job_list_panel.automated_scan_done) - self.daq.automation_critical_failure.connect( - self._on_automation_critical_failure - ) + self.daq.automation_critical_failure.connect(self._on_automation_critical_failure) self.daq.manual_collection_critical_failure.connect( self._on_manual_collection_critical_failure ) @@ -964,9 +847,7 @@ class MainWindow(QMainWindow): self.data_collection.fluo.fluo_scan.connect(self._on_fluo_scan_requested) self.daq.fluorimeter_spectrum_update.connect(self.fluor_panel.update_plot) - self.daq.fluorimeter_spectrum_update.connect( - lambda: self.fluor_panel_dock.setVisible(True) - ) + self.daq.fluorimeter_spectrum_update.connect(lambda: self.fluor_panel_dock.setVisible(True)) # === Alert/Status Message Routing === # Status bar: General status messages (not device connection status) @@ -978,26 +859,18 @@ class MainWindow(QMainWindow): self._shortcut_manual_sample = QAction("Raise Manual Sample Dock", self) self._shortcut_manual_sample.setShortcut(QKeySequence("Ctrl+M")) self._shortcut_manual_sample.triggered.connect( - lambda: ( - self.manual_sample_dock.setVisible(True), - self.manual_sample_dock.raise_(), - ) + lambda: (self.manual_sample_dock.setVisible(True), self.manual_sample_dock.raise_()) ) self.addAction(self._shortcut_manual_sample) self._shortcut_raise_sample_list = QAction("Raise sample list", self) self._shortcut_raise_sample_list.setShortcut(QKeySequence("Ctrl+L")) self._shortcut_raise_sample_list.triggered.connect( - lambda: ( - self.tell_samples_dock.setVisible(True), - self.tell_samples_dock.raise_(), - ) + lambda: (self.tell_samples_dock.setVisible(True), self.tell_samples_dock.raise_()) ) self.addAction(self._shortcut_raise_sample_list) - self._shortcut_raise_reference_tools_list = QAction( - "Raise reference tools", self - ) + self._shortcut_raise_reference_tools_list = QAction("Raise reference tools", self) self._shortcut_raise_reference_tools_list.setShortcut(QKeySequence("Ctrl+R")) self._shortcut_raise_reference_tools_list.triggered.connect( lambda: (self.ref_tools_dock.setVisible(True), self.ref_tools_dock.raise_()) @@ -1011,9 +884,7 @@ class MainWindow(QMainWindow): ) self.addAction(self._shortcut_raise_job_list) - self._shortcut_toggle_target_stability = QAction( - "Toggle target stability panel", self - ) + self._shortcut_toggle_target_stability = QAction("Toggle target stability panel", self) self._shortcut_toggle_target_stability.setShortcut(QKeySequence("Ctrl+Shift+T")) self._shortcut_toggle_target_stability.triggered.connect( lambda: self.target_stability_dock.setVisible( @@ -1022,12 +893,8 @@ class MainWindow(QMainWindow): ) self.addAction(self._shortcut_toggle_target_stability) - self._shortcut_toggle_prediction_metrics = QAction( - "Toggle prediction metrics panel", self - ) - self._shortcut_toggle_prediction_metrics.setShortcut( - QKeySequence("Ctrl+Shift+P") - ) + self._shortcut_toggle_prediction_metrics = QAction("Toggle prediction metrics panel", self) + self._shortcut_toggle_prediction_metrics.setShortcut(QKeySequence("Ctrl+Shift+P")) self._shortcut_toggle_prediction_metrics.triggered.connect( lambda: self.prediction_metrics_dock.setVisible( not self.prediction_metrics_dock.isVisible() @@ -1035,14 +902,10 @@ class MainWindow(QMainWindow): ) self.addAction(self._shortcut_toggle_prediction_metrics) - self._shortcut_toggle_smargon_trace = QAction( - "Toggle smargon trace panel", self - ) + self._shortcut_toggle_smargon_trace = QAction("Toggle smargon trace panel", self) self._shortcut_toggle_smargon_trace.setShortcut(QKeySequence("Ctrl+Shift+S")) self._shortcut_toggle_smargon_trace.triggered.connect( - lambda: self.smargon_trace_dock.setVisible( - not self.smargon_trace_dock.isVisible() - ) + lambda: self.smargon_trace_dock.setVisible(not self.smargon_trace_dock.isVisible()) ) self.addAction(self._shortcut_toggle_smargon_trace) @@ -1060,10 +923,7 @@ class MainWindow(QMainWindow): current_widget = self.content_stack.currentWidget() - if ( - hasattr(self, "portrait_mode_page") - and current_widget is self.portrait_mode_page - ): + if hasattr(self, "portrait_mode_page") and current_widget is self.portrait_mode_page: self._return_from_portrait_mode() elif bool(getattr(self, "_in_compact_automation_view", False)): self._return_from_compact_automation_view() @@ -1076,19 +936,11 @@ class MainWindow(QMainWindow): def _restore_samcam_overlay_settings(self) -> None: settings = QSettings("PSI", "AareGUI") show_detections = settings.value("samcam/show_detections", True, type=bool) - show_detection_polygons = settings.value( - "samcam/show_detection_polygons", True, type=bool - ) + show_detection_polygons = settings.value("samcam/show_detection_polygons", True, type=bool) show_target_point = settings.value("samcam/show_target_point", True, type=bool) - show_target_coordinates = settings.value( - "samcam/show_target_coordinates", True, type=bool - ) - show_overlay_legend = settings.value( - "samcam/show_overlay_legend", True, type=bool - ) - compact_overlay_legend = settings.value( - "samcam/compact_overlay_legend", False, type=bool - ) + show_target_coordinates = settings.value("samcam/show_target_coordinates", True, type=bool) + show_overlay_legend = settings.value("samcam/show_overlay_legend", True, type=bool) + compact_overlay_legend = settings.value("samcam/compact_overlay_legend", False, type=bool) target_color = settings.value("samcam/target_color", "Cyan", type=str) self.beamline.samcam.apply_overlay_settings( @@ -1116,13 +968,9 @@ class MainWindow(QMainWindow): "samcam/show_detection_polygons", overlay["show_detection_polygons"] ) # NEW settings.setValue("samcam/show_target_point", overlay["show_target_point"]) - settings.setValue( - "samcam/show_target_coordinates", overlay["show_target_coordinates"] - ) + settings.setValue("samcam/show_target_coordinates", overlay["show_target_coordinates"]) settings.setValue("samcam/show_overlay_legend", overlay["show_overlay_legend"]) - settings.setValue( - "samcam/compact_overlay_legend", overlay["compact_overlay_legend"] - ) + settings.setValue("samcam/compact_overlay_legend", overlay["compact_overlay_legend"]) settings.setValue("samcam/target_color", overlay["target_color"]) @Slot(bool) @@ -1141,10 +989,7 @@ class MainWindow(QMainWindow): def _show_samcam_feed_banner(self, message: str) -> None: self.__samcam_feed_banner_message = message self._show_runtime_notification( - title="Sample camera", - message=message, - level="warning", - sticky=True, + title="Sample camera", message=message, level="warning", sticky=True ) self.__samcam_feed_banner_active = True @@ -1189,9 +1034,7 @@ class MainWindow(QMainWindow): if self._beamline_cam_addr: self.beamline_camera_thread = VideoThread(ip=self._beamline_cam_addr) - self.beamline_camera_thread.frame_ready.connect( - self.beamline_view.update_frame - ) + self.beamline_camera_thread.frame_ready.connect(self.beamline_view.update_frame) self.beamline_camera_thread.frame_ready.connect( self.beamline_view_2_combined.update_frame ) @@ -1202,9 +1045,7 @@ class MainWindow(QMainWindow): ip=self._gonio_cam_addr, camera=self._gonio_cam_id ) self.gonio_camera_thread.frame_ready.connect(self.gonio_view.update_frame) - self.gonio_camera_thread.frame_ready.connect( - self.beamline_view_1_combined.update_frame - ) + self.gonio_camera_thread.frame_ready.connect(self.beamline_view_1_combined.update_frame) self.gonio_camera_thread.start() def _all_tell_samples_in_default_order(self) -> list: @@ -1235,12 +1076,8 @@ class MainWindow(QMainWindow): if not self._in_compact_automation_view: self._pre_automation_window_state = self.saveState() self._pre_automation_ref_tools_visible = self.ref_tools_dock.isVisible() - self._pre_automation_left_column_visible = ( - self.collection_controls_scroll.isVisible() - ) - self._pre_automation_right_column_visible = ( - self.beamline_controls_scroll.isVisible() - ) + self._pre_automation_left_column_visible = self.collection_controls_scroll.isVisible() + self._pre_automation_right_column_visible = self.beamline_controls_scroll.isVisible() self.tell_samples_dock.setVisible(False) self.job_list_dock.setVisible(False) @@ -1270,12 +1107,8 @@ class MainWindow(QMainWindow): if self._pre_automation_window_state is not None: self.restoreState(self._pre_automation_window_state) - self.collection_controls_scroll.setVisible( - self._pre_automation_left_column_visible - ) - self.beamline_controls_scroll.setVisible( - self._pre_automation_right_column_visible - ) + self.collection_controls_scroll.setVisible(self._pre_automation_left_column_visible) + self.beamline_controls_scroll.setVisible(self._pre_automation_right_column_visible) if self.__decoded_token.staff: self.ref_tools_dock.setVisible(self._pre_automation_ref_tools_visible) @@ -1289,14 +1122,8 @@ class MainWindow(QMainWindow): @Slot() def _refresh_compact_queue_preview(self) -> None: - current_sample, next_sample, next_next_sample = ( - self.job_list_panel.queue_preview() - ) - self.compact_automation_panel.set_samples( - current_sample, - next_sample, - next_next_sample, - ) + current_sample, next_sample, next_next_sample = self.job_list_panel.queue_preview() + self.compact_automation_panel.set_samples(current_sample, next_sample, next_next_sample) @Slot() def enter_portrait_mode(self) -> None: @@ -1304,9 +1131,7 @@ class MainWindow(QMainWindow): self._pre_portrait_geometry = self.saveGeometry() self.portrait_mode_panel.set_running(self.job_list_panel.is_running()) - self.portrait_mode_panel.set_samples_in_queue( - len(self.job_list_panel.table_model.samples) - ) + self.portrait_mode_panel.set_samples_in_queue(len(self.job_list_panel.table_model.samples)) self._refresh_portrait_queue_preview() self.content_stack.setCurrentWidget(self.portrait_mode_page) @@ -1455,9 +1280,7 @@ class MainWindow(QMainWindow): return mapping.get(annotation, str(annotation).strip()) @staticmethod - def _append_annotation_to_comment( - existing_comment: str | None, annotation: str - ) -> str: + def _append_annotation_to_comment(existing_comment: str | None, annotation: str) -> str: token = MainWindow._annotation_token(annotation) current = str(existing_comment or "").strip() @@ -1474,19 +1297,14 @@ class MainWindow(QMainWindow): def _handle_compact_annotation(self, annotation: str) -> None: current_sample, _, _ = self.job_list_panel.queue_preview() if current_sample is None: - self.status_bar.show_connection_message( - "No sample selected for annotation.", True - ) + self.status_bar.show_connection_message("No sample selected for annotation.", True) return updated_comment = self._append_annotation_to_comment( - getattr(current_sample, "comment", None), - annotation, + getattr(current_sample, "comment", None), annotation ) - self.job_list_panel.annotate_sample_comment( - current_sample.db_id, updated_comment - ) + self.job_list_panel.annotate_sample_comment(current_sample.db_id, updated_comment) self.tell_samples.annotate_sample_comment(current_sample.db_id, updated_comment) self._refresh_compact_queue_preview() @@ -1540,16 +1358,12 @@ class MainWindow(QMainWindow): self._enter_automation_view_action = QAction("Automation View", self) self._enter_automation_view_action.setShortcut(QKeySequence("Ctrl+5")) - self._enter_automation_view_action.triggered.connect( - self.enter_compact_automation_view - ) + self._enter_automation_view_action.triggered.connect(self.enter_compact_automation_view) menu_bar.addAction(self._enter_automation_view_action) self._return_main_view_action = QAction("Return to Main View", self) self._return_main_view_action.setShortcut(QKeySequence("Ctrl+Shift+5")) - self._return_main_view_action.triggered.connect( - self._return_from_compact_automation_view - ) + self._return_main_view_action.triggered.connect(self._return_from_compact_automation_view) menu_bar.addAction(self._return_main_view_action) self._portrait_mode_action = QAction("Portrait Mode", self) @@ -1579,13 +1393,9 @@ class MainWindow(QMainWindow): view_menu.addSeparator() if self._beamline_state_panel_enabled: - self._show_beamline_state_action = QAction( - "Show Beamline State Panel", self - ) + self._show_beamline_state_action = QAction("Show Beamline State Panel", self) self._show_beamline_state_action.setCheckable(True) - self._show_beamline_state_action.setChecked( - self.beamline_state_panel.isVisible() - ) + self._show_beamline_state_action.setChecked(self.beamline_state_panel.isVisible()) self._show_beamline_state_action.triggered.connect( lambda checked: self.beamline_state_panel.setVisible(checked) ) @@ -1607,9 +1417,7 @@ class MainWindow(QMainWindow): show_reference_tools_action.triggered.connect( lambda checked: self.ref_tools_dock.setVisible(checked) ) - self.ref_tools_dock.visibilityChanged.connect( - show_reference_tools_action.setChecked - ) + self.ref_tools_dock.visibilityChanged.connect(show_reference_tools_action.setChecked) view_menu.addAction(show_reference_tools_action) show_job_list_action = QAction("Show job List", self) @@ -1627,9 +1435,7 @@ class MainWindow(QMainWindow): show_manual_sample_action.triggered.connect( lambda checked: self.manual_sample_dock.setVisible(checked) ) - self.manual_sample_dock.visibilityChanged.connect( - show_manual_sample_action.setChecked - ) + self.manual_sample_dock.visibilityChanged.connect(show_manual_sample_action.setChecked) view_menu.addAction(show_manual_sample_action) show_face_panel_action = QAction("Show face detection", self) @@ -1638,9 +1444,7 @@ class MainWindow(QMainWindow): show_face_panel_action.triggered.connect( lambda checked: self.face_panel_dock.setVisible(checked) ) - self.face_panel_dock.visibilityChanged.connect( - show_face_panel_action.setChecked - ) + self.face_panel_dock.visibilityChanged.connect(show_face_panel_action.setChecked) view_menu.addAction(show_face_panel_action) show_fluor_panel_action = QAction("Show fluorescence", self) @@ -1649,9 +1453,7 @@ class MainWindow(QMainWindow): show_fluor_panel_action.triggered.connect( lambda checked: self.fluor_panel_dock.setVisible(checked) ) - self.fluor_panel_dock.visibilityChanged.connect( - show_fluor_panel_action.setChecked - ) + self.fluor_panel_dock.visibilityChanged.connect(show_fluor_panel_action.setChecked) view_menu.addAction(show_fluor_panel_action) show_smargon_trace_action = QAction("Show Smargon trace", self) @@ -1660,13 +1462,9 @@ class MainWindow(QMainWindow): show_smargon_trace_action.triggered.connect( lambda checked: self.smargon_trace_dock.setVisible(checked) ) + self.smargon_trace_dock.visibilityChanged.connect(show_smargon_trace_action.setChecked) self.smargon_trace_dock.visibilityChanged.connect( - show_smargon_trace_action.setChecked - ) - self.smargon_trace_dock.visibilityChanged.connect( - lambda visible: ( - self.smargon_trace_panel.refresh_plot(force=True) if visible else None - ) + lambda visible: self.smargon_trace_panel.refresh_plot(force=True) if visible else None ) view_menu.addAction(show_smargon_trace_action) @@ -1695,9 +1493,7 @@ class MainWindow(QMainWindow): show_log_action = QAction("Show Log", self) show_log_action.setCheckable(True) show_log_action.setChecked(False) - show_log_action.triggered.connect( - lambda checked: self.log_dock.setVisible(checked) - ) + show_log_action.triggered.connect(lambda checked: self.log_dock.setVisible(checked)) self.log_dock.visibilityChanged.connect(show_log_action.setChecked) view_menu.addAction(show_log_action) @@ -1705,30 +1501,22 @@ class MainWindow(QMainWindow): sample_camera_tab_action = QAction("Sample camera tab", self) sample_camera_tab_action.setShortcut(QKeySequence("Ctrl+1")) - sample_camera_tab_action.triggered.connect( - lambda: self.video_tab.setCurrentIndex(0) - ) + sample_camera_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(0)) view_menu.addAction(sample_camera_tab_action) gonio_camera_tab_action = QAction("Gonio camera tab", self) gonio_camera_tab_action.setShortcut(QKeySequence("Ctrl+2")) - gonio_camera_tab_action.triggered.connect( - lambda: self.video_tab.setCurrentIndex(1) - ) + gonio_camera_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(1)) view_menu.addAction(gonio_camera_tab_action) beamline_view_tab_action = QAction("Beamline view tab", self) beamline_view_tab_action.setShortcut(QKeySequence("Ctrl+3")) - beamline_view_tab_action.triggered.connect( - lambda: self.video_tab.setCurrentIndex(2) - ) + beamline_view_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(2)) view_menu.addAction(beamline_view_tab_action) beamline_combined_tab_action = QAction("Beamline combined view tab", self) beamline_combined_tab_action.setShortcut(QKeySequence("Ctrl+4")) - beamline_combined_tab_action.triggered.connect( - lambda: self.video_tab.setCurrentIndex(3) - ) + beamline_combined_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(3)) view_menu.addAction(beamline_combined_tab_action) view_menu.addSeparator() @@ -1769,12 +1557,8 @@ class MainWindow(QMainWindow): start_text_tutorial_action.triggered.connect(self.start_text_tutorial) help_menu.addAction(start_text_tutorial_action) - start_interactive_tutorial_action = QAction( - "Start Tutorial (Interactive)", self - ) - start_interactive_tutorial_action.triggered.connect( - self.start_interactive_tutorial - ) + start_interactive_tutorial_action = QAction("Start Tutorial (Interactive)", self) + start_interactive_tutorial_action.triggered.connect(self.start_interactive_tutorial) help_menu.addAction(start_interactive_tutorial_action) def _capture_default_window_state(self) -> None: @@ -1854,11 +1638,7 @@ class MainWindow(QMainWindow): auto_clear_ms: int | None = None, ) -> None: self.log_dock.show_notification( - title=title, - message=message, - level=level, - sticky=sticky, - auto_clear_ms=auto_clear_ms, + title=title, message=message, level=level, sticky=sticky, auto_clear_ms=auto_clear_ms ) def _show_runtime_waiting_notification(self, *, title: str, message: str) -> None: @@ -1987,9 +1767,7 @@ class MainWindow(QMainWindow): "complete the safety search before mounting." ) if getattr(bl, "pss_alarm", False): - return ( - "The hutch safety alarm is active. Mounting is blocked until it clears." - ) + return "The hutch safety alarm is active. Mounting is blocked until it clears." return None def _on_manual_mount_requested(self, sample, reference: bool = False) -> None: @@ -2047,10 +1825,7 @@ class MainWindow(QMainWindow): logger.critical(f"Manual collection critical detector failure: {message}") self._show_runtime_notification( - title="Collection paused", - message=message, - level="error", - sticky=True, + title="Collection paused", message=message, level="error", sticky=True ) try: @@ -2091,9 +1866,7 @@ class MainWindow(QMainWindow): if self.daq is not None: self.daq.send_status_request() except Exception as e: - logger.error( - f"Failed to pause automation queue after critical failure: {e}" - ) + logger.error(f"Failed to pause automation queue after critical failure: {e}") # 2. Mark the automation progress widget as finished-with-error so # _is_automation_active() returns False and idle/close timers behave. @@ -2110,23 +1883,13 @@ class MainWindow(QMainWindow): progress = AutomationProgress( current_step=None, steps=[ + StepState(step=WorkflowStateKind.MOUNT, status=StepStatus.PENDING), + StepState(step=WorkflowStateKind.LOOP_CENTRE, status=StepStatus.PENDING), + StepState(step=WorkflowStateKind.RASTER, status=StepStatus.PENDING), StepState( - step=WorkflowStateKind.MOUNT, status=StepStatus.PENDING - ), - StepState( - step=WorkflowStateKind.LOOP_CENTRE, - status=StepStatus.PENDING, - ), - StepState( - step=WorkflowStateKind.RASTER, status=StepStatus.PENDING - ), - StepState( - step=WorkflowStateKind.DATA_COLLECTION, - status=StepStatus.PENDING, - ), - StepState( - step=WorkflowStateKind.FINAL, status=StepStatus.PENDING + step=WorkflowStateKind.DATA_COLLECTION, status=StepStatus.PENDING ), + StepState(step=WorkflowStateKind.FINAL, status=StepStatus.PENDING), ], finished=False, success=None, @@ -2142,15 +1905,10 @@ class MainWindow(QMainWindow): progress.success = False self.automation_progress_panel.set_progress(progress) except Exception as e: - logger.error( - f"Failed to update automation progress after critical failure: {e}" - ) + logger.error(f"Failed to update automation progress after critical failure: {e}") self._show_runtime_notification( - title="Automation paused", - message=message, - level="error", - sticky=True, + title="Automation paused", message=message, level="error", sticky=True ) # 4. Surface recovery UI @@ -2204,10 +1962,7 @@ class MainWindow(QMainWindow): if not bool(getattr(self.__decoded_token, "staff", False)): return if self._beamline_recovery_dialog is None: - self._beamline_recovery_dialog = BeamlineRecoveryDialog( - daq=self.daq, - parent=self, - ) + self._beamline_recovery_dialog = BeamlineRecoveryDialog(daq=self.daq, parent=self) self._beamline_recovery_dialog.show() self._beamline_recovery_dialog.raise_() self._beamline_recovery_dialog.activateWindow() @@ -2216,10 +1971,7 @@ class MainWindow(QMainWindow): if not bool(getattr(self.__decoded_token, "staff", False)): return if self._local_contact_dialog is None: - self._local_contact_dialog = LocalContactDialog( - daq=self.daq, - parent=self, - ) + self._local_contact_dialog = LocalContactDialog(daq=self.daq, parent=self) self._local_contact_dialog.set_active_tab(tab_name) self._local_contact_dialog.show() self._local_contact_dialog.raise_() @@ -2248,16 +2000,10 @@ class MainWindow(QMainWindow): if self._is_automation_active(): self._refresh_idle_activity(report_backend=False) - if ( - hasattr(self, "beamline_camera_thread") - and self.beamline_camera_thread is not None - ): + if hasattr(self, "beamline_camera_thread") and self.beamline_camera_thread is not None: self.beamline_camera_thread.set_busy(s.busy) - if ( - hasattr(self, "gonio_camera_thread") - and self.gonio_camera_thread is not None - ): + if hasattr(self, "gonio_camera_thread") and self.gonio_camera_thread is not None: self.gonio_camera_thread.set_busy(s.busy) busy_style = build_busy_overlay_style( @@ -2266,22 +2012,15 @@ class MainWindow(QMainWindow): session_state=getattr(getattr(s, "session", None), "session", None), ) - if ( - hasattr(self, "beamline_view_panel") - and self.beamline_view_panel is not None - ): + if hasattr(self, "beamline_view_panel") and self.beamline_view_panel is not None: self.beamline_view_panel.set_busy_style(busy_style) if hasattr(self, "gonio_view_panel") and self.gonio_view_panel is not None: self.gonio_view_panel.set_busy_style(busy_style) - if ( - hasattr(self, "beamline_combined_panel") - and self.beamline_combined_panel is not None - ): + if hasattr(self, "beamline_combined_panel") and self.beamline_combined_panel is not None: self.beamline_combined_panel.set_busy_style(busy_style) self.target_stability_panel.set_beam_center( - s.geom.beam_location_pxl.x, - s.geom.beam_location_pxl.y, + s.geom.beam_location_pxl.x, s.geom.beam_location_pxl.y ) self._refresh_compact_queue_preview() @@ -2327,16 +2066,12 @@ class MainWindow(QMainWindow): self._baton_request_dialog = None self._baton_request_dialog = BatonRequestDialog( - requester=requester, - timeout_seconds=timeout, - parent=self, + requester=requester, timeout_seconds=timeout, parent=self ) self._baton_request_dialog.accepted_signal.connect( self.status_bar._on_baton_dialog_accepted ) - self._baton_request_dialog.refused_signal.connect( - self.status_bar._on_baton_dialog_refused - ) + self._baton_request_dialog.refused_signal.connect(self.status_bar._on_baton_dialog_refused) self._baton_request_dialog.show() self._baton_request_dialog.raise_() self._baton_request_dialog.activateWindow() @@ -2361,9 +2096,7 @@ class MainWindow(QMainWindow): self._close_baton_pending_dialog() if status.you_are_holder: - self.alert_banner.show_message( - "Baton acquired!", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Baton acquired!", False, auto_clear_ms=10000) else: self.alert_banner.show_message( "Request declined or cancelled", False, auto_clear_ms=10000 @@ -2371,9 +2104,7 @@ class MainWindow(QMainWindow): # Manage incoming request dialog (when someone requests from us) if not status.incoming_request and self._baton_request_dialog is not None: - logger.info( - "Incoming baton request no longer active, closing request dialog" - ) + logger.info("Incoming baton request no longer active, closing request dialog") self._close_baton_dialog() @Slot(dict) @@ -2386,9 +2117,7 @@ class MainWindow(QMainWindow): """ if result.get("granted"): self._waiting_for_baton_response = False - self.alert_banner.show_message( - "Baton acquired!", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Baton acquired!", False, auto_clear_ms=10000) logger.info("Baton acquired") # Close pending dialog; StatusBar will trigger p-group selection via SSE self._close_baton_pending_dialog() @@ -2401,15 +2130,12 @@ class MainWindow(QMainWindow): if getattr(self, "_baton_pending_dialog", None) is None: target_user = holder.replace("Request sent to ", "").replace( - " (Note: beamline is currently busy, transfer will be queued if accepted)", - "", + " (Note: beamline is currently busy, transfer will be queued if accepted)", "" ) self._baton_pending_dialog = BatonPendingDialog( target_user=target_user, timeout_seconds=timeout, parent=self ) - self._baton_pending_dialog.cancelled_signal.connect( - self.daq.cancel_baton_request - ) + self._baton_pending_dialog.cancelled_signal.connect(self.daq.cancel_baton_request) self._baton_pending_dialog.show() else: self._baton_pending_dialog.update_remaining(timeout) @@ -2419,18 +2145,14 @@ class MainWindow(QMainWindow): elif result.get("queued"): self._waiting_for_baton_response = True - self.alert_banner.show_waiting( - "Control transfer queued - waiting for beamline" - ) + self.alert_banner.show_waiting("Control transfer queued - waiting for beamline") logger.info("Baton transfer queued") if getattr(self, "_baton_pending_dialog", None) is None: self._baton_pending_dialog = BatonPendingDialog( target_user="Current Holder", timeout_seconds=0, parent=self ) - self._baton_pending_dialog.cancelled_signal.connect( - self.daq.cancel_baton_request - ) + self._baton_pending_dialog.cancelled_signal.connect(self.daq.cancel_baton_request) self._baton_pending_dialog.show() self._baton_pending_dialog.set_queued_state() @@ -2456,16 +2178,12 @@ class MainWindow(QMainWindow): logger.debug(f"Baton response result: {result}") if result.get("accepted"): self._waiting_for_baton_response = False - self.alert_banner.show_message( - "Control transferred", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Control transferred", False, auto_clear_ms=10000) self._close_baton_dialog() self.status_bar.update_baton_status(self.status_bar._baton_status) elif result.get("refused"): self._waiting_for_baton_response = False - self.alert_banner.show_message( - "Request declined", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Request declined", False, auto_clear_ms=10000) self._close_baton_dialog() self.status_bar.update_baton_status(self.status_bar._baton_status) else: @@ -2484,16 +2202,12 @@ class MainWindow(QMainWindow): elif result.get("granted"): self._waiting_for_baton_response = False - self.alert_banner.show_message( - "Baton acquired!", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Baton acquired!", False, auto_clear_ms=10000) self._close_baton_pending_dialog() elif result.get("queued"): self._waiting_for_baton_response = True - self.alert_banner.show_waiting( - "Control transfer queued - waiting for beamline" - ) + self.alert_banner.show_waiting("Control transfer queued - waiting for beamline") if getattr(self, "_baton_pending_dialog", None) is not None: self._baton_pending_dialog.set_queued_state() @@ -2501,17 +2215,13 @@ class MainWindow(QMainWindow): self._baton_pending_dialog = BatonPendingDialog( target_user="Current Holder", timeout_seconds=0, parent=self ) - self._baton_pending_dialog.cancelled_signal.connect( - self.daq.cancel_baton_request - ) + self._baton_pending_dialog.cancelled_signal.connect(self.daq.cancel_baton_request) self._baton_pending_dialog.show() self._baton_pending_dialog.set_queued_state() elif result.get("refused"): self._waiting_for_baton_response = False - self.alert_banner.show_message( - "Request declined", False, auto_clear_ms=10000 - ) + self.alert_banner.show_message("Request declined", False, auto_clear_ms=10000) self._close_baton_pending_dialog() else: @@ -2542,9 +2252,7 @@ class MainWindow(QMainWindow): settings.beginGroup("panel_visibility") settings.setValue("smargon_trace", self.smargon_trace_dock.isVisible()) settings.setValue("target_stability", self.target_stability_dock.isVisible()) - settings.setValue( - "prediction_metrics", self.prediction_metrics_dock.isVisible() - ) + settings.setValue("prediction_metrics", self.prediction_metrics_dock.isVisible()) settings.setValue("face_detection", self.face_panel_dock.isVisible()) settings.setValue("fluorescence", self.fluor_panel_dock.isVisible()) settings.setValue("log", self.log_dock.isVisible()) @@ -2556,9 +2264,7 @@ class MainWindow(QMainWindow): settings.beginGroup("panel_visibility") if settings.contains("smargon_trace"): - self.smargon_trace_dock.setVisible( - settings.value("smargon_trace", False, type=bool) - ) + self.smargon_trace_dock.setVisible(settings.value("smargon_trace", False, type=bool)) if settings.contains("target_stability"): self.target_stability_dock.setVisible( settings.value("target_stability", False, type=bool) @@ -2568,13 +2274,9 @@ class MainWindow(QMainWindow): settings.value("prediction_metrics", False, type=bool) ) if settings.contains("face_detection"): - self.face_panel_dock.setVisible( - settings.value("face_detection", False, type=bool) - ) + self.face_panel_dock.setVisible(settings.value("face_detection", False, type=bool)) if settings.contains("fluorescence"): - self.fluor_panel_dock.setVisible( - settings.value("fluorescence", False, type=bool) - ) + self.fluor_panel_dock.setVisible(settings.value("fluorescence", False, type=bool)) if settings.contains("log"): self.log_dock.setVisible(settings.value("log", False, type=bool)) @@ -2626,10 +2328,7 @@ class MainWindow(QMainWindow): self._cleanup_done = True try: - if ( - hasattr(self, "_samcam_source_timer") - and self._samcam_source_timer is not None - ): + if hasattr(self, "_samcam_source_timer") and self._samcam_source_timer is not None: self._samcam_source_timer.stop() except Exception as e: logger.warning(f"Failed to stop _samcam_source_timer: {e}") @@ -2641,10 +2340,7 @@ class MainWindow(QMainWindow): logger.warning(f"Failed to stop _idle_timer: {e}") try: - if ( - hasattr(self, "_remote_close_timer") - and self._remote_close_timer is not None - ): + if hasattr(self, "_remote_close_timer") and self._remote_close_timer is not None: self._remote_close_timer.stop() except Exception as e: logger.warning(f"Failed to stop _remote_close_timer: {e}") @@ -2703,10 +2399,7 @@ class MainWindow(QMainWindow): return now = time.monotonic() - if ( - now - self._last_interaction_report_ts - < self._interaction_report_min_interval_s - ): + if now - self._last_interaction_report_ts < self._interaction_report_min_interval_s: return self._last_interaction_report_ts = now @@ -2783,8 +2476,7 @@ class MainWindow(QMainWindow): if not self._can_close_for_idle_or_remote(): if self._remote_close_banner_active: self.alert_banner_secondary.show_message( - "GUI close requested, waiting for beamline/automation to become idle.", - True, + "GUI close requested, waiting for beamline/automation to become idle.", True ) return diff --git a/src/aare/gui/models/bookmark.py b/src/aare/gui/models/bookmark.py index 8483d8f6..e0370467 100644 --- a/src/aare/gui/models/bookmark.py +++ b/src/aare/gui/models/bookmark.py @@ -6,9 +6,7 @@ from PySide6.QtGui import QColor class SmargonBookmark: coord: SmargonCoordinate - color: Literal[ - "red", "green", "blue", "indigo", "lime" - ] # Restrict to specific colors + color: Literal["red", "green", "blue", "indigo", "lime"] # Restrict to specific colors def qt_color(self) -> QColor: """Convert the color property to a QColor.""" diff --git a/src/aare/gui/models/gui_state_manager.py b/src/aare/gui/models/gui_state_manager.py index c49d9245..2cdaa90f 100644 --- a/src/aare/gui/models/gui_state_manager.py +++ b/src/aare/gui/models/gui_state_manager.py @@ -21,7 +21,6 @@ class UIStateManager: def save_model(self, key: str, model): try: - data = model.to_list() self.settings.setValue(key, json.dumps(data)) @@ -45,4 +44,4 @@ class UIStateManager: self.settings.setValue(key, value) def load_value(self, key: str, default=None, value_type=None): - return self.settings.value(key, default, type=value_type) \ No newline at end of file + return self.settings.value(key, default, type=value_type) diff --git a/src/aare/gui/models/sample_queue_model.py b/src/aare/gui/models/sample_queue_model.py index c2a20ed4..5452a610 100644 --- a/src/aare/gui/models/sample_queue_model.py +++ b/src/aare/gui/models/sample_queue_model.py @@ -25,10 +25,7 @@ def get_entry(sample: SampleShortInfo, column: int, *, show_user: bool = False): class SampleQueueSpreadsheet(QAbstractTableModel): def __init__( - self, - parent=None, - samples: list[SampleShortInfo] | None = None, - show_user: bool = False, + self, parent=None, samples: list[SampleShortInfo] | None = None, show_user: bool = False ): super().__init__(parent) if samples is None: @@ -37,9 +34,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel): self._show_user = bool(show_user) self.samples: list[SampleShortInfo] = samples self.header = ( - ["User", "Dewar", "Puck", "Sample"] - if self._show_user - else ["Dewar", "Puck", "Sample"] + ["User", "Dewar", "Puck", "Sample"] if self._show_user else ["Dewar", "Puck", "Sample"] ) def flags(self, index): @@ -54,9 +49,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel): def data(self, index, role=None): if role == Qt.ItemDataRole.DisplayRole: - return get_entry( - self.samples[index.row()], index.column(), show_user=self._show_user - ) + return get_entry(self.samples[index.row()], index.column(), show_user=self._show_user) elif role == Qt.ItemDataRole.TextAlignmentRole: return Qt.AlignmentFlag.AlignCenter elif role == Qt.ItemDataRole.BackgroundRole: @@ -76,10 +69,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel): return str(section + 1) return None - def updateData( - self, - samples: list[SampleShortInfo], - ): + def updateData(self, samples: list[SampleShortInfo]): self.beginResetModel() self.samples = list(samples) self.endResetModel() @@ -148,15 +138,11 @@ class SampleQueueSpreadsheet(QAbstractTableModel): self.endResetModel() def to_state(self) -> dict: - return { - "samples": [s.model_dump() for s in self.samples], - } + return {"samples": [s.model_dump() for s in self.samples]} def from_state(self, state: dict): self.beginResetModel() - self.samples = [ - SampleShortInfo.model_validate(d) for d in state.get("samples", []) - ] + self.samples = [SampleShortInfo.model_validate(d) for d in state.get("samples", [])] self.endResetModel() diff --git a/src/aare/gui/models/user_sample_model.py b/src/aare/gui/models/user_sample_model.py index 1637c8f4..10e85340 100644 --- a/src/aare/gui/models/user_sample_model.py +++ b/src/aare/gui/models/user_sample_model.py @@ -304,9 +304,7 @@ class UserSampleSpreadsheet(QAbstractTableModel): items = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])) return [k for k, _ in items[:limit]] - def suggested_prefixes_for_location( - self, limit: int = 200 - ) -> tuple[list[str], list[str]]: + def suggested_prefixes_for_location(self, limit: int = 200) -> tuple[list[str], list[str]]: """Get location prefixes from currently filtered samples (excluding column 3 filter).""" # Get currently filtered samples, excluding the location filter temp_filter = self.__filters.pop(3, None) @@ -326,7 +324,5 @@ class UserSampleSpreadsheet(QAbstractTableModel): if seg: seg_seen.add(seg) segs = sorted(seg_seen) - segpos = sorted( - segpos_seen, key=lambda x: (x[0], int(x[1:]) if x[1:].isdigit() else 0) - ) + segpos = sorted(segpos_seen, key=lambda x: (x[0], int(x[1:]) if x[1:].isdigit() else 0)) return (segs[:limit], segpos[:limit]) diff --git a/src/aare/gui/panels/LogPanel.py b/src/aare/gui/panels/LogPanel.py index 3ad76b36..e2ad35e9 100644 --- a/src/aare/gui/panels/LogPanel.py +++ b/src/aare/gui/panels/LogPanel.py @@ -149,15 +149,9 @@ class RuntimeNotificationWidget(QFrame): timeout = 5000 if auto_clear_ms is None else int(auto_clear_ms) self._auto_clear_timer.start(timeout) - def show_waiting( - self, *, title: str, message: str, auto_clear_ms: int | None = None - ) -> None: + def show_waiting(self, *, title: str, message: str, auto_clear_ms: int | None = None) -> None: self.show_notification( - title=title, - message=message, - level="warning", - sticky=True, - auto_clear_ms=auto_clear_ms, + title=title, message=message, level="warning", sticky=True, auto_clear_ms=auto_clear_ms ) @Slot() @@ -225,11 +219,7 @@ class LogDock(QDockWidget): self.setVisible(True) self.raise_() self.notification.show_notification( - title=title, - message=message, - level=level, - sticky=sticky, - auto_clear_ms=auto_clear_ms, + title=title, message=message, level=level, sticky=sticky, auto_clear_ms=auto_clear_ms ) def show_waiting_notification(self, *, title: str, message: str) -> None: diff --git a/src/aare/gui/panels/automation_panel.py b/src/aare/gui/panels/automation_panel.py index 6e4186ab..e1b95266 100644 --- a/src/aare/gui/panels/automation_panel.py +++ b/src/aare/gui/panels/automation_panel.py @@ -5,17 +5,9 @@ import time from datetime import datetime from aarecommon.config.logger import setup_logger -from aarecommon.models.automation import ( - AutomationProgress, - StepStatus, - WorkflowStateKind, -) +from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind from PySide6.QtCore import QTimer, Slot -from PySide6.QtWidgets import ( - QLabel, - QVBoxLayout, - QWidget, -) +from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget logger = setup_logger("aareGUI") @@ -128,10 +120,7 @@ class AutomationProgressWidget(QWidget): ) if status == StepStatus.SUCCESS: - return ( - base - + " background-color: #ECFDF3; color: #166534; border-color: #A7F3D0;" - ) + return base + " background-color: #ECFDF3; color: #166534; border-color: #A7F3D0;" if status == StepStatus.RUNNING: return ( base @@ -148,13 +137,8 @@ class AutomationProgressWidget(QWidget): + " background-color: #FFF7ED; color: #C2410C; font-weight: 700; border-color: #FED7AA;" ) if status == StepStatus.SKIPPED: - return ( - base - + " background-color: #F8FAFC; color: #475569; border-color: #E2E8F0;" - ) - return ( - base + " background-color: #F8FAFC; color: #64748B; border-color: #E2E8F0;" - ) + return base + " background-color: #F8FAFC; color: #475569; border-color: #E2E8F0;" + return base + " background-color: #F8FAFC; color: #64748B; border-color: #E2E8F0;" @staticmethod def _format_duration(seconds: float | None) -> str: @@ -209,8 +193,7 @@ class AutomationProgressWidget(QWidget): progress = copy.deepcopy(self._progress) final_step = next( - (step for step in progress.steps if step.step == WorkflowStateKind.FINAL), - None, + (step for step in progress.steps if step.step == WorkflowStateKind.FINAL), None ) if self._is_paused: @@ -245,13 +228,9 @@ class AutomationProgressWidget(QWidget): if self._stats_label: measured_avg_seconds = ( - progress.avg_time_per_sample - if progress.avg_time_per_sample > 0 - else None - ) - displayed_avg_seconds = ( - measured_avg_seconds or self.DEFAULT_SAMPLE_ESTIMATE_S + progress.avg_time_per_sample if progress.avg_time_per_sample > 0 else None ) + displayed_avg_seconds = measured_avg_seconds or self.DEFAULT_SAMPLE_ESTIMATE_S current_sample = progress.current_sample_name or "None" samples_left = ( @@ -260,11 +239,7 @@ class AutomationProgressWidget(QWidget): else max(0, int(progress.samples_in_queue or 0)) ) - if ( - progress.finished - or self._is_paused - or not self._has_active_sample(progress) - ): + if progress.finished or self._is_paused or not self._has_active_sample(progress): queue_remaining = displayed_avg_seconds * samples_left else: queue_remaining = displayed_avg_seconds * max(0, samples_left) @@ -291,9 +266,7 @@ class AutomationProgressWidget(QWidget): duration_str = "" if step_state.started_at is not None: end = step_state.completed_at or time.time() - duration_str = ( - f" ({self._format_duration(end - step_state.started_at)})" - ) + duration_str = f" ({self._format_duration(end - step_state.started_at)})" if step_state.status == StepStatus.FAILED and step_state.message: message = f" — Failed: {step_state.message}" diff --git a/src/aare/gui/panels/axis_video_panel.py b/src/aare/gui/panels/axis_video_panel.py index 216ffd11..bd8153a5 100644 --- a/src/aare/gui/panels/axis_video_panel.py +++ b/src/aare/gui/panels/axis_video_panel.py @@ -80,12 +80,8 @@ class AxisVideoPanel(QWidget): else: self._status_label.setText(style.text) self._status_container.setProperty("busyState", "active") - self._status_dot.setStyleSheet( - f"background-color: {style.accent_dot};" - ) - self._status_label.setStyleSheet( - f"color: {style.badge_fg};" - ) + self._status_dot.setStyleSheet(f"background-color: {style.accent_dot};") + self._status_label.setStyleSheet(f"color: {style.badge_fg};") self._refresh_status_style() self._status_container.show() @@ -94,4 +90,4 @@ class AxisVideoPanel(QWidget): view.set_busy_overlay_style(style) def set_status_text(self, text: str) -> None: - self._status_label.setText(text or "") \ No newline at end of file + self._status_label.setText(text or "") diff --git a/src/aare/gui/panels/beamline_controls.py b/src/aare/gui/panels/beamline_controls.py index 6b4c2155..588470fd 100644 --- a/src/aare/gui/panels/beamline_controls.py +++ b/src/aare/gui/panels/beamline_controls.py @@ -15,7 +15,7 @@ from aare.gui.panels.zoom_panel import ZoomPanel class BeamlineControls(QFrame): set_width = 250 - def __init__(self, parent=None, staff : bool = True): + def __init__(self, parent=None, staff: bool = True): super().__init__(parent) self.setFixedWidth(self.set_width) self.setFrameShape(QFrame.Shape.StyledPanel) diff --git a/src/aare/gui/panels/beamline_recovery_panel.py b/src/aare/gui/panels/beamline_recovery_panel.py index de779de3..c83590b5 100644 --- a/src/aare/gui/panels/beamline_recovery_panel.py +++ b/src/aare/gui/panels/beamline_recovery_panel.py @@ -28,8 +28,7 @@ class RecoveryPanel(QWidget): layout.setSpacing(8) self._warning_primary = QLabel( - "⚠ Recovery actions are staff-only and intentionally dangerous.", - self, + "⚠ Recovery actions are staff-only and intentionally dangerous.", self ) self._warning_primary.setWordWrap(True) self._warning_primary.setStyleSheet( @@ -151,10 +150,7 @@ class RecoveryPanel(QWidget): def _prompt_recovery_code(self, action_name: str) -> str | None: code, ok = QInputDialog.getText( - self, - action_name, - "Enter recovery confirmation code:", - QLineEdit.EchoMode.Password, + self, action_name, "Enter recovery confirmation code:", QLineEdit.EchoMode.Password ) if not ok: return None @@ -163,9 +159,7 @@ class RecoveryPanel(QWidget): def _sample_appears_mounted(self) -> bool: try: - return ( - self._last_status is not None and self._last_status.sample is not None - ) + return self._last_status is not None and self._last_status.sample is not None except Exception: return False @@ -188,15 +182,11 @@ class RecoveryPanel(QWidget): self._free_beamline_btn.setEnabled(beamline_busy) self._free_beamline_btn.setToolTip( - "" - if beamline_busy - else "Disabled because beamline does not currently appear busy." + "" if beamline_busy else "Disabled because beamline does not currently appear busy." ) self._resync_sample_btn.setEnabled(True) - self._resync_sample_btn.setToolTip( - "Force a one-shot sample reconciliation against TELL." - ) + self._resync_sample_btn.setToolTip("Force a one-shot sample reconciliation against TELL.") def _confirm(self, title: str, msg: str) -> bool: reply = QMessageBox.warning( @@ -229,8 +219,7 @@ class RecoveryPanel(QWidget): @Slot() def _free_beamline(self) -> None: if not self._confirm( - "Free beamline", - "This will clear the beamline busy flag.\n\nDo you want to continue?", + "Free beamline", "This will clear the beamline busy flag.\n\nDo you want to continue?" ): return code = self._prompt_recovery_code("Free beamline") diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py index 70e4dbdd..26210c41 100644 --- a/src/aare/gui/panels/beamline_state_panel.py +++ b/src/aare/gui/panels/beamline_state_panel.py @@ -150,12 +150,7 @@ class BeamlineStatePanel(QFrame): "Sample centring and alignment mode", ), StationSpec( - BeamlineStateEnum.BeamLocation, - "Beam location", - 54, - 284, - True, - "Beam location mode", + BeamlineStateEnum.BeamLocation, "Beam location", 54, 284, True, "Beam location mode" ), StationSpec( BeamlineStateEnum.BeamstopAlignment, @@ -190,12 +185,7 @@ class BeamlineStatePanel(QFrame): "Crystal snapshot mode", ), StationSpec( - BeamlineStateEnum.XrayFluorescence, - "XRF", - 54, - 464, - True, - "X-ray fluorescence mode", + BeamlineStateEnum.XrayFluorescence, "XRF", 54, 464, True, "X-ray fluorescence mode" ), ] @@ -252,8 +242,7 @@ class BeamlineStatePanel(QFrame): return tuple(sorted((a, b), key=lambda state: state.value)) def _build_graph( - self, - segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]], + self, segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]] ) -> dict[BeamlineStateEnum, set[BeamlineStateEnum]]: graph: dict[BeamlineStateEnum, set[BeamlineStateEnum]] = {} for a, b in segments: @@ -265,9 +254,7 @@ class BeamlineStatePanel(QFrame): return {station.state: station for station in self._stations} def _path_segments_between( - self, - start: BeamlineStateEnum | None, - end: BeamlineStateEnum | None, + self, start: BeamlineStateEnum | None, end: BeamlineStateEnum | None ) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]: if start is None or end is None: return set() @@ -317,9 +304,7 @@ class BeamlineStatePanel(QFrame): self._current_state == BeamlineStateEnum.Moving and self._pending_target_state is not None ): - return self._path_segments_between( - self._last_stable_state, self._pending_target_state - ) + return self._path_segments_between(self._last_stable_state, self._pending_target_state) return set() @@ -330,9 +315,7 @@ class BeamlineStatePanel(QFrame): widget.setFlat(True) widget.setCursor(Qt.CursorShape.PointingHandCursor) widget.clicked.connect( - lambda _checked=False, state=station.state: self._emit_for_state( - state - ) + lambda _checked=False, state=station.state: self._emit_for_state(state) ) else: widget = HoverableLabel(station.label, self) @@ -543,17 +526,14 @@ class BeamlineStatePanel(QFrame): return self._line_hover current_path = self._path_segments_between( - BeamlineStateEnum.DewarTransfer, - self._last_stable_state or self._current_state, + BeamlineStateEnum.DewarTransfer, self._last_stable_state or self._current_state ) if segment in current_path: return self._line_current return self._line_color - def _draw_segment( - self, painter: QPainter, start: QPoint, end: QPoint, color: QColor - ) -> None: + def _draw_segment(self, painter: QPainter, start: QPoint, end: QPoint, color: QColor) -> None: pen = QPen(color, 3) pen.setCapStyle(Qt.PenCapStyle.RoundCap) painter.setPen(pen) @@ -625,9 +605,7 @@ class BeamlineStatePanel(QFrame): tell_state = status.tell_state tell_text = f"Tell: {tell_state.activity.display_name()}" - tell_phase = ( - tell_state.phase.display_name() if tell_state.phase is not None else "" - ) + tell_phase = tell_state.phase.display_name() if tell_state.phase is not None else "" tell_message = (tell_state.message or "").strip() if tell_phase: @@ -637,12 +615,7 @@ class BeamlineStatePanel(QFrame): if tell_state.activity.value == "error": tell_color = "red" - elif tell_state.activity.value in { - "mounting", - "unmounting", - "drying", - "cooling", - }: + elif tell_state.activity.value in {"mounting", "unmounting", "drying", "cooling"}: tell_color = "orange" else: tell_color = "green" diff --git a/src/aare/gui/panels/compact_automation_panel.py b/src/aare/gui/panels/compact_automation_panel.py index de2ca6fe..cd48421b 100644 --- a/src/aare/gui/panels/compact_automation_panel.py +++ b/src/aare/gui/panels/compact_automation_panel.py @@ -103,9 +103,7 @@ class CompactAutomationPanel(QFrame): self.annotation_button = QToolButton(self) self.annotation_button.setObjectName("compactSecondaryButton") self.annotation_button.setText("Annotate") - self.annotation_button.setPopupMode( - QToolButton.ToolButtonPopupMode.InstantPopup - ) + self.annotation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) annotation_menu = QMenu(self.annotation_button) for label in ["Heart", "Thumbs Up", "Thumbs Down", "Eyes", "Scan Again"]: @@ -180,11 +178,7 @@ class CompactAutomationPanel(QFrame): layout.addWidget(title_label) layout.addWidget(value_label) - return { - "frame": frame, - "title": title_label, - "value": value_label, - } + return {"frame": frame, "title": title_label, "value": value_label} @Slot(bool) def set_running(self, running: bool) -> None: diff --git a/src/aare/gui/panels/data_collection_settings.py b/src/aare/gui/panels/data_collection_settings.py index 6912f5e9..6a65fd1b 100644 --- a/src/aare/gui/panels/data_collection_settings.py +++ b/src/aare/gui/panels/data_collection_settings.py @@ -2,12 +2,7 @@ from aarecommon.math.diffraction_geometry import DiffractionGeometry from aarecommon.math.sample_geometry import SampleGeometryModel from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Signal, Slot -from PySide6.QtWidgets import ( - QFrame, - QPushButton, - QTabWidget, - QVBoxLayout, -) +from PySide6.QtWidgets import QFrame, QPushButton, QTabWidget, QVBoxLayout from aare.gui.panels.file_path_panel import FilePathPanel from aare.gui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel @@ -45,9 +40,7 @@ class DataCollectionSettings(QFrame): ) self.__tab_widget.addTab(self.raster, "Raster scan") - self.screening = RotationDataCollectionPanel( - parent=self, diffraction=diffraction - ) + self.screening = RotationDataCollectionPanel(parent=self, diffraction=diffraction) self.__tab_widget.addTab(self.screening, "Rotation") self.simple = SimpleRotationSettingsPanel(parent=self) diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py index 0a92ce38..905b9c13 100644 --- a/src/aare/gui/panels/developer_help_dialog.py +++ b/src/aare/gui/panels/developer_help_dialog.py @@ -57,9 +57,7 @@ class DeveloperHelpDialog(QDialog): self._banner = QLabel(self) self._banner.setVisible(self._is_staff) self._banner.setWordWrap(True) - self._banner.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self._banner.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self._banner.setStyleSheet( "QLabel {" " background: #f6f6f6;" @@ -78,9 +76,7 @@ class DeveloperHelpDialog(QDialog): top.addWidget(QLabel("Filter:", self)) self._filter = QLineEdit(self) - self._filter.setPlaceholderText( - "Type to filter (matches name, value, or help)…" - ) + self._filter.setPlaceholderText("Type to filter (matches name, value, or help)…") self._filter.setClearButtonEnabled(True) self._filter.setMinimumHeight(28) self._filter.setStyleSheet( @@ -124,16 +120,12 @@ class DeveloperHelpDialog(QDialog): self._open_gui_log_btn = QPushButton("Open GUI log", self) self._open_gui_log_btn.setVisible(self._is_staff) - self._open_gui_log_btn.clicked.connect( - lambda: self._open_log_file_for_logger("aareGUI") - ) + self._open_gui_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareGUI")) top.addWidget(self._open_gui_log_btn) self._open_daq_log_btn = QPushButton("Open DAQ log", self) self._open_daq_log_btn.setVisible(self._is_staff) - self._open_daq_log_btn.clicked.connect( - lambda: self._open_log_file_for_logger("aareDAQ") - ) + self._open_daq_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareDAQ")) top.addWidget(self._open_daq_log_btn) self._tabs = QTabWidget(self) @@ -144,9 +136,7 @@ class DeveloperHelpDialog(QDialog): self._codes_table.setHorizontalHeaderLabels(["Name"]) self._codes_table.setSortingEnabled(True) self._codes_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) - self._codes_table.setSelectionBehavior( - QTableWidget.SelectionBehavior.SelectRows - ) + self._codes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self._codes_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) self._codes_table.itemSelectionChanged.connect(self._update_code_details) self._codes_table.horizontalHeader().setStretchLastSection(True) @@ -154,11 +144,7 @@ class DeveloperHelpDialog(QDialog): self._details_frame = QFrame(self) self._details_frame.setFrameShape(QFrame.Shape.StyledPanel) self._details_frame.setStyleSheet( - "QFrame {" - " background: #fafafa;" - " border: 1px solid #d0d0d0;" - " border-radius: 6px;" - "}" + "QFrame { background: #fafafa; border: 1px solid #d0d0d0; border-radius: 6px;}" ) details_layout = QVBoxLayout(self._details_frame) @@ -166,23 +152,17 @@ class DeveloperHelpDialog(QDialog): details_layout.setSpacing(8) form = QFormLayout() - form.setLabelAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) details_layout.addLayout(form) self._detail_name = QLabel("-", self) - self._detail_name.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self._detail_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) form.addRow("Name:", self._detail_name) value_row = QHBoxLayout() value_row.setSpacing(8) self._detail_value = QLabel("-", self) - self._detail_value.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self._detail_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self._copy_value_btn = QPushButton("Copy value", self) self._copy_value_btn.clicked.connect(self._copy_selected_value) value_row.addWidget(self._detail_value, 1) @@ -193,9 +173,7 @@ class DeveloperHelpDialog(QDialog): self._detail_help = QLabel("-", self) self._detail_help.setWordWrap(True) - self._detail_help.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self._detail_help.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self._detail_help.setStyleSheet( "QLabel {" " background: white;" @@ -219,9 +197,7 @@ class DeveloperHelpDialog(QDialog): self._payload_summary = QLabel(self) self._payload_summary.setVisible(self._is_staff) self._payload_summary.setWordWrap(True) - self._payload_summary.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self._payload_summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self._payload_text = QTextEdit(self) self._payload_text.setReadOnly(True) @@ -284,15 +260,9 @@ class DeveloperHelpDialog(QDialog): "Requested by", ] ) - self._gui_sessions_table.setEditTriggers( - QTableWidget.EditTrigger.NoEditTriggers - ) - self._gui_sessions_table.setSelectionBehavior( - QTableWidget.SelectionBehavior.SelectRows - ) - self._gui_sessions_table.setSelectionMode( - QTableWidget.SelectionMode.SingleSelection - ) + self._gui_sessions_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + self._gui_sessions_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self._gui_sessions_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) self._gui_sessions_table.setSortingEnabled(True) self._gui_sessions_table.horizontalHeader().setStretchLastSection(True) @@ -305,9 +275,7 @@ class DeveloperHelpDialog(QDialog): self._gui_close_grace_spin.setSuffix(" s") self._gui_request_close_btn = QPushButton("Request Close", self) - self._gui_request_close_btn.clicked.connect( - self._request_close_selected_gui - ) + self._gui_request_close_btn.clicked.connect(self._request_close_selected_gui) self._gui_force_remove_btn = QPushButton("Force Remove", self) self._gui_force_remove_btn.clicked.connect(self._force_remove_selected_gui) @@ -424,9 +392,7 @@ class DeveloperHelpDialog(QDialog): except Exception: pretty = str(p) blocks.append(f"#{i}\n{pretty}") - self._payloads_text.setPlainText( - "\n\n".join(blocks) if blocks else "(none captured yet)" - ) + self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)") @Slot(str) def _append_error_log_line(self, line: str) -> None: @@ -445,10 +411,7 @@ class DeveloperHelpDialog(QDialog): if isinstance(body, dict): code = body.get("code") msg = body.get("message") - return ( - str(code) if code is not None else None, - str(msg) if msg is not None else None, - ) + return (str(code) if code is not None else None, str(msg) if msg is not None else None) return (None, None) def _update_banner(self) -> None: @@ -520,11 +483,7 @@ class DeveloperHelpDialog(QDialog): self._update_code_details() # If enabled, re-try selection after the table content changes - if ( - self._is_staff - and self._always_highlight_last_error - and not self._freeze_payload - ): + if self._is_staff and self._always_highlight_last_error and not self._freeze_payload: self._select_code_from_last_error() def _selected_code(self) -> tuple[str | None, str | None]: @@ -550,9 +509,7 @@ class DeveloperHelpDialog(QDialog): self._detail_name.setText(name) self._detail_value.setText(value) - self._detail_help.setText( - error_code_help(value) or "(no help text defined yet)" - ) + self._detail_help.setText(error_code_help(value) or "(no help text defined yet)") self._copy_value_btn.setEnabled(True) @Slot() @@ -599,9 +556,7 @@ class DeveloperHelpDialog(QDialog): return self._gui_sessions = [ - s - if isinstance(s, OpenGuiSessionInfo) - else OpenGuiSessionInfo.model_validate(s) + s if isinstance(s, OpenGuiSessionInfo) else OpenGuiSessionInfo.model_validate(s) for s in sessions ] self._gui_sessions_table.setSortingEnabled(False) @@ -647,9 +602,7 @@ class DeveloperHelpDialog(QDialog): def _request_close_selected_gui(self) -> None: session_id = self._selected_gui_session_id() if session_id is None: - QMessageBox.information( - self, "Active GUIs", "Please select a GUI session first." - ) + QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.") return grace_seconds = int(self._gui_close_grace_spin.value()) @@ -668,9 +621,7 @@ class DeveloperHelpDialog(QDialog): def _force_remove_selected_gui(self) -> None: session_id = self._selected_gui_session_id() if session_id is None: - QMessageBox.information( - self, "Active GUIs", "Please select a GUI session first." - ) + QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.") return answer = QMessageBox.warning( diff --git a/src/aare/gui/panels/face_detection_panel.py b/src/aare/gui/panels/face_detection_panel.py index 12c09ebf..6f174662 100644 --- a/src/aare/gui/panels/face_detection_panel.py +++ b/src/aare/gui/panels/face_detection_panel.py @@ -27,13 +27,7 @@ class FaceDetectionPanel(QWidget): self.step_size = int(val) self.fig = Figure(figsize=(5, 4)) - self.fig.subplots_adjust( - left=0.18, - right=0.97, - bottom=0.10, - top=0.95, - hspace=0.45, - ) + self.fig.subplots_adjust(left=0.18, right=0.97, bottom=0.10, top=0.95, hspace=0.45) self.canvas = FigureCanvas(self.fig) self.ax1 = self.fig.add_subplot(2, 1, 1) self.ax2 = self.fig.add_subplot(2, 1, 2) @@ -130,9 +124,7 @@ class FaceDetectionPanel(QWidget): height_fit = A + B * np.cos(C * np.deg2rad(ang_grid) - phi) self.ax1.plot(ang_grid, height_fit, color="tab:orange", label="Height fit") if "best_angle_deg" in hf: - self.ax1.axvline( - hf["best_angle_deg"], color="tab:orange", ls="--", alpha=0.6 - ) + self.ax1.axvline(hf["best_angle_deg"], color="tab:orange", ls="--", alpha=0.6) af = data.get("area_fit", {}) or {} if {"A", "B", "phi_rad", "C"} <= af.keys(): @@ -140,9 +132,7 @@ class FaceDetectionPanel(QWidget): area_fit = A2 + B2 * np.cos(C2 * np.deg2rad(ang_grid) - phi2) self.ax2.plot(ang_grid, area_fit, color="tab:red", label="Area fit") if "best_angle_deg" in af: - self.ax2.axvline( - af["best_angle_deg"], color="tab:red", ls="--", alpha=0.6 - ) + self.ax2.axvline(af["best_angle_deg"], color="tab:red", ls="--", alpha=0.6) self.ax1.set_xlabel("Angle (deg)") self.ax1.set_ylabel("Height") diff --git a/src/aare/gui/panels/file_path_panel.py b/src/aare/gui/panels/file_path_panel.py index 6cd2c730..d3155482 100644 --- a/src/aare/gui/panels/file_path_panel.py +++ b/src/aare/gui/panels/file_path_panel.py @@ -4,14 +4,7 @@ from pathlib import Path from aarecommon.models.models import DAQStatusModel, SampleShortInfo from PySide6.QtCore import Qt, Signal, Slot -from PySide6.QtWidgets import ( - QGridLayout, - QLabel, - QLineEdit, - QMessageBox, - QSpinBox, - QWidget, -) +from PySide6.QtWidgets import QGridLayout, QLabel, QLineEdit, QMessageBox, QSpinBox, QWidget from aare.gui.widgets.title_label import TitleLabel @@ -203,19 +196,14 @@ class FilePathPanel(QWidget): self.__puck_pos = sample.pin self.run_number_edit.setValue(1) - if ( - sample.aaredb_params is not None - and sample.aaredb_params.directory is not None - ): + if sample.aaredb_params is not None and sample.aaredb_params.directory is not None: self.directory_edit.setText(f"{sample.aaredb_params.directory}") elif sample.location is not None: self.directory_edit.setText( f"{self.__formatted_date}/{self.__puck_name}/{self.__puck_pos:02d}" ) else: - self.directory_edit.setText( - f"{self.__formatted_date}/manual/{self.__sample_name}" - ) + self.directory_edit.setText(f"{self.__formatted_date}/manual/{self.__sample_name}") self.update_filename() @Slot(DAQStatusModel) @@ -234,15 +222,9 @@ class FilePathPanel(QWidget): def next_free_run_from(self, start_rn: int) -> tuple[int, str]: # Compute next free run number and updated base - dir_name = self.directory_edit.text().replace( - "{prefix}", self.file_prefix_edit.text() - ) + dir_name = self.directory_edit.text().replace("{prefix}", self.file_prefix_edit.text()) base = dir_name if dir_name.endswith("/") or dir_name == "" else dir_name + "/" - base += ( - "run" - if self.file_prefix_edit.text() == "" - else self.file_prefix_edit.text() - ) + base += "run" if self.file_prefix_edit.text() == "" else self.file_prefix_edit.text() rn = start_rn while rn <= self.run_number_edit.maximum(): diff --git a/src/aare/gui/panels/fluorescence_data_collection.py b/src/aare/gui/panels/fluorescence_data_collection.py index e7d57ea8..9266dfff 100644 --- a/src/aare/gui/panels/fluorescence_data_collection.py +++ b/src/aare/gui/panels/fluorescence_data_collection.py @@ -15,9 +15,7 @@ class FluorescenceDataCollectionPanel(QWidget): # Beam transmission (0..1) lay.addWidget(QLabel("Beam transmission", self), 0, 0) - self.transmission = NumberLineEdit( - 0.0, 1.0, decimals=4, default=0.1, parent=self - ) + self.transmission = NumberLineEdit(0.0, 1.0, decimals=4, default=0.1, parent=self) lay.addWidget(self.transmission, 0, 1) # Exposure time (seconds) @@ -27,9 +25,7 @@ class FluorescenceDataCollectionPanel(QWidget): lay.addWidget(QLabel("s", self), 1, 2) # Accumulate checkbox (reverse of erase) - self.accumulate_cb = QCheckBox( - "Accumulate", self - ) # accumulate=True => erase=False + self.accumulate_cb = QCheckBox("Accumulate", self) # accumulate=True => erase=False self.accumulate_cb.setChecked(False) lay.addWidget(self.accumulate_cb, 2, 0, 1, 3) @@ -49,7 +45,5 @@ class FluorescenceDataCollectionPanel(QWidget): exp = float(self.exposure.value) erase = not self.accumulate_cb.isChecked() self.fluo_scan.emit( - FluorescenceSpectrumParameterModel( - acq_time_s=exp, transmission=t, erase=erase - ) + FluorescenceSpectrumParameterModel(acq_time_s=exp, transmission=t, erase=erase) ) diff --git a/src/aare/gui/panels/fluorescence_panel.py b/src/aare/gui/panels/fluorescence_panel.py index 28135d08..fb83d5e5 100644 --- a/src/aare/gui/panels/fluorescence_panel.py +++ b/src/aare/gui/panels/fluorescence_panel.py @@ -78,10 +78,7 @@ class FluorescencePanel(QWidget): def eventFilter(self, obj, event): try: - if ( - obj is self.chart_view.viewport() - and event.type() == QEvent.Type.MouseMove - ): + if obj is self.chart_view.viewport() and event.type() == QEvent.Type.MouseMove: pos = event.position() if hasattr(event, "position") else event.pos() p = QPointF(pos.x(), pos.y()) plot = self.chart.plotArea() @@ -109,9 +106,7 @@ class FluorescencePanel(QWidget): best_i = i pt = self.series.at(best_i) - self.chart_view.setToolTip( - f"Energy {pt.x():.3f} keV counts {pt.y():.3f}" - ) + self.chart_view.setToolTip(f"Energy {pt.x():.3f} keV counts {pt.y():.3f}") self._update_vline(pt.x()) return False except Exception as e: @@ -134,11 +129,7 @@ class FluorescencePanel(QWidget): # Prefer cached arrays; fall back to reading from series x_vals = self._last_x_keV y_vals = self._last_y_counts - if ( - x_vals is None - or y_vals is None - or len(x_vals) != self.series.count() - ): + if x_vals is None or y_vals is None or len(x_vals) != self.series.count(): x_vals = [self.series.at(i).x() for i in range(self.series.count())] y_vals = [self.series.at(i).y() for i in range(self.series.count())] @@ -170,9 +161,9 @@ class FluorescencePanel(QWidget): try: ymin = self.axis_y.min() ymax = self.axis_y.max() - self._vline.replace( - 0, x_val, ymin - ) if self._vline.count() > 0 else self._vline.append(x_val, ymin) + self._vline.replace(0, x_val, ymin) if self._vline.count() > 0 else self._vline.append( + x_val, ymin + ) if self._vline.count() == 1: self._vline.append(x_val, ymax) else: diff --git a/src/aare/gui/panels/illumination_panel.py b/src/aare/gui/panels/illumination_panel.py index 7d624ac5..f14094b7 100644 --- a/src/aare/gui/panels/illumination_panel.py +++ b/src/aare/gui/panels/illumination_panel.py @@ -20,9 +20,7 @@ class IlluminationPanel(QWidget): grid_layout.addWidget(front_label, 1, 0, 1, 2) self.is_sliding = False - self.front_light_slider = QSlider( - orientation=Qt.Orientation.Horizontal, parent=self - ) + self.front_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) self.front_light_slider.setRange(0, 100) self.front_light_slider.sliderPressed.connect(self.on_slider_pressed) self.front_light_slider.sliderReleased.connect(self.on_front_slider_released) @@ -32,9 +30,7 @@ class IlluminationPanel(QWidget): back_label.setAlignment(Qt.AlignmentFlag.AlignCenter) grid_layout.addWidget(back_label, 3, 0, 1, 2) - self.back_light_slider = QSlider( - orientation=Qt.Orientation.Horizontal, parent=self - ) + self.back_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) self.back_light_slider.setRange(0, 100) self.back_light_slider.sliderPressed.connect(self.on_slider_pressed) self.back_light_slider.sliderReleased.connect(self.on_back_slider_released) diff --git a/src/aare/gui/panels/local_contact_panel.py b/src/aare/gui/panels/local_contact_panel.py index 9c158c08..ce1c676b 100644 --- a/src/aare/gui/panels/local_contact_panel.py +++ b/src/aare/gui/panels/local_contact_panel.py @@ -98,8 +98,7 @@ class LocalContactPanel(QFrame): layout.addWidget(TitleLabel("Local Contact", parent=self)) self._info_label = QLabel( - "Staff tools for beamline recovery and local-contact operations.", - self, + "Staff tools for beamline recovery and local-contact operations.", self ) self._info_label.setWordWrap(True) layout.addWidget(self._info_label) @@ -141,16 +140,12 @@ class LocalContactPanel(QFrame): self._tabs.addTab(self._build_detector_tab(), self.TAB_DETECTOR) self._tabs.addTab(self._build_config_tab(), self.TAB_CONFIG) - self._daq.local_contact_simulation_state_loaded.connect( - self._apply_simulation_state - ) + self._daq.local_contact_simulation_state_loaded.connect(self._apply_simulation_state) self._daq.local_contact_device_state_loaded.connect(self._apply_device_state) self._daq.local_contact_links_loaded.connect(self._apply_links) self._daq.local_contact_config_loaded.connect(self._apply_local_contact_config) self._daq.local_contact_config_saved.connect(self._apply_local_contact_config) - self._daq.local_contact_transfer_error.connect( - self._show_local_contact_transfer_error - ) + self._daq.local_contact_transfer_error.connect(self._show_local_contact_transfer_error) self._daq.bec_user_macros_loaded.connect(self._show_bec_user_macros) self._daq.bec_devices_loaded.connect(self._show_bec_devices) self._daq.update.connect(self._update_from_status) @@ -162,17 +157,12 @@ class LocalContactPanel(QFrame): def set_active_tab(self, tab_name: str) -> None: for index in range(self._tabs.count()): - if ( - self._tabs.tabText(index).strip().lower() - == str(tab_name).strip().lower() - ): + if self._tabs.tabText(index).strip().lower() == str(tab_name).strip().lower(): self._tabs.setCurrentIndex(index) return logger.warning(f"Unknown Local Contact tab requested: {tab_name}") - def _register_status_widget( - self, widget: LocalContactStatusWidget - ) -> LocalContactStatusWidget: + def _register_status_widget(self, widget: LocalContactStatusWidget) -> LocalContactStatusWidget: self._status_widgets.append(widget) if self._last_status is not None: widget.set_daq_status(self._last_status) @@ -181,17 +171,10 @@ class LocalContactPanel(QFrame): return widget def _make_status_widget( - self, - *, - title: str, - summary: str, - fields: tuple[str, ...], + self, *, title: str, summary: str, fields: tuple[str, ...] ) -> LocalContactStatusWidget: widget = LocalContactStatusWidget( - title=title, - visible_fields=fields, - summary=summary, - parent=self, + title=title, visible_fields=fields, summary=summary, parent=self ) return self._register_status_widget(widget) @@ -263,13 +246,7 @@ class LocalContactPanel(QFrame): self._make_status_widget( title="Recovery status", summary="Recovery-relevant DAQ state.", - fields=( - "beamline_state", - "busy", - "sample", - "tell_connected", - "tell_state", - ), + fields=("beamline_state", "busy", "sample", "tell_connected", "tell_state"), ) ) layout.addWidget(RecoveryPanel(daq=self._daq, parent=tab), 1) @@ -303,37 +280,25 @@ class LocalContactPanel(QFrame): row += 1 grid.addWidget( - self._make_button( - "Unmount", self._daq.unmount, "Requesting sample unmount." - ), - row, - 0, - ) - grid.addWidget( - self._make_button("Dry", self._daq.tell_dry, "Requesting TELL dry."), row, 1 + self._make_button("Unmount", self._daq.unmount, "Requesting sample unmount."), row, 0 ) + grid.addWidget(self._make_button("Dry", self._daq.tell_dry, "Requesting TELL dry."), row, 1) row += 1 grid.addWidget( - self._make_button( - "Park and dry", self._daq.park_and_dry, "Requesting park and dry." - ), + self._make_button("Park and dry", self._daq.park_and_dry, "Requesting park and dry."), row, 0, ) grid.addWidget( - self._make_button( - "Toggle blower", self._daq.tell_toggle_blower, "Toggling blower." - ), + self._make_button("Toggle blower", self._daq.tell_toggle_blower, "Toggling blower."), row, 1, ) row += 1 grid.addWidget(self._make_button("Anneal", self._anneal_from_dialog), row, 0) - grid.addWidget( - self._make_button("TELL access info", self._show_tell_access_info), row, 1 - ) + grid.addWidget(self._make_button("TELL access info", self._show_tell_access_info), row, 1) wrapper = QGroupBox("TELL actions", tab) wrapper_layout = QVBoxLayout(wrapper) @@ -356,14 +321,7 @@ class LocalContactPanel(QFrame): self._make_status_widget( title="BEC status", summary="BEC-focused state and related backend status.", - fields=( - "bec", - "busy", - "beamline_state", - "detector", - "aerotech", - "smargon", - ), + fields=("bec", "busy", "beamline_state", "detector", "aerotech", "smargon"), ) ) @@ -376,9 +334,7 @@ class LocalContactPanel(QFrame): tools_layout.addWidget( self._make_button( - "Load BEC user macros", - self._daq.bec_load_user_macros, - "Loading BEC user macros.", + "Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros." ) ) tools_layout.addWidget( @@ -390,9 +346,7 @@ class LocalContactPanel(QFrame): ) tools_layout.addWidget( self._make_button( - "Show BEC position devices", - self._daq.bec_list_all_devices, - "Listing BEC devices.", + "Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices." ) ) tools_layout.addWidget( @@ -445,15 +399,7 @@ class LocalContactPanel(QFrame): self._make_status_widget( title="Hardware status", summary="Hardware-related backend and beamline status.", - fields=( - "aerotech", - "smargon", - "detector", - "bec", - "tell", - "busy", - "beamline_state", - ), + fields=("aerotech", "smargon", "detector", "bec", "tell", "busy", "beamline_state"), ) ) @@ -508,9 +454,7 @@ class LocalContactPanel(QFrame): "Initialising detector.", ), self._make_button( - "Initialise Smargon", - self._daq.initialise_smargon, - "Initialising Smargon.", + "Initialise Smargon", self._daq.initialise_smargon, "Initialising Smargon." ), self._make_button( "Initialise Aerotech", @@ -555,18 +499,14 @@ class LocalContactPanel(QFrame): grid.addWidget( self._make_button( - "Cancel current state", - self._daq.cancel, - "Cancelling current detector/scan state.", + "Cancel current state", self._daq.cancel, "Cancelling current detector/scan state." ), row, 0, ) grid.addWidget( self._make_button( - "Take pedestal", - self._daq.detector_take_pedestal, - "Requesting detector pedestal.", + "Take pedestal", self._daq.detector_take_pedestal, "Requesting detector pedestal." ), row, 1, @@ -596,10 +536,7 @@ class LocalContactPanel(QFrame): layout.setContentsMargins(6, 6, 6, 6) layout.setSpacing(8) - description = QLabel( - "Staff-only tuning values used by automation and recovery flows.", - tab, - ) + description = QLabel("Staff-only tuning values used by automation and recovery flows.", tab) description.setWordWrap(True) layout.addWidget(description) @@ -622,12 +559,8 @@ class LocalContactPanel(QFrame): form_layout.addWidget(label, 0, 0) form_layout.addWidget(self._mount_to_center_sleep_s, 0, 1) - loop_face_padding_label = QLabel( - "Line scan loop_face Y padding / side", form_box - ) - self._line_scan_loop_face_y_padding_fraction_each_side = QDoubleSpinBox( - form_box - ) + loop_face_padding_label = QLabel("Line scan loop_face Y padding / side", form_box) + self._line_scan_loop_face_y_padding_fraction_each_side = QDoubleSpinBox(form_box) self._line_scan_loop_face_y_padding_fraction_each_side.setRange(0.0, 5.0) self._line_scan_loop_face_y_padding_fraction_each_side.setDecimals(2) self._line_scan_loop_face_y_padding_fraction_each_side.setSingleStep(0.05) @@ -647,13 +580,9 @@ class LocalContactPanel(QFrame): ) form_layout.addWidget(loop_face_padding_label, 1, 0) - form_layout.addWidget( - self._line_scan_loop_face_y_padding_fraction_each_side, 1, 1 - ) + form_layout.addWidget(self._line_scan_loop_face_y_padding_fraction_each_side, 1, 1) form_layout.addWidget(loop_all_padding_label, 2, 0) - form_layout.addWidget( - self._line_scan_loop_all_y_padding_fraction_each_side, 2, 1 - ) + form_layout.addWidget(self._line_scan_loop_all_y_padding_fraction_each_side, 2, 1) button_row = QWidget(tab) button_layout = QHBoxLayout(button_row) @@ -760,12 +689,9 @@ class LocalContactPanel(QFrame): @Slot(list) def _show_bec_user_macros(self, items: list) -> None: if self._bec_macros_dialog is None: - self._bec_macros_dialog = TextListDialog( - title="BEC user macros", parent=self - ) + self._bec_macros_dialog = TextListDialog(title="BEC user macros", parent=self) self._bec_macros_dialog.set_items( - [str(item) for item in items], - empty_message="No BEC user macros found.", + [str(item) for item in items], empty_message="No BEC user macros found." ) self._bec_macros_dialog.show() self._bec_macros_dialog.raise_() @@ -774,12 +700,9 @@ class LocalContactPanel(QFrame): @Slot(list) def _show_bec_devices(self, items: list) -> None: if self._bec_devices_dialog is None: - self._bec_devices_dialog = TextListDialog( - title="BEC position devices", parent=self - ) + self._bec_devices_dialog = TextListDialog(title="BEC position devices", parent=self) self._bec_devices_dialog.set_items( - [str(item) for item in items], - empty_message="No BEC devices found.", + [str(item) for item in items], empty_message="No BEC devices found." ) self._bec_devices_dialog.show() self._bec_devices_dialog.raise_() @@ -797,9 +720,7 @@ class LocalContactPanel(QFrame): url = self._links_payload.get(key) if not url: QMessageBox.information( - self, - "Link unavailable", - f"No configured link is available for {key}.", + self, "Link unavailable", f"No configured link is available for {key}." ) return QDesktopServices.openUrl(QUrl(str(url))) @@ -807,37 +728,23 @@ class LocalContactPanel(QFrame): @Slot() def _show_tell_access_info(self) -> None: message = ( - self._links_payload.get("tell_hint") - or "Please check TELL status via Remmina / VNC." + self._links_payload.get("tell_hint") or "Please check TELL status via Remmina / VNC." ) QMessageBox.information(self, "TELL access", str(message)) def _make_button( - self, - text: str, - callback: Callable[[], None], - log_message: str | None = None, + self, text: str, callback: Callable[[], None], log_message: str | None = None ) -> QPushButton: button = QPushButton(text, self) if log_message is None: button.clicked.connect(callback) else: - button.clicked.connect( - lambda: self._run_logged_action(log_message, callback) - ) + button.clicked.connect(lambda: self._run_logged_action(log_message, callback)) return button @Slot() def _anneal_from_dialog(self) -> None: - seconds, ok = QInputDialog.getDouble( - self, - "Anneal", - "Anneal time (s):", - 1.0, - 0.1, - 60.0, - 1, - ) + seconds, ok = QInputDialog.getDouble(self, "Anneal", "Anneal time (s):", 1.0, 0.1, 60.0, 1) if not ok: return self._daq.anneal(seconds) @@ -853,9 +760,7 @@ class LocalContactPanel(QFrame): self._mount_to_center_sleep_s.blockSignals(True) self._mount_to_center_sleep_s.setValue( - float( - self._local_contact_config_payload.get("mount_to_center_sleep_s", 0.0) - ) + float(self._local_contact_config_payload.get("mount_to_center_sleep_s", 0.0)) ) self._mount_to_center_sleep_s.blockSignals(False) @@ -863,8 +768,7 @@ class LocalContactPanel(QFrame): self._line_scan_loop_face_y_padding_fraction_each_side.setValue( float( self._local_contact_config_payload.get( - "line_scan_loop_face_y_padding_fraction_each_side", - 0.5, + "line_scan_loop_face_y_padding_fraction_each_side", 0.5 ) ) ) @@ -874,8 +778,7 @@ class LocalContactPanel(QFrame): self._line_scan_loop_all_y_padding_fraction_each_side.setValue( float( self._local_contact_config_payload.get( - "line_scan_loop_all_y_padding_fraction_each_side", - 0.5, + "line_scan_loop_all_y_padding_fraction_each_side", 0.5 ) ) ) diff --git a/src/aare/gui/panels/manual_sample_panel.py b/src/aare/gui/panels/manual_sample_panel.py index 64fe051c..873045b2 100644 --- a/src/aare/gui/panels/manual_sample_panel.py +++ b/src/aare/gui/panels/manual_sample_panel.py @@ -1,14 +1,7 @@ from aarecommon.models.models import DAQStatusModel, SampleShortInfo from aareDB import DataCollectionParameters from PySide6.QtCore import Signal, Slot -from PySide6.QtWidgets import ( - QCheckBox, - QGridLayout, - QLabel, - QLineEdit, - QPushButton, - QWidget, -) +from PySide6.QtWidgets import QCheckBox, QGridLayout, QLabel, QLineEdit, QPushButton, QWidget from aare.gui.widgets.number_line_edit import NumberLineEdit from aare.gui.widgets.title_label import TitleLabel diff --git a/src/aare/gui/panels/portrait_mode.py b/src/aare/gui/panels/portrait_mode.py index b3f1e145..8f01449a 100644 --- a/src/aare/gui/panels/portrait_mode.py +++ b/src/aare/gui/panels/portrait_mode.py @@ -3,20 +3,9 @@ from __future__ import annotations import math from aarecommon.config.logger import setup_logger -from aarecommon.models.automation import ( - AutomationProgress, - StepStatus, - WorkflowStateKind, -) +from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal, Slot -from PySide6.QtGui import ( - QColor, - QFont, - QFontMetrics, - QLinearGradient, - QPainter, - QPen, -) +from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen from PySide6.QtWidgets import ( QFrame, QHBoxLayout, @@ -109,10 +98,7 @@ class LEDStages(QWidget): line_color = QColor(ACCENT) if i < self._active else QColor(LED_OFF) pen = QPen(line_color, 2) p.setPen(pen) - p.drawLine( - QPointF(cx + led_r + 3, cy), - QPointF(next_cx - led_r - 3, cy), - ) + p.drawLine(QPointF(cx + led_r + 3, cy), QPointF(next_cx - led_r - 3, cy)) p.setPen(Qt.NoPen) if i < self._active: @@ -144,9 +130,7 @@ class LEDStages(QWidget): fm = QFontMetrics(font) text_w = fm.horizontalAdvance(name) p.drawText( - QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), - Qt.AlignCenter, - name, + QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), Qt.AlignCenter, name ) @@ -197,12 +181,7 @@ class PlayPauseButton(QPushButton): # --------------------------------------------------------------------------- class QueueItemCard(QFrame): def __init__( - self, - index: int | str, - title: str, - subtitle: str, - is_next: bool = False, - parent=None, + self, index: int | str, title: str, subtitle: str, is_next: bool = False, parent=None ): super().__init__(parent) self.setFixedHeight(72) @@ -245,9 +224,7 @@ class QueueItemCard(QFrame): ) title_lbl.setWordWrap(False) sub_lbl = QLabel(subtitle) - sub_lbl.setStyleSheet( - f"color: {SUBTEXT}; font-size: 11px; background: transparent;" - ) + sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; background: transparent;") text_col.addWidget(title_lbl) text_col.addWidget(sub_lbl) layout.addLayout(text_col, stretch=1) @@ -350,9 +327,7 @@ class PortraitModePanel(QWidget): # Camera card — wraps the real compact_sample_camera cam_card = QFrame() - cam_card.setStyleSheet( - f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}" - ) + cam_card.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}") cam_card_layout = QVBoxLayout(cam_card) cam_card_layout.setContentsMargins(4, 4, 4, 4) cam_card_layout.setSpacing(0) @@ -363,9 +338,7 @@ class PortraitModePanel(QWidget): # Sample name labels self._name_lbl = QLabel("—") - self._name_lbl.setStyleSheet( - f"color: {TEXT}; font-size: 18px; font-weight: 700;" - ) + self._name_lbl.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 700;") self._sub_lbl = QLabel("No sample queued") self._sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 12px;") layout.addWidget(self._name_lbl) @@ -377,9 +350,7 @@ class PortraitModePanel(QWidget): # Play/Pause button (single, centred) ctrl_frame = QFrame() - ctrl_frame.setStyleSheet( - f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}" - ) + ctrl_frame.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}") ctrl_layout = QHBoxLayout(ctrl_frame) ctrl_layout.setContentsMargins(12, 12, 12, 12) @@ -497,9 +468,7 @@ class PortraitModePanel(QWidget): layout.addWidget(self._queue_scroll, stretch=1) back_btn = self._accent_button("← BACK TO CAMERA") - back_btn.clicked.connect( - lambda: self._stack.setCurrentWidget(self._player_page) - ) + back_btn.clicked.connect(lambda: self._stack.setCurrentWidget(self._player_page)) layout.addWidget(back_btn) return page @@ -578,12 +547,7 @@ class PortraitModePanel(QWidget): for i, s in enumerate(suggested): name = str(getattr(s, "sample_name", "") or f"Sample {i + 1}") puck = str(getattr(s, "puck_name", "") or "") - card = QueueItemCard( - index=i + 1, - title=name, - subtitle=puck, - is_next=False, - ) + card = QueueItemCard(index=i + 1, title=name, subtitle=puck, is_next=False) self._preview_layout.addWidget(card) return @@ -591,10 +555,7 @@ class PortraitModePanel(QWidget): name = str(getattr(sample, "sample_name", "") or f"Sample {i + 1}") puck = str(getattr(sample, "puck_name", "") or "") card = QueueItemCard( - index="▶" if i == 0 else i + 1, - title=name, - subtitle=puck, - is_next=(i == 0), + index="▶" if i == 0 else i + 1, title=name, subtitle=puck, is_next=(i == 0) ) self._preview_layout.addWidget(card) @@ -636,12 +597,7 @@ class PortraitModePanel(QWidget): for i, sample in enumerate(samples): name = str(getattr(sample, "sample_name", "") or f"Sample {i + 1}") puck = str(getattr(sample, "puck_name", "") or "") - card = QueueItemCard( - index=i + 1, - title=name, - subtitle=puck, - is_next=False, - ) + card = QueueItemCard(index=i + 1, title=name, subtitle=puck, is_next=False) self._queue_inner_layout.addWidget(card) # ------------------------------------------------------------------ @@ -727,9 +683,7 @@ class PortraitModePanel(QWidget): self._alert_toast.setVisible(False) self._alert_toast_label.clear() - def _flush_portrait_alerts_to_banners( - self, primary_banner, secondary_banner - ) -> None: + def _flush_portrait_alerts_to_banners(self, primary_banner, secondary_banner) -> None: """ Called when returning to main view — replay any error alerts that arrived during portrait mode so the operator doesn't miss them. @@ -762,27 +716,17 @@ class PortraitModePanel(QWidget): return # Timeout protection - if ( - self._loop_restart_deadline is not None - and self._loop_restart_deadline.hasExpired() - ): + if self._loop_restart_deadline is not None and self._loop_restart_deadline.hasExpired(): self._loop_restart_timer.stop() self._loop_enabled = False self._loop_btn.setChecked(False) - self.show_portrait_alert( - "Loop stopped: beamline remained busy too long", - True, - ) + self.show_portrait_alert("Loop stopped: beamline remained busy too long", True) return # Still busy, wait - if getattr( - self._job_list_panel, - "_SampleQueuePanel__busy", - False, - ): + if getattr(self._job_list_panel, "_SampleQueuePanel__busy", False): return # Safe to restart @@ -805,10 +749,7 @@ class PortraitModePanel(QWidget): if self._loop_remaining <= 0: return False - self._job_list_panel.queue_samples( - list(self._loop_samples), - replace=True, - ) + self._job_list_panel.queue_samples(list(self._loop_samples), replace=True) self._loop_remaining -= 1 @@ -847,7 +788,4 @@ class PortraitModePanel(QWidget): ) if ordered: - self._job_list_panel.queue_samples( - ordered, - replace=True, - ) + self._job_list_panel.queue_samples(ordered, replace=True) diff --git a/src/aare/gui/panels/prediction_metrics_panel.py b/src/aare/gui/panels/prediction_metrics_panel.py index 50de44e6..7ac54152 100644 --- a/src/aare/gui/panels/prediction_metrics_panel.py +++ b/src/aare/gui/panels/prediction_metrics_panel.py @@ -137,8 +137,7 @@ class ConfidenceHistogramWidget(QWidget): self.chart.legend().setVisible(False) categories = [ - f"{self.BINS[i]:.1f}-{self.BINS[i + 1]:.1f}" - for i in range(len(self.BINS) - 1) + f"{self.BINS[i]:.1f}-{self.BINS[i + 1]:.1f}" for i in range(len(self.BINS) - 1) ] self.axis_x = QBarCategoryAxis() self.axis_x.append(categories) @@ -216,9 +215,7 @@ class ObjectCountWidget(QWidget): # Count label with matching color count_label = QLabel("0") - count_label.setStyleSheet( - f"color: {color}; font-size: 14px; font-weight: bold;" - ) + count_label.setStyleSheet(f"color: {color}; font-size: 14px; font-weight: bold;") count_label.setAlignment(Qt.AlignmentFlag.AlignRight) row = i // 2 @@ -289,11 +286,7 @@ class TimingStatsWidget(QWidget): layout.addWidget(group) def update_stats( - self, - frame_time_ms: float, - fps: float, - detection_count: int, - mean_confidence: float, + self, frame_time_ms: float, fps: float, detection_count: int, mean_confidence: float ): """Update timing statistics display.""" self.frame_time_label.setText(f"{frame_time_ms:.1f} ms") @@ -650,10 +643,7 @@ class PredictionMetricsPanel(QWidget): super().showEvent(event) if self._refresh_timer is not None and not self._refresh_timer.isActive(): self._refresh_timer.start() - if ( - self._chart_refresh_timer is not None - and not self._chart_refresh_timer.isActive() - ): + if self._chart_refresh_timer is not None and not self._chart_refresh_timer.isActive(): self._chart_refresh_timer.start() self._dirty = True @@ -758,9 +748,7 @@ class PredictionMetricsPanel(QWidget): # Update timing stats avg_frame_time = ( - sum(self._frame_times) / len(self._frame_times) - if self._frame_times - else 0.0 + sum(self._frame_times) / len(self._frame_times) if self._frame_times else 0.0 ) fps = 1000.0 / avg_frame_time if avg_frame_time > 0 else 0.0 @@ -781,10 +769,7 @@ class PredictionMetricsPanel(QWidget): return path, _ = QFileDialog.getSaveFileName( - self, - "Export Prediction Metrics", - "prediction_metrics.csv", - "CSV files (*.csv)", + self, "Export Prediction Metrics", "prediction_metrics.csv", "CSV files (*.csv)" ) if not path: return diff --git a/src/aare/gui/panels/raster_data_collection.py b/src/aare/gui/panels/raster_data_collection.py index 1a4199c5..4027317a 100644 --- a/src/aare/gui/panels/raster_data_collection.py +++ b/src/aare/gui/panels/raster_data_collection.py @@ -36,9 +36,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): default_transmission: float = 1.0, parent=None, ): - super().__init__( - diffraction, raster_mgr.active_grid.dtz, default_transmission, parent - ) + super().__init__(diffraction, raster_mgr.active_grid.dtz, default_transmission, parent) self._previous_sample_was_none_raster = True @@ -71,11 +69,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("Image time", parent=self), 4, 0) self.image_time_enter = DbOverrideLineEdit( - 0.0005, - 10.0, - default=raster_mgr.active_grid.exp_time_s, - decimals=4, - parent=self, + 0.0005, 10.0, default=raster_mgr.active_grid.exp_time_s, decimals=4, parent=self ) self._layout.addWidget(self.image_time_enter, 4, 1, 1, 3) self._layout.addWidget(QLabel("s", parent=self), 4, 4) @@ -110,26 +104,18 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("Metric", parent=self), 7, 0) self.metric_combo.addItem("Raster score", RasterGridMetric.RASTER_SCORE) - self.metric_combo.addItem( - "Spot count (low res.)", RasterGridMetric.SPOTS_LOW_RES - ) + self.metric_combo.addItem("Spot count (low res.)", RasterGridMetric.SPOTS_LOW_RES) self.metric_combo.addItem("Spot count", RasterGridMetric.SPOTS) - self.metric_combo.addItem( - "Spot count (indexed)", RasterGridMetric.SPOTS_INDEXED - ) + self.metric_combo.addItem("Spot count (indexed)", RasterGridMetric.SPOTS_INDEXED) self.metric_combo.addItem("Spot count (ice)", RasterGridMetric.SPOTS_ICE) - self.metric_combo.addItem( - "Spot ratio (ice/low res.)", RasterGridMetric.SPOTS_ICE_LOW_RES - ) + self.metric_combo.addItem("Spot ratio (ice/low res.)", RasterGridMetric.SPOTS_ICE_LOW_RES) self.metric_combo.addItem("Background estimate", RasterGridMetric.BKG) self.metric_combo.addItem("Indexing result", RasterGridMetric.INDEXING) self.metric_combo.addItem("Profile Radius", RasterGridMetric.PR) self.metric_combo.addItem("B-factor", RasterGridMetric.BFACTOR) self.metric_combo.addItem("Resolution (ML est.)", RasterGridMetric.RES) - self.metric_combo.setCurrentIndex( - self.metric_combo.findData(RasterGridMetric.RASTER_SCORE) - ) + self.metric_combo.setCurrentIndex(self.metric_combo.findData(RasterGridMetric.RASTER_SCORE)) self.metric_combo.currentIndexChanged.connect(self.metric_changed) self._layout.addWidget(self.metric_combo, 7, 1, 1, 3) @@ -138,9 +124,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self) slider.setRange(0, 255) slider.setValue(127) - slider.valueChanged.connect( - lambda: self.raster_alpha_changed.emit(255 - slider.value()) - ) + slider.valueChanged.connect(lambda: self.raster_alpha_changed.emit(255 - slider.value())) self._layout.addWidget(slider, 8, 1, 1, 3) self._table = RasterGridTable(raster_mgr) @@ -153,9 +137,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("Measurement time", parent=self), 11, 0) self.total_time = QLabel(f"{self.__total_time} min 0 s") - self.total_time.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.total_time, 11, 1, 1, 3) self.calculate_total_time() @@ -185,9 +167,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self.calculate_total_time() @Slot(int, int, float, float) - def grid_scan_size_change( - self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float - ): + def grid_scan_size_change(self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float): self.__size_x = size_x_mm * 1000.0 self.__size_y = size_y_mm * 1000.0 self.__n_x = n_x @@ -235,7 +215,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): def get_parameter_mappings(self): """Return raster-specific parameter mappings.""" return [ - ("exposure", self.image_time_enter, None), + ("exposure", self.image_time_enter, None) # Add other raster-specific parameters here as needed ] @@ -251,9 +231,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): if self.__n_x <= 0 or self.__n_y <= 0 or self.image_time_enter.value < 0: self.__total_time = 0.0 else: - self.__total_time = ( - self.__n_x * self.__n_y * self.image_time_enter.value * 1.3 - ) + self.__total_time = self.__n_x * self.__n_y * self.image_time_enter.value * 1.3 # 30% buffer added # Show minutes self.update_total_time_label() @@ -261,12 +239,8 @@ class RasterDataCollectionPanel(ScanSettingsPanel): @Slot() def _on_evaluate_clicked(self): if self.__beamline_state != BeamlineStateEnum.SampleAlignment: - logger.error( - f"Beamline state {self.__beamline_state} is not Sample Alignment" - ) - QMessageBox.critical( - None, "Error", "Beamline state is not Sample Alignment" - ) + logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment") + QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment") return if self.check_before_run(scan_kind="raster"): self.evaluate_grid.emit() @@ -274,12 +248,8 @@ class RasterDataCollectionPanel(ScanSettingsPanel): @Slot() def _on_evaluate_auto_clicked(self): if self.__beamline_state != BeamlineStateEnum.SampleAlignment: - logger.error( - f"Beamline state {self.__beamline_state} is not Sample Alignment" - ) - QMessageBox.critical( - None, "Error", "Beamline state is not Sample Alignment" - ) + logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment") + QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment") return if self.check_before_run(scan_kind="raster"): self.evaluate_grid_auto.emit() diff --git a/src/aare/gui/panels/reference_tools_panel.py b/src/aare/gui/panels/reference_tools_panel.py index 4933ceb7..ba85b143 100644 --- a/src/aare/gui/panels/reference_tools_panel.py +++ b/src/aare/gui/panels/reference_tools_panel.py @@ -2,11 +2,7 @@ from typing import Optional from aarecommon.config.logger import setup_logger -from aarecommon.models.models import ( - DAQStatusModel, - SampleShortInfo, - SampleShortInfoList, -) +from aarecommon.models.models import DAQStatusModel, SampleShortInfo, SampleShortInfoList from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, Slot from PySide6.QtGui import QBrush, QColor from PySide6.QtWidgets import ( @@ -96,11 +92,7 @@ class ReferenceToolsModel(QAbstractTableModel): if role != Qt.ItemDataRole.DisplayRole: return None if orientation == Qt.Orientation.Horizontal: - return ( - self.header[section] - if section < len(self.header) - else f"Column {section + 1}" - ) + return self.header[section] if section < len(self.header) else f"Column {section + 1}" return str(section + 1) def update_rows(self, rows: list[SampleShortInfo]): @@ -137,16 +129,12 @@ class ReferenceToolsModel(QAbstractTableModel): elif self.__sort_col == 2: # Numeric sort for Mount count; place None last on ascending, first on descending none_sentinel = ( - float("inf") - if self.__sort_order == Qt.SortOrder.AscendingOrder - else float("-inf") + float("inf") if self.__sort_order == Qt.SortOrder.AscendingOrder else float("-inf") ) self.__sorted_samples = sorted( self.samples, key=lambda row: ( - row.mount_count - if isinstance(row.mount_count, (int, float)) - else none_sentinel + row.mount_count if isinstance(row.mount_count, (int, float)) else none_sentinel ), reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder), ) @@ -220,9 +208,7 @@ class ReferenceToolsPanel(QFrame): # initialize model with provided samples self.table_model = ReferenceToolsModel(rows=samples.s) self.table_view.setModel(self.table_model) - self.table_view.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeMode.Stretch - ) + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers) logger.debug("Setting up table header") @@ -237,9 +223,7 @@ class ReferenceToolsPanel(QFrame): logger.debug("Setting up table view context menu") self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table_view.customContextMenuRequested.connect(self._context_menu) - self.table_view.setSelectionBehavior( - QAbstractItemView.SelectionBehavior.SelectRows - ) + self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection) def _selected_item(self) -> Optional[SampleShortInfo]: diff --git a/src/aare/gui/panels/rotation_data_collection.py b/src/aare/gui/panels/rotation_data_collection.py index 83fc9e2e..bda9cc79 100644 --- a/src/aare/gui/panels/rotation_data_collection.py +++ b/src/aare/gui/panels/rotation_data_collection.py @@ -8,11 +8,7 @@ from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtWidgets import QComboBox, QLabel, QMessageBox, QPushButton from aare.gui.panels.scan_settings_panel import ScanSettingsPanel -from aare.gui.widgets.number_line_edit import ( - CheckedLineEdit, - DbOverrideLineEdit, - NumberLineEdit, -) +from aare.gui.widgets.number_line_edit import CheckedLineEdit, DbOverrideLineEdit, NumberLineEdit logger = setup_logger("aareGUI") @@ -69,36 +65,22 @@ class RotationDataCollectionPanel(ScanSettingsPanel): ) self._layout.addWidget(QLabel("Image angle", parent=self), 5, 0) - self.screening_image_angle = NumberLineEdit( - 0, 90.0, 0.5, decimals=3, parent=self - ) + self.screening_image_angle = NumberLineEdit(0, 90.0, 0.5, decimals=3, parent=self) self._layout.addWidget(self.screening_image_angle, 5, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 5, 4) self._layout.addWidget(QLabel("Image time", parent=self), 6, 0) - self.screening_image_time_enter = NumberLineEdit( - 0.0005, 10.0, 0.1, decimals=4, parent=self - ) + self.screening_image_time_enter = NumberLineEdit(0.0005, 10.0, 0.1, decimals=4, parent=self) self._layout.addWidget(self.screening_image_time_enter, 6, 1, 1, 3) self._layout.addWidget(QLabel("s", parent=self), 6, 4) self.screening_type = QComboBox(parent=self) self.screening_type.addItem("1 image", {"steps": 1, "omega_step_deg": 0}) - self.screening_type.addItem( - "2 images every 90°", {"steps": 2, "omega_step_deg": 90} - ) - self.screening_type.addItem( - "4 images every 90°", {"steps": 4, "omega_step_deg": 90} - ) - self.screening_type.addItem( - "3 images every 60°", {"steps": 3, "omega_step_deg": 60} - ) - self.screening_type.addItem( - "2 images every 45°", {"steps": 2, "omega_step_deg": 45} - ) - self.screening_type.addItem( - "4 images every 45°", {"steps": 4, "omega_step_deg": 45} - ) + self.screening_type.addItem("2 images every 90°", {"steps": 2, "omega_step_deg": 90}) + self.screening_type.addItem("4 images every 90°", {"steps": 4, "omega_step_deg": 90}) + self.screening_type.addItem("3 images every 60°", {"steps": 3, "omega_step_deg": 60}) + self.screening_type.addItem("2 images every 45°", {"steps": 2, "omega_step_deg": 45}) + self.screening_type.addItem("4 images every 45°", {"steps": 4, "omega_step_deg": 45}) self._layout.addWidget(self.screening_type, 7, 0, 1, 6) @@ -112,17 +94,13 @@ class RotationDataCollectionPanel(ScanSettingsPanel): ) self._layout.addWidget(QLabel("Total angle", parent=self), 10, 0) - self.total_angle = DbOverrideLineEdit( - 0, 9999.0, default=360.0, decimals=3, parent=self - ) + self.total_angle = DbOverrideLineEdit(0, 9999.0, default=360.0, decimals=3, parent=self) self._layout.addWidget(self.total_angle, 10, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 10, 4) self._register_override_field(self.total_angle) self._layout.addWidget(QLabel("Image angle", parent=self), 11, 0) - self.image_angle = DbOverrideLineEdit( - 0, 10.0, default=0.2, decimals=3, parent=self - ) + self.image_angle = DbOverrideLineEdit(0, 10.0, default=0.2, decimals=3, parent=self) self._layout.addWidget(self.image_angle, 11, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 11, 4) self._register_override_field(self.image_angle) @@ -137,9 +115,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("Total measurement time", parent=self), 13, 0) self.total_time = QLabel(f"{self.__total_time} min 0 s") - self.total_time.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.total_time, 13, 1, 1, 3) self.total_angle.valueChanged.connect(self.calculate_measurement_time) @@ -150,9 +126,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("Dose", parent=self), 14, 0) self.dose = QLabel(f"{self.__dose_mgy}") - self.dose.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.dose.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.dose, 14, 1, 1, 3) self._layout.addWidget(QLabel("MGy", parent=self), 14, 4) @@ -168,12 +142,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel): @Slot() def run_screening(self): if self.__beamline_state != BeamlineStateEnum.SampleAlignment: - logger.error( - f"Beamline state {self.__beamline_state} is not Sample Alignment" - ) - QMessageBox.critical( - None, "Error", "Beamline state is not Sample Alignment" - ) + logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment") + QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment") return if not self.check_before_run(scan_kind="screening"): logger.error("Cannot run measurement because of check") @@ -181,8 +151,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): screening_settings = self.screening_type.currentData() is_single_image = ( - screening_settings["steps"] == 1 - and screening_settings["omega_step_deg"] == 0 + screening_settings["steps"] == 1 and screening_settings["omega_step_deg"] == 0 ) if is_single_image: @@ -216,12 +185,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel): @Slot() def run_measurement(self): if self.__beamline_state != BeamlineStateEnum.SampleAlignment: - logger.error( - f"Beamline state {self.__beamline_state} is not Sample Alignment" - ) - QMessageBox.critical( - None, "Error", "Beamline state is not Sample Alignment" - ) + logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment") + QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment") return if not self.check_before_run(scan_kind="rotation"): logger.error("Cannot run measurement because of check") @@ -318,9 +283,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): beam_area = s.geom.beam_size_mm.x * s.geom.beam_size_mm.y * 1e6 time = self.image_number() * self.image_time_enter.value - self.__dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / ( - beam_area * kdose - ) + self.__dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / (beam_area * kdose) self.dose.setText(f"{(self.__dose_mgy / 1e6):.1f}") self.__beamline_state = s.state diff --git a/src/aare/gui/panels/samcam_panel.py b/src/aare/gui/panels/samcam_panel.py index 51c78645..959b7ca6 100644 --- a/src/aare/gui/panels/samcam_panel.py +++ b/src/aare/gui/panels/samcam_panel.py @@ -43,9 +43,7 @@ class SamcamPanel(QWidget): self.exposure_spinbox = QDoubleSpinBox() self.exposure_spinbox.setRange(0, 1.0) # Adjust range as needed self.exposure_spinbox.setSingleStep(0.001) - self.exposure_spinbox.setStyleSheet( - "QDoubleSpinBox { background-color: white; }" - ) + self.exposure_spinbox.setStyleSheet("QDoubleSpinBox { background-color: white; }") self.exposure_spinbox.setDecimals(3) self.exposure_spinbox.valueChanged.connect(self.__changed) @@ -67,17 +65,13 @@ class SamcamPanel(QWidget): # Persist the current gain/exposure as the beam-location preset for the # current zoom (only meaningful in beam-location mode). self.save_beam_location_button = QPushButton("Save samcam settings (bl)") - self.save_beam_location_button.clicked.connect( - self.save_beam_location_setting.emit - ) + self.save_beam_location_button.clicked.connect(self.save_beam_location_setting.emit) screenshot_filename_layout = QHBoxLayout() screenshot_filename_label = QLabel("Filename:") self.screenshot_filename_edit = QLineEdit() self.screenshot_filename_edit.setPlaceholderText("optional") - self.screenshot_filename_edit.setStyleSheet( - "QLineEdit { background-color: white; }" - ) + self.screenshot_filename_edit.setStyleSheet("QLineEdit { background-color: white; }") screenshot_filename_layout.addWidget(screenshot_filename_label) screenshot_filename_layout.addWidget(self.screenshot_filename_edit) @@ -85,9 +79,7 @@ class SamcamPanel(QWidget): screenshot_message_label = QLabel("Message:") self.screenshot_message_edit = QLineEdit() self.screenshot_message_edit.setPlaceholderText("optional") - self.screenshot_message_edit.setStyleSheet( - "QLineEdit { background-color: white; }" - ) + self.screenshot_message_edit.setStyleSheet("QLineEdit { background-color: white; }") screenshot_message_layout.addWidget(screenshot_message_label) screenshot_message_layout.addWidget(self.screenshot_message_edit) @@ -114,9 +106,7 @@ class SamcamPanel(QWidget): target_point_layout = QHBoxLayout() self.show_target_point_checkbox = QCheckBox("Show target point") self.show_target_point_checkbox.setChecked(True) - self.show_target_point_checkbox.toggled.connect( - self.show_target_point_changed.emit - ) + self.show_target_point_checkbox.toggled.connect(self.show_target_point_changed.emit) target_point_layout.addWidget(self.show_target_point_checkbox) # Show target coordinates checkbox @@ -132,9 +122,7 @@ class SamcamPanel(QWidget): legend_layout = QHBoxLayout() self.show_overlay_legend_checkbox = QCheckBox("Show overlay legend") self.show_overlay_legend_checkbox.setChecked(True) - self.show_overlay_legend_checkbox.toggled.connect( - self.show_overlay_legend_changed.emit - ) + self.show_overlay_legend_checkbox.toggled.connect(self.show_overlay_legend_changed.emit) legend_layout.addWidget(self.show_overlay_legend_checkbox) # Compact legend checkbox @@ -152,9 +140,7 @@ class SamcamPanel(QWidget): self.target_color_combo = QComboBox() self.target_color_combo.addItems(["Cyan", "Dark Blue", "Dark Red"]) self.target_color_combo.setCurrentText("Cyan") - self.target_color_combo.currentTextChanged.connect( - self.target_color_changed.emit - ) + self.target_color_combo.currentTextChanged.connect(self.target_color_changed.emit) target_color_layout.addWidget(target_color_label) target_color_layout.addWidget(self.target_color_combo) @@ -183,8 +169,7 @@ class SamcamPanel(QWidget): def __request_screenshot(self): self.screenshot_requested.emit( - self.screenshot_filename_edit.text(), - self.screenshot_message_edit.text(), + self.screenshot_filename_edit.text(), self.screenshot_message_edit.text() ) def apply_overlay_settings( diff --git a/src/aare/gui/panels/sample_queue_panel.py b/src/aare/gui/panels/sample_queue_panel.py index ed0087cb..846cd5f7 100644 --- a/src/aare/gui/panels/sample_queue_panel.py +++ b/src/aare/gui/panels/sample_queue_panel.py @@ -37,10 +37,7 @@ class SampleQueuePanel(QFrame): step_through_changed = Signal(bool) def __init__( - self, - parent=None, - samples: SampleShortInfoList | None = None, - show_user: bool = False, + self, parent=None, samples: SampleShortInfoList | None = None, show_user: bool = False ): super().__init__(parent) self.__baton_holder = None @@ -76,25 +73,17 @@ class SampleQueuePanel(QFrame): header = self.table_view.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self.table_view.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding - ) + self.table_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.table_view.setAcceptDrops(True) self.table_view.setDropIndicatorShown(True) self.table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) - self.delete_shortcut = QShortcut( - QKeySequence.StandardKey.Delete, self.table_view - ) + self.delete_shortcut = QShortcut(QKeySequence.StandardKey.Delete, self.table_view) self.delete_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) self.delete_shortcut.activated.connect(self.remove_selected_samples) - self.run_pause_shortcut = QShortcut( - QKeySequence(Qt.Key.Key_Space), self.table_view - ) - self.run_pause_shortcut.setContext( - Qt.ShortcutContext.WidgetWithChildrenShortcut - ) + self.run_pause_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self.table_view) + self.run_pause_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) self.run_pause_shortcut.activated.connect(self.run) layout.addWidget(self.table_view) @@ -109,9 +98,7 @@ class SampleQueuePanel(QFrame): self.clear_button = QPushButton("✖ Clear list", self) self.clear_button.clicked.connect(self.clear) - self.park_and_dry_when_cleared = QCheckBox( - "Park and dry when automation finishes", self - ) + self.park_and_dry_when_cleared = QCheckBox("Park and dry when automation finishes", self) self.park_and_dry_when_cleared.setChecked(True) self.pause_on_conditions_cb = QCheckBox( @@ -160,9 +147,7 @@ class SampleQueuePanel(QFrame): next_next_sample = samples[2] if len(samples) > 2 else None return current, next_sample, next_next_sample - def queue_samples( - self, samples: list[SampleShortInfo], replace: bool = True - ) -> None: + def queue_samples(self, samples: list[SampleShortInfo], replace: bool = True) -> None: if replace: self.table_model.updateData(list(samples)) else: @@ -229,11 +214,7 @@ class SampleQueuePanel(QFrame): """Beamline conditions that currently block/should pause automation.""" problems: list[str] = [] if self.ring_current is None or self.ring_current < LOW_CURRENT_THRESHOLD: - rc = ( - "unknown" - if self.ring_current is None - else f"{round(self.ring_current, 2)} mA" - ) + rc = "unknown" if self.ring_current is None else f"{round(self.ring_current, 2)} mA" problems.append(f"beam (ring current {rc})") if not self._experiment_shutter_state: problems.append("experiment shutter closed") @@ -263,9 +244,7 @@ class SampleQueuePanel(QFrame): self.__warning_msg_box = QMessageBox(self) self.__warning_msg_box.setIcon(QMessageBox.Icon.Warning) self.__warning_msg_box.setWindowTitle("TELL Warning") - self.__warning_msg_box.setText( - "TELL reported a warning. Please see console for details." - ) + self.__warning_msg_box.setText("TELL reported a warning. Please see console for details.") self.__warning_msg_box.setInformativeText( "Automation paused for 10 minutes to allow sufficient dry time." "\nClick 'Continue Now' to resume immediately, or wait for auto-resume." @@ -340,14 +319,11 @@ class SampleQueuePanel(QFrame): return if self.__busy: - logger.error( - f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}" - ) + logger.error(f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}") self.show_error_dialog( title="Beamline is busy", msg="Cannot run automation while beamline is busy", - info="Please wait until beamline is idle " - "or contact your local contact for support", + info="Please wait until beamline is idle or contact your local contact for support", ) return @@ -385,9 +361,7 @@ class SampleQueuePanel(QFrame): if checks_enabled: bad = self._bad_conditions() if bad: - logger.warning( - f"Cannot start automation; beamline not ready: {bad}" - ) + logger.warning(f"Cannot start automation; beamline not ready: {bad}") self.show_error_dialog( title="Beamline not ready", msg="Cannot start automation:\n- " + "\n- ".join(bad), @@ -450,9 +424,7 @@ class SampleQueuePanel(QFrame): msg = "Automation paused — beamline not ready:\n- " + "\n- ".join(bad) if conditions_auto_check(self, msg, self._conditions_ok): - logger.debug( - "Conditions recovered or user chose to continue; resuming automation" - ) + logger.debug("Conditions recovered or user chose to continue; resuming automation") self.table_model.set_running(True) self.__pause = False self.play_button.setText("⏸ Pause") diff --git a/src/aare/gui/panels/scan_settings_panel.py b/src/aare/gui/panels/scan_settings_panel.py index 34c4ad08..ce71e80a 100644 --- a/src/aare/gui/panels/scan_settings_panel.py +++ b/src/aare/gui/panels/scan_settings_panel.py @@ -103,13 +103,9 @@ class ScanSettingsPanel(QWidget): self._register_override_field(self.transmission_enter) self.reload_params_button = QPushButton("Reload DB params") - self.reload_params_button.setToolTip( - "Reload data collection parameters from database" - ) + self.reload_params_button.setToolTip("Reload data collection parameters from database") self.reload_params_button.clicked.connect(self.reload_parameters) - self.reload_params_button.setVisible( - False - ) # Child classes should make it visible + self.reload_params_button.setVisible(False) # Child classes should make it visible # -- source toggle ----------------------------------------------------- def _build_source_toggle(self) -> QWidget: @@ -176,8 +172,7 @@ class ScanSettingsPanel(QWidget): self._experiment_shutter_state = s.bl.exp_shutter_open self._door_prohibited = getattr(s.bl, "pss_prohibited", None) can_edit = (not s.busy) and ( - s.session.session - in (SessionsStateEnum.OwnedByYou, SessionsStateEnum.PendingElseToYou) + s.session.session in (SessionsStateEnum.OwnedByYou, SessionsStateEnum.PendingElseToYou) ) self._can_edit_params = can_edit # Lock/unlock the override fields @@ -187,9 +182,7 @@ class ScanSettingsPanel(QWidget): # Update sample and parameters if s.sample is not None: self._sample = s.sample - self._params = ( - s.sample.aaredb_params if hasattr(s.sample, "aaredb_params") else None - ) + self._params = s.sample.aaredb_params if hasattr(s.sample, "aaredb_params") else None # Enable reload button only if sample has parameters self._previous_sample_was_none = False self.reload_params_button.setEnabled(self._params is not None) @@ -304,9 +297,7 @@ class ScanSettingsPanel(QWidget): # Handle transmission with conversion (common to all panels) if (transmission := getattr(self._params, "transmission", None)) is not None: - transmission_value = ( - transmission / 100.0 if transmission > 1.0 else transmission - ) + transmission_value = transmission / 100.0 if transmission > 1.0 else transmission self._transmission = transmission_value self.transmission_enter.set_db_value(transmission_value) diff --git a/src/aare/gui/panels/smargon_panel.py b/src/aare/gui/panels/smargon_panel.py index 9bd66726..61c3eceb 100644 --- a/src/aare/gui/panels/smargon_panel.py +++ b/src/aare/gui/panels/smargon_panel.py @@ -45,9 +45,7 @@ class SmargonMoveWidget(QWidget): @Slot(dict) def smargon_button(self, payload: dict): - self.smargon_rel.emit( - Coordinate(x=payload["x"], y=payload["y"], z=payload["z"]) - ) + self.smargon_rel.emit(Coordinate(x=payload["x"], y=payload["y"], z=payload["z"])) class SmargonPanel(QWidget): @@ -96,9 +94,7 @@ class SmargonPanel(QWidget): @Slot() def home(self): # TODO move SMARGON_HOME to REDIS, allow GUI to read this value - self.smargon.emit( - SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0) - ) + self.smargon.emit(SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0)) @Slot(float) def phi(self, f: float): @@ -120,7 +116,5 @@ class SmargonPanel(QWidget): return self.smargon.emit( - SmargonCoordinate( - sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0) - ) + SmargonCoordinate(sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0)) ) diff --git a/src/aare/gui/panels/smargon_trace_panel.py b/src/aare/gui/panels/smargon_trace_panel.py index 7f031cc4..e17c6d10 100644 --- a/src/aare/gui/panels/smargon_trace_panel.py +++ b/src/aare/gui/panels/smargon_trace_panel.py @@ -150,8 +150,7 @@ class SmargonTracePanel(QWidget): self._last_distances = [] self._last_lengths = [] self._status.setText( - "No trace file found. Tried: " - + ", ".join(str(p) for p in self._candidate_paths()) + "No trace file found. Tried: " + ", ".join(str(p) for p in self._candidate_paths()) ) self._summary.setText( f"Home position: " @@ -214,8 +213,7 @@ class SmargonTracePanel(QWidget): dz = [value * unit_scale for value in dz_mm] distances_mm = [ - sqrt(dx0**2 + dy0**2 + dz0**2) - for dx0, dy0, dz0 in zip(dx_mm, dy_mm, dz_mm) + sqrt(dx0**2 + dy0**2 + dz0**2) for dx0, dy0, dz0 in zip(dx_mm, dy_mm, dz_mm) ] distances = [value * unit_scale for value in distances_mm] @@ -340,49 +338,49 @@ class SmargonTracePanel(QWidget): unit_name, unit_scale = self._unit_settings() file_path, _ = QFileDialog.getSaveFileName( - self, - "Export Smargon Trace Table", - "smargon_trace_export.csv", - "CSV Files (*.csv)", + self, "Export Smargon Trace Table", "smargon_trace_export.csv", "CSV Files (*.csv)" ) if not file_path: return with open(file_path, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) - writer.writerow([ - "point", - "timestamp", - "event", - "sample_id", - f"shx_{unit_name}", - f"shy_{unit_name}", - f"shz_{unit_name}", - f"distance_{unit_name}", - f"length_{unit_name}", - "omega_deg", - "phi_deg", - "chi_deg", - ]) + writer.writerow( + [ + "point", + "timestamp", + "event", + "sample_id", + f"shx_{unit_name}", + f"shy_{unit_name}", + f"shz_{unit_name}", + f"distance_{unit_name}", + f"length_{unit_name}", + "omega_deg", + "phi_deg", + "chi_deg", + ] + ) for idx, (row, distance_value, length_value) in enumerate( - zip(self._last_rows, self._last_distances, self._last_lengths), - start=1, + zip(self._last_rows, self._last_distances, self._last_lengths), start=1 ): - writer.writerow([ - idx, - row["timestamp"], - row["event"], - row["sample_id"], - float(row["shx_mm"]) * unit_scale, - float(row["shy_mm"]) * unit_scale, - float(row["shz_mm"]) * unit_scale, - distance_value, - length_value, - row["omega_deg"], - row["phi_deg"], - row["chi_deg"], - ]) + writer.writerow( + [ + idx, + row["timestamp"], + row["event"], + row["sample_id"], + float(row["shx_mm"]) * unit_scale, + float(row["shy_mm"]) * unit_scale, + float(row["shz_mm"]) * unit_scale, + distance_value, + length_value, + row["omega_deg"], + row["phi_deg"], + row["chi_deg"], + ] + ) def _redraw_plots( self, @@ -431,7 +429,13 @@ class SmargonTracePanel(QWidget): ax.legend(loc="best") elif plot_name == "distance": - ax.plot(x, distances, marker="o", color=self.COLOR_DISTANCE, label=f"Total distance [{unit_name}]") + ax.plot( + x, + distances, + marker="o", + color=self.COLOR_DISTANCE, + label=f"Total distance [{unit_name}]", + ) ax.set_title("Total distance from home") ax.set_ylabel(f"Distance [{unit_name}]") ax.grid(True, alpha=0.3) @@ -451,9 +455,9 @@ class SmargonTracePanel(QWidget): unit_name: str, unit_scale: float, ) -> None: - recent_rows = rows[-self.TABLE_MAX_ROWS:] - recent_distances = distances[-self.TABLE_MAX_ROWS:] - recent_lengths = lengths[-self.TABLE_MAX_ROWS:] + recent_rows = rows[-self.TABLE_MAX_ROWS :] + recent_distances = distances[-self.TABLE_MAX_ROWS :] + recent_lengths = lengths[-self.TABLE_MAX_ROWS :] self._table.setRowCount(len(recent_rows)) self._table.setHorizontalHeaderLabels( @@ -580,12 +584,7 @@ class SmargonTracePanel(QWidget): return sqrt(beam_x**2 + beam_y**2) def _smargon_nudge_basis( - self, - *, - axis: str, - omega_deg: float, - phi_deg: float, - chi_deg: float, + self, *, axis: str, omega_deg: float, phi_deg: float, chi_deg: float ) -> np.ndarray: phi = np.radians(np.around(phi_deg, decimals=1)) chi = np.radians(np.around(chi_deg, decimals=1)) @@ -637,4 +636,4 @@ class SmargonTracePanel(QWidget): ax.set_xticks([]) ax.set_yticks([]) self._figure.tight_layout() - self._canvas.draw_idle() \ No newline at end of file + self._canvas.draw_idle() diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py index 156e6d04..e0c2442b 100644 --- a/src/aare/gui/panels/smart_rotation_panel.py +++ b/src/aare/gui/panels/smart_rotation_panel.py @@ -4,14 +4,7 @@ from aarecommon.config.logger import setup_logger from aarecommon.models.models import CrystalSize, DAQStatusModel, SimpleScanParameters from aarecommon.models.rotation_scan import RotationScanRequest from PySide6.QtCore import Qt, Signal, Slot -from PySide6.QtWidgets import ( - QGridLayout, - QLabel, - QPushButton, - QSizePolicy, - QSpacerItem, - QWidget, -) +from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton, QSizePolicy, QSpacerItem, QWidget from aare.gui.panels.rotation_data_collection import add_data_to_path from aare.gui.widgets.number_line_edit import NumberLineEdit @@ -52,17 +45,13 @@ class SimpleRotationSettingsPanel(QWidget): # Visible resolution (entry) self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0) - self.visible_res_enter = NumberLineEdit( - 0.8, 10.0, decimals=2, default=2.0, parent=self - ) + self.visible_res_enter = NumberLineEdit(0.8, 10.0, decimals=2, default=2.0, parent=self) self._layout.addWidget(self.visible_res_enter, 0, 1, 1, 3) self._layout.addWidget(QLabel("Å", parent=self), 0, 4) self.visible_res_enter.newValue.connect(self.set_visible_resolution) self._layout.addWidget(QLabel("Start angle", parent=self), 1, 0) - self.start_angle_enter = NumberLineEdit( - -720, 720.0, 0.0, decimals=3, parent=self - ) + self.start_angle_enter = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self) self._layout.addWidget(self.start_angle_enter, 1, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 1, 4) @@ -81,17 +70,13 @@ class SimpleRotationSettingsPanel(QWidget): self.visible_res_enter.newValue.connect(self.set_total_angle) self._layout.addWidget(QLabel("Image angle", parent=self), 3, 0) - self.image_angle_enter = NumberLineEdit( - 0.001, 1.000, decimals=3, default=0.2, parent=self - ) + self.image_angle_enter = NumberLineEdit(0.001, 1.000, decimals=3, default=0.2, parent=self) self._layout.addWidget(self.image_angle_enter, 3, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 3, 4) self.image_angle_enter.newValue.connect(self.set_image_angle) self._layout.addWidget(QLabel("Temperature", parent=self), 4, 0) - self.temp_enter = NumberLineEdit( - 80, 330, decimals=2, default=100.0, parent=self - ) + self.temp_enter = NumberLineEdit(80, 330, decimals=2, default=100.0, parent=self) self._layout.addWidget(self.temp_enter, 4, 1, 1, 3) self._layout.addWidget(QLabel("K", parent=self), 4, 4) self.temp_enter.newValue.connect(self.set_temperature) @@ -123,9 +108,7 @@ class SimpleRotationSettingsPanel(QWidget): self._layout.addWidget(QLabel("Detector distance", parent=self), 8, 0) self.dtz_label = QLabel(f"--", parent=self) - self.dtz_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.dtz_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.dtz_label, 8, 1, 1, 3) self._layout.addWidget(QLabel("mm", parent=self), 8, 4) @@ -155,31 +138,23 @@ class SimpleRotationSettingsPanel(QWidget): self._layout.addWidget(QLabel("Crystal Size x", parent=self), 12, 0) self.xtal_x_label = QLabel(f"--", parent=self) - self.xtal_x_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.xtal_x_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.xtal_x_label, 12, 1, 1, 3) self._layout.addWidget(QLabel("um", parent=self), 12, 4) self._layout.addWidget(QLabel("Crystal Size y", parent=self), 13, 0) self.xtal_y_label = QLabel(f"--", parent=self) - self.xtal_y_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.xtal_y_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.xtal_y_label, 13, 1, 1, 3) self._layout.addWidget(QLabel("um", parent=self), 13, 4) self._layout.addWidget(QLabel("Crystal Size z", parent=self), 14, 0) self.xtal_z_label = QLabel(f"--", parent=self) - self.xtal_z_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.xtal_z_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.xtal_z_label, 14, 1, 1, 3) self._layout.addWidget(QLabel("um", parent=self), 14, 4) - self._layout.addWidget( - QLabel("Calculated Dose (xtal size)", parent=self), 15, 0 - ) + self._layout.addWidget(QLabel("Calculated Dose (xtal size)", parent=self), 15, 0) self.xtal_size_dose_label = QLabel(f"--", parent=self) self.xtal_size_dose_label.setAlignment( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter @@ -197,13 +172,9 @@ class SimpleRotationSettingsPanel(QWidget): self._layout.addWidget(QLabel("Flux", parent=self), 17, 0) self.flux_label = QLabel(f"--", parent=self) - self.flux_label.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.flux_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.flux_label, 17, 1, 1, 3) - self._layout.addWidget( - QLabel("x 109 ph s-1", parent=self), 17, 4 - ) + self._layout.addWidget(QLabel("x 109 ph s-1", parent=self), 17, 4) self._layout.addWidget(QLabel("Beam Size", parent=self), 18, 0) self.beam_size_label = QLabel(f"--", parent=self) @@ -223,18 +194,12 @@ class SimpleRotationSettingsPanel(QWidget): self._layout.addWidget(QLabel("Total measurement time", parent=self), 20, 0) self.total_time = QLabel(f"{self.total_time_s} min 0 s") - self.total_time.setAlignment( - Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter - ) + self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self._layout.addWidget(self.total_time, 20, 1, 1, 3) # add vertical stretch between detector distance and the run button self._layout.addItem( - QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding), - 21, - 0, - 1, - 6, + QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding), 21, 0, 1, 6 ) # Run rotation button @@ -283,9 +248,7 @@ class SimpleRotationSettingsPanel(QWidget): if s.diffraction.wavelength_angstrom is None: self.wavelength_label.setText("N/A") else: - self.wavelength_label.setText( - f"{s.diffraction.wavelength_angstrom:.3f}" - ) + self.wavelength_label.setText(f"{s.diffraction.wavelength_angstrom:.3f}") self.update_calculated_labels() @Slot(float) @@ -330,9 +293,7 @@ class SimpleRotationSettingsPanel(QWidget): total_angle = self.angular_range_enter.value d_vis = self.visible_res_enter.value image_angle = self.image_angle_enter.value - d_vis = ( - d_vis - 0.2 - ) # additional fudge factor that weights more towards high_res + d_vis = d_vis - 0.2 # additional fudge factor that weights more towards high_res if d_vis <= 0.0: d_vis = 1.3 d_tar = 1 / (1 / d_vis + 0.1) @@ -356,9 +317,7 @@ class SimpleRotationSettingsPanel(QWidget): multiplier_1 = max(self.xtal_y, beam_size_um_y) multiplier_2 = max(self.xtal_z, beam_size_um_y) new_beam_um_y = math.sqrt(multiplier_1 * multiplier_2) - self.xtal_size_dose_rate_MGy_s = ( - flux / (beam_size_um_x * new_beam_um_y * Kdose) - ) / 1e6 + self.xtal_size_dose_rate_MGy_s = (flux / (beam_size_um_x * new_beam_um_y * Kdose)) / 1e6 else: self.xtal_size_dose_rate_MGy_s = self.dose_rate_MGy_s @@ -398,9 +357,7 @@ class SimpleRotationSettingsPanel(QWidget): if self.dtz <= 0.0: self.dtz_label.setText(f"""-""") elif self.dtz < self.__d.bl.dtz_min: - self.dtz_label.setText( - f"""{self.dtz:.2f}""" - ) + self.dtz_label.setText(f"""{self.dtz:.2f}""") self.dtz = self.__d.bl.dtz_min else: self.dtz_label.setText(f"{self.dtz:.2f}") diff --git a/src/aare/gui/panels/target_stability_panel.py b/src/aare/gui/panels/target_stability_panel.py index 9ac5bfa7..76bb80d0 100644 --- a/src/aare/gui/panels/target_stability_panel.py +++ b/src/aare/gui/panels/target_stability_panel.py @@ -215,52 +215,31 @@ class TargetStabilityPanel(QWidget): self.help_button.clicked.connect(self._show_metrics_help) self.score_basis_combo = QComboBox() - self.score_basis_combo.addItems( - [self.SCORE_FROM_STEP_XY, self.SCORE_FROM_SIGMA_XY] - ) + self.score_basis_combo.addItems([self.SCORE_FROM_STEP_XY, self.SCORE_FROM_SIGMA_XY]) self.score_basis_combo.setCurrentText(self.SCORE_FROM_STEP_XY) self.score_basis_combo.currentTextChanged.connect(self._on_score_basis_changed) self.wheel_mode_combo = QComboBox() self.wheel_mode_combo.addItems( - [ - self.WHEEL_X, - self.WHEEL_LEFT_Y, - self.WHEEL_RIGHT_Y, - self.WHEEL_SCORE, - ] + [self.WHEEL_X, self.WHEEL_LEFT_Y, self.WHEEL_RIGHT_Y, self.WHEEL_SCORE] ) self.wheel_mode_combo.setCurrentText(self.WHEEL_X) - self.wheel_mode_combo.currentTextChanged.connect( - lambda _text: self._update_status_label() - ) + self.wheel_mode_combo.currentTextChanged.connect(lambda _text: self._update_status_label()) self.pan_mode_combo = QComboBox() self.pan_mode_combo.addItems( - [ - self.PAN_ALL, - self.PAN_X, - self.PAN_LEFT_Y, - self.PAN_RIGHT_Y, - self.PAN_SCORE, - ] + [self.PAN_ALL, self.PAN_X, self.PAN_LEFT_Y, self.PAN_RIGHT_Y, self.PAN_SCORE] ) self.pan_mode_combo.setCurrentText(self.PAN_ALL) - self.pan_mode_combo.currentTextChanged.connect( - lambda _text: self._update_status_label() - ) + self.pan_mode_combo.currentTextChanged.connect(lambda _text: self._update_status_label()) self.status_label = QLabel("Waiting for target-point data...") - self.status_label.setAlignment( - Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter - ) + self.status_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.metrics_label = QLabel( "Target: (-, -) | Beam: (-, -) | Distance: - px | Std dev: - px" ) - self.metrics_label.setAlignment( - Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter - ) + self.metrics_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.metrics_label.setTextFormat(Qt.TextFormat.RichText) self.metrics_label.setWordWrap(True) @@ -346,9 +325,7 @@ class TargetStabilityPanel(QWidget): self.chart.addSeries(series) self.chart.legend().setVisible(True) - self.chart.setTitle( - "Target stability, distance, score, and step motion (last 60 s)" - ) + self.chart.setTitle("Target stability, distance, score, and step motion (last 60 s)") self.axis_x = QValueAxis() self.axis_x.setTitleText("Time [s ago]") @@ -371,12 +348,7 @@ class TargetStabilityPanel(QWidget): self.chart.addAxis(self.axis_y_distance, Qt.AlignmentFlag.AlignRight) self.chart.addAxis(self.axis_y_score, Qt.AlignmentFlag.AlignRight) - for series in ( - self.series, - self.sigma_x_series, - self.sigma_y_series, - self.step_series, - ): + for series in (self.series, self.sigma_x_series, self.sigma_y_series, self.step_series): series.attachAxis(self.axis_x) series.attachAxis(self.axis_y) @@ -486,9 +458,7 @@ class TargetStabilityPanel(QWidget): ) def _connect_series_selection(self, series: QLineSeries, target_name: str) -> None: - series.clicked.connect( - lambda _point, target=target_name: self._select_trace_target(target) - ) + series.clicked.connect(lambda _point, target=target_name: self._select_trace_target(target)) def _try_select_series_at_point(self, view_pos: QPoint) -> bool: """Check if click is near a visible series point and select it. Returns True if found.""" @@ -545,9 +515,7 @@ class TargetStabilityPanel(QWidget): def _set_paused(self, paused: bool) -> None: self._paused = paused if paused: - self.pause_button.setText( - "Resume (Live)" if self._collected_samples else "Resume" - ) + self.pause_button.setText("Resume (Live)" if self._collected_samples else "Resume") else: self._unfreeze_plot() self.pause_button.setText("Pause") @@ -613,11 +581,7 @@ class TargetStabilityPanel(QWidget): def _current_sigma_stats(self) -> dict[str, float]: n = self._rolling_count if n < 2: - return { - "std_dx": 0.0, - "std_dy": 0.0, - "sigma": 0.0, - } + return {"std_dx": 0.0, "std_dy": 0.0, "sigma": 0.0} mean_dx = self._rolling_sum_dx / n mean_dy = self._rolling_sum_dy / n @@ -628,30 +592,16 @@ class TargetStabilityPanel(QWidget): std_dy = math.sqrt(var_dy) sigma = math.hypot(std_dx, std_dy) - return { - "std_dx": std_dx, - "std_dy": std_dy, - "sigma": sigma, - } + return {"std_dx": std_dx, "std_dy": std_dy, "sigma": sigma} def _current_step_jitter_stats(self) -> dict[str, float]: if self._rolling_step_count <= 0: - return { - "rms_step_dx": 0.0, - "rms_step_dy": 0.0, - "rms_step_xy": 0.0, - } + return {"rms_step_dx": 0.0, "rms_step_dy": 0.0, "rms_step_xy": 0.0} return { - "rms_step_dx": math.sqrt( - self._rolling_step_dx2_sum / self._rolling_step_count - ), - "rms_step_dy": math.sqrt( - self._rolling_step_dy2_sum / self._rolling_step_count - ), - "rms_step_xy": math.sqrt( - self._rolling_step_xy2_sum / self._rolling_step_count - ), + "rms_step_dx": math.sqrt(self._rolling_step_dx2_sum / self._rolling_step_count), + "rms_step_dy": math.sqrt(self._rolling_step_dy2_sum / self._rolling_step_count), + "rms_step_xy": math.sqrt(self._rolling_step_xy2_sum / self._rolling_step_count), } def _current_score_value(self) -> float: @@ -664,10 +614,7 @@ class TargetStabilityPanel(QWidget): def _update_metrics_label(self, force: bool = False) -> None: now = time.monotonic() - if ( - not force - and (now - self._last_metrics_update_ts) < self._metrics_update_interval_s - ): + if not force and (now - self._last_metrics_update_ts) < self._metrics_update_interval_s: return self._last_metrics_update_ts = now @@ -849,12 +796,7 @@ class TargetStabilityPanel(QWidget): dy = ty - by distance = math.hypot(dx, dy) - sample = { - "ts": ts, - "dx": dx, - "dy": dy, - "distance": distance, - } + sample = {"ts": ts, "dx": dx, "dy": dy, "distance": distance} should_finish_collection = False with self._data_lock: @@ -1033,9 +975,7 @@ class TargetStabilityPanel(QWidget): while self._plot_points and float(self._plot_points[0]["ts"]) < cutoff: self._plot_points.popleft() - def _data_limits( - self, - ) -> tuple[float, float, float, float, float, float, float, float]: + def _data_limits(self) -> tuple[float, float, float, float, float, float, float, float]: left_values: list[float] = [] right_values: list[float] = [] @@ -1048,13 +988,7 @@ class TargetStabilityPanel(QWidget): float(point["step_xy"]), ] ) - right_values.extend( - [ - float(point["distance"]), - float(point["dx"]), - float(point["dy"]), - ] - ) + right_values.extend([float(point["distance"]), float(point["dx"]), float(point["dy"])]) left_min = min(left_values, default=0.0) left_max = max(left_values, default=1.0) @@ -1064,12 +998,8 @@ class TargetStabilityPanel(QWidget): left_span = max(1.0, left_max - left_min) right_span = max(1.0, right_max - right_min) - score_min = min( - (float(point["score"]) for point in self._plot_points), default=0.0 - ) - score_max = max( - (float(point["score"]) for point in self._plot_points), default=100.0 - ) + score_min = min((float(point["score"]) for point in self._plot_points), default=0.0) + score_max = max((float(point["score"]) for point in self._plot_points), default=100.0) score_lo = max(0.0, score_min - 5.0) score_hi = min(100.0, max(score_lo + 10.0, score_max + 5.0)) @@ -1140,9 +1070,7 @@ class TargetStabilityPanel(QWidget): self._clamp_axes() self._update_status_label() - def _pan_selected_axes( - self, dx_pixels: int, dy_pixels: int, width: int, height: int - ) -> None: + def _pan_selected_axes(self, dx_pixels: int, dy_pixels: int, width: int, height: int) -> None: if width <= 0 or height <= 0: return @@ -1166,9 +1094,7 @@ class TargetStabilityPanel(QWidget): mode = self.pan_mode_combo.currentText() if mode == self.PAN_X: - self.axis_x.setRange( - self.axis_x.min() + x_shift, self.axis_x.max() + x_shift - ) + self.axis_x.setRange(self.axis_x.min() + x_shift, self.axis_x.max() + x_shift) elif mode == self.PAN_LEFT_Y: if ( @@ -1177,9 +1103,7 @@ class TargetStabilityPanel(QWidget): or self.show_sigma_y_cb.isChecked() or self.show_step_cb.isChecked() ): - self.axis_y.setRange( - self.axis_y.min() + y_shift, self.axis_y.max() + y_shift - ) + self.axis_y.setRange(self.axis_y.min() + y_shift, self.axis_y.max() + y_shift) elif mode == self.PAN_RIGHT_Y: if ( @@ -1188,21 +1112,17 @@ class TargetStabilityPanel(QWidget): or self.show_dy_cb.isChecked() ): self.axis_y_distance.setRange( - self.axis_y_distance.min() + y2_shift, - self.axis_y_distance.max() + y2_shift, + self.axis_y_distance.min() + y2_shift, self.axis_y_distance.max() + y2_shift ) elif mode == self.PAN_SCORE: if self.show_score_cb.isChecked(): self.axis_y_score.setRange( - self.axis_y_score.min() + y3_shift, - self.axis_y_score.max() + y3_shift, + self.axis_y_score.min() + y3_shift, self.axis_y_score.max() + y3_shift ) else: - self.axis_x.setRange( - self.axis_x.min() + x_shift, self.axis_x.max() + x_shift - ) + self.axis_x.setRange(self.axis_x.min() + x_shift, self.axis_x.max() + x_shift) if ( self.show_sigma_cb.isChecked() @@ -1210,9 +1130,7 @@ class TargetStabilityPanel(QWidget): or self.show_sigma_y_cb.isChecked() or self.show_step_cb.isChecked() ): - self.axis_y.setRange( - self.axis_y.min() + y_shift, self.axis_y.max() + y_shift - ) + self.axis_y.setRange(self.axis_y.min() + y_shift, self.axis_y.max() + y_shift) if ( self.show_distance_cb.isChecked() @@ -1220,14 +1138,12 @@ class TargetStabilityPanel(QWidget): or self.show_dy_cb.isChecked() ): self.axis_y_distance.setRange( - self.axis_y_distance.min() + y2_shift, - self.axis_y_distance.max() + y2_shift, + self.axis_y_distance.min() + y2_shift, self.axis_y_distance.max() + y2_shift ) if self.show_score_cb.isChecked(): self.axis_y_score.setRange( - self.axis_y_score.min() + y3_shift, - self.axis_y_score.max() + y3_shift, + self.axis_y_score.min() + y3_shift, self.axis_y_score.max() + y3_shift ) self._clamp_axes() @@ -1273,11 +1189,7 @@ class TargetStabilityPanel(QWidget): self._update_metrics_label() return - if ( - self._paused - and self._collecting_until is None - and self._frozen_plot_points is None - ): + if self._paused and self._collecting_until is None and self._frozen_plot_points is None: self._update_status_label() self._update_metrics_label() return @@ -1292,36 +1204,28 @@ class TargetStabilityPanel(QWidget): self._chart_dirty = False sigma_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["sigma"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["sigma"])) for point in plot_data ] sigma_x_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"])) for point in plot_data ] sigma_y_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"])) for point in plot_data ] distance_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["distance"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["distance"])) for point in plot_data ] dx_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["dx"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["dx"])) for point in plot_data ] dy_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["dy"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["dy"])) for point in plot_data ] score_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["score"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["score"])) for point in plot_data ] step_points = [ - QPointF(float(point["ts"]) - ref_time, float(point["step_xy"])) - for point in plot_data + QPointF(float(point["ts"]) - ref_time, float(point["step_xy"])) for point in plot_data ] self.series.replace(sigma_points) @@ -1334,16 +1238,9 @@ class TargetStabilityPanel(QWidget): self.step_series.replace(step_points) if self._auto_scale_enabled: - ( - xmin, - xmax, - left_ymin, - left_ymax, - right_ymin, - right_ymax, - score_lo, - score_hi, - ) = self._data_limits() + (xmin, xmax, left_ymin, left_ymax, right_ymin, right_ymax, score_lo, score_hi) = ( + self._data_limits() + ) self.axis_x.setRange(xmin, xmax) self.axis_y.setRange(left_ymin, left_ymax) self.axis_y_distance.setRange(right_ymin, right_ymax) @@ -1358,9 +1255,7 @@ class TargetStabilityPanel(QWidget): seconds = float(self.seconds_spin.value()) now = time.monotonic() rows = [s for s in self._samples if float(s["ts"]) >= now - seconds] - self._save_rows( - rows, suggested_name=f"target_stability_last_{self._seconds_text()}s.csv" - ) + self._save_rows(rows, suggested_name=f"target_stability_last_{self._seconds_text()}s.csv") def _collect_next_x_seconds(self) -> None: if self._paused: @@ -1413,14 +1308,8 @@ class TargetStabilityPanel(QWidget): if temp_rolling_count >= 2: mean_dx = temp_rolling_sum_dx / temp_rolling_count mean_dy = temp_rolling_sum_dy / temp_rolling_count - var_dx = max( - 0.0, - (temp_rolling_sum_dx2 / temp_rolling_count) - (mean_dx * mean_dx), - ) - var_dy = max( - 0.0, - (temp_rolling_sum_dy2 / temp_rolling_count) - (mean_dy * mean_dy), - ) + var_dx = max(0.0, (temp_rolling_sum_dx2 / temp_rolling_count) - (mean_dx * mean_dx)) + var_dy = max(0.0, (temp_rolling_sum_dy2 / temp_rolling_count) - (mean_dy * mean_dy)) std_dx = math.sqrt(var_dx) std_dy = math.sqrt(var_dy) sigma = math.hypot(std_dx, std_dy) @@ -1474,8 +1363,7 @@ class TargetStabilityPanel(QWidget): # This avoids blocking while any locks might be held file_name = f"target_stability_collected_{self._seconds_text()}s.csv" QTimer.singleShot( - 0, - lambda: self._save_rows(self._collected_samples, suggested_name=file_name), + 0, lambda: self._save_rows(self._collected_samples, suggested_name=file_name) ) self._update_status_label() @@ -1491,10 +1379,7 @@ class TargetStabilityPanel(QWidget): return path, _ = QFileDialog.getSaveFileName( - self, - "Save Target Stability Data", - suggested_name, - "CSV files (*.csv)", + self, "Save Target Stability Data", suggested_name, "CSV files (*.csv)" ) if not path: return diff --git a/src/aare/gui/panels/tell_sample_panel.py b/src/aare/gui/panels/tell_sample_panel.py index ddd5ae84..16f0a17c 100644 --- a/src/aare/gui/panels/tell_sample_panel.py +++ b/src/aare/gui/panels/tell_sample_panel.py @@ -59,19 +59,13 @@ class TellSamplePanel(QFrame): self.table_model = UserSampleSpreadsheet(samples=samples.s) self.table_view.setModel(self.table_model) - self.table_view.horizontalHeader().setSectionResizeMode( - QHeaderView.ResizeMode.Stretch - ) + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table_view.setSortingEnabled(True) self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table_view.customContextMenuRequested.connect(self.context_menu) - self.table_view.setSelectionBehavior( - QAbstractItemView.SelectionBehavior.SelectRows - ) - self.table_view.setSelectionMode( - QAbstractItemView.SelectionMode.ExtendedSelection - ) + self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.table_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers) self.table_view.setDragEnabled(True) @@ -187,9 +181,7 @@ class TellSamplePanel(QFrame): toggle_all.setChecked(self.table_model.show_all_pgroups) def _toggle_all(): - self.table_model.set_show_all_pgroups( - not self.table_model.show_all_pgroups - ) + self.table_model.set_show_all_pgroups(not self.table_model.show_all_pgroups) toggle_all.triggered.connect(_toggle_all) @@ -200,9 +192,7 @@ class TellSamplePanel(QFrame): if action == set_filter_action: from PySide6.QtWidgets import QInputDialog - text, ok = QInputDialog.getText( - self, "Set filter", f"Filter for '{col_name}':" - ) + text, ok = QInputDialog.getText(self, "Set filter", f"Filter for '{col_name}':") if ok: self.table_model.set_column_filter(logical_index, text) elif action == clear_filter_action: @@ -218,9 +208,7 @@ class TellSamplePanel(QFrame): tell_details = "" if tell_state is not None: activity = tell_state.activity.display_name() - phase = ( - tell_state.phase.display_name() if tell_state.phase is not None else "" - ) + phase = tell_state.phase.display_name() if tell_state.phase is not None else "" message = (tell_state.message or "").strip() tell_parts = [activity] @@ -237,9 +225,7 @@ class TellSamplePanel(QFrame): else: try: if sample.location is None: - base_text = ( - f"Current sample: {sample.sample_name} (Manual mount)" - ) + base_text = f"Current sample: {sample.sample_name} (Manual mount)" else: base_text = ( f"Current sample: {sample.sample_name} " diff --git a/src/aare/gui/panels/zoom_panel.py b/src/aare/gui/panels/zoom_panel.py index 678cf26e..feff1415 100644 --- a/src/aare/gui/panels/zoom_panel.py +++ b/src/aare/gui/panels/zoom_panel.py @@ -43,8 +43,6 @@ class ZoomPanel(QWidget): self.__zoom = status.bl.zoom for z in self.__zoom_settings: if z["value"] == self.__zoom: - self.__buttons[self.__zoom_settings.index(z)].setStyleSheet( - "font-weight: bold;" - ) + self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("font-weight: bold;") else: self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("") diff --git a/src/aare/gui/scan_logic/raster_grid_manager.py b/src/aare/gui/scan_logic/raster_grid_manager.py index cfbbb2a8..417d7544 100644 --- a/src/aare/gui/scan_logic/raster_grid_manager.py +++ b/src/aare/gui/scan_logic/raster_grid_manager.py @@ -136,9 +136,7 @@ class RasterGridManager(QObject): n_x=0, n_y=0, smargon_top_left=self.__geom.smargon, - grid_size_mm=Coordinate( - x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y - ), + grid_size_mm=Coordinate(x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y), omega_deg=self.__geom.omega_deg, exp_time_s=0.02, transmission=1.0, @@ -150,9 +148,7 @@ class RasterGridManager(QObject): # Each entry is (QImage, backing ndarray); the ndarray must be kept alive # because QImage shares its buffer without copying. Rebuilt only when the # data or metric changes, not on every repaint (sample move / zoom). - self.__heatmap_cache: dict[ - tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray] - ] = {} + self.__heatmap_cache: dict[tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]] = {} @property def active_grid(self) -> RasterGridRequest: @@ -194,12 +190,7 @@ class RasterGridManager(QObject): return None start_x, start_y, cell_w, cell_h = geo - return QRectF( - start_x, - start_y, - cell_w * grid.n_x, - cell_h * grid.n_y, - ) + return QRectF(start_x, start_y, cell_w * grid.n_x, cell_h * grid.n_y) def _visible_index_range( self, @@ -214,13 +205,9 @@ class RasterGridManager(QObject): return 0, grid.n_x, 0, grid.n_y min_x = max(0, int(math.floor((visible_rect.left() - start_x) / cell_w)) - 1) - max_x = min( - grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1 - ) + max_x = min(grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1) min_y = max(0, int(math.floor((visible_rect.top() - start_y) / cell_h)) - 1) - max_y = min( - grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1 - ) + max_y = min(grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1) return min_x, max_x, min_y, max_y @@ -246,9 +233,7 @@ class RasterGridManager(QObject): self.clear_active_grid() if self.__beam_size_mm != s.geom.beam_size_mm: self.__beam_size_mm = s.geom.beam_size_mm - self.update_grid_size( - 0.8 * self.__beam_size_mm.x, 0.8 * self.__beam_size_mm.y - ) + self.update_grid_size(0.8 * self.__beam_size_mm.x, 0.8 * self.__beam_size_mm.y) self.__geom = s.geom def resize_active_grid(self, end_point: QPointF): @@ -306,15 +291,10 @@ class RasterGridManager(QObject): self.__active_grid.n_x = 0 self.__active_grid.n_y = 0 self.grid_scan_size_changed.emit( - 0, - 0, - self.__active_grid.grid_size_mm.x, - self.__active_grid.grid_size_mm.y, + 0, 0, self.__active_grid.grid_size_mm.x, self.__active_grid.grid_size_mm.y ) - def get_grid_coord( - self, grid: RasterGridRequest, point: QPointF - ) -> Tuple[int, int]: + def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]: point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y())) delta = point_bl - self.__geom.smargon_to_beamline(grid.smargon_top_left.sh_mm) @@ -363,9 +343,7 @@ class RasterGridManager(QObject): delta = point_bl - self.__geom.smargon_to_beamline( self.__active_grid.smargon_top_left.sh_mm ) - return ( - 0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x - ) and ( + return (0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x) and ( 0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y ) @@ -432,9 +410,7 @@ class RasterGridManager(QObject): grid.result.images[cell].pr is not None and grid.result.images[cell].pr >= 0 ): - txt += ( - f"Profile Radius {grid.result.images[cell].pr:.2f}
" - ) + txt += f"Profile Radius {grid.result.images[cell].pr:.2f}
" if ( grid.result.images[cell].nx is not None and grid.result.images[cell].ny is not None @@ -537,15 +513,11 @@ class RasterGridManager(QObject): case RasterGridMetric.INDEXING: v = [obj.index for obj in i.result.images] case RasterGridMetric.PR: - v = [ - obj.index / max(obj.spots_low_res, 1) for obj in i.result.images - ] + v = [obj.index / max(obj.spots_low_res, 1) for obj in i.result.images] case RasterGridMetric.RASTER_SCORE: score = compute_crystal_score_array(i.result.images) v = [ - score[obj.nx, obj.ny] - if obj.nx is not None and obj.ny is not None - else None + score[obj.nx, obj.ny] if obj.nx is not None and obj.ny is not None else None for obj in i.result.images ] case RasterGridMetric.BFACTOR: @@ -558,9 +530,7 @@ class RasterGridManager(QObject): v = [self.spot_ice_ratio(obj) for obj in i.result.images] case RasterGridMetric.SPOTS_INDEXED: v = [obj.spots_indexed for obj in i.result.images] - self._draw_completed_heatmap( - painter, i, v, alpha, visible_rect, (id(i), self.__metric) - ) + self._draw_completed_heatmap(painter, i, v, alpha, visible_rect, (id(i), self.__metric)) def _invalidate_heatmap_cache(self) -> None: """Drop all cached heatmap bitmaps. Call whenever the completed-grid data @@ -568,10 +538,7 @@ class RasterGridManager(QObject): self.__heatmap_cache.clear() def _heatmap_image( - self, - cache_key: tuple, - grid: RasterGridRequest, - values: List[float] | List[int], + self, cache_key: tuple, grid: RasterGridRequest, values: List[float] | List[int] ) -> QImage | None: """Return a cached n_x*n_y heatmap bitmap for this grid, building it once on a cache miss. One pixel per cell; colours baked at full opacity with @@ -589,10 +556,7 @@ class RasterGridManager(QObject): return built[0] def _build_heatmap_image( - self, - n_x: int, - n_y: int, - values: List[float] | List[int], + self, n_x: int, n_y: int, values: List[float] | List[int] ) -> tuple[QImage, np.ndarray] | None: if n_x <= 0 or n_y <= 0: return None @@ -601,10 +565,7 @@ class RasterGridManager(QObject): # Cell values are laid out row-major: idx = x + y * n_x (matches the # per-cell loop and load_image()/get_grid_coord() hit-testing). - flat = np.array( - [np.nan if v is None else v for v in values[:count]], - dtype=np.float64, - ) + flat = np.array([np.nan if v is None else v for v in values[:count]], dtype=np.float64) if flat.size < count: flat = np.concatenate([flat, np.full(count - flat.size, np.nan)]) @@ -673,9 +634,7 @@ class RasterGridManager(QObject): painter.drawImage(bounds, image) painter.setOpacity(1.0) - painter.setPen( - QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine) - ) + painter.setPen(QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRect(bounds) painter.restore() @@ -715,9 +674,7 @@ class RasterGridManager(QObject): return if values is not None and (cell_w < 3.0 or cell_h < 3.0): - if self._draw_completed_grid_fast( - painter, grid, values, alpha, visible_rect - ): + if self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect): return painter.save() @@ -725,12 +682,8 @@ class RasterGridManager(QObject): if values is not None: painter.setPen(Qt.PenStyle.NoPen) - min_value = min( - (x for x in values if x is not None and not math.isnan(x)), default=0 - ) - max_value = max( - (x for x in values if x is not None and not math.isnan(x)), default=1 - ) + min_value = min((x for x in values if x is not None and not math.isnan(x)), default=0) + max_value = max((x for x in values if x is not None and not math.isnan(x)), default=1) diff = 1 if min_value == max_value else (max_value - min_value) else: painter.setPen(QPen(QColor(114, 159, 207), 1, Qt.PenStyle.SolidLine)) @@ -749,9 +702,7 @@ class RasterGridManager(QObject): if values is None: painter.setBrush(Qt.BrushStyle.NoBrush) - painter.drawRect( - QRect(round(px), round(py), round(cell_w), round(cell_h)) - ) + painter.drawRect(QRect(round(px), round(py), round(cell_w), round(cell_h))) continue idx = x + y * grid.n_x @@ -763,22 +714,14 @@ class RasterGridManager(QObject): ): continue - brush = float_to_viridis_brush( - (values[idx] - min_value) / diff, alpha=alpha - ) - painter.fillRect( - QRectF(px, py, max(1.0, cell_w), max(1.0, cell_h)), brush - ) + brush = float_to_viridis_brush((values[idx] - min_value) / diff, alpha=alpha) + painter.fillRect(QRectF(px, py, max(1.0, cell_w), max(1.0, cell_h)), brush) if values is None: painter.setBrush(Qt.BrushStyle.NoBrush) else: painter.setPen( - QPen( - QColor(114, 159, 207, min(255, alpha + 40)), - 1, - Qt.PenStyle.SolidLine, - ) + QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine) ) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRect(bounds) @@ -786,10 +729,7 @@ class RasterGridManager(QObject): painter.restore() def _draw_active_grid_fast( - self, - painter: QPainter, - grid: RasterGridRequest, - visible_rect: QRectF | None, + self, painter: QPainter, grid: RasterGridRequest, visible_rect: QRectF | None ) -> bool: geo = self._grid_pixel_geometry(grid) if geo is None: @@ -869,9 +809,7 @@ class RasterGridManager(QObject): ): return True - valid_values = [ - x for x in values if x is not None and not math.isnan(x) and x >= 0 - ] + valid_values = [x for x in values if x is not None and not math.isnan(x) and x >= 0] min_value = min(valid_values, default=0) max_value = max(valid_values, default=1) diff = 1 if min_value == max_value else (max_value - min_value) @@ -897,12 +835,7 @@ class RasterGridManager(QObject): draw_w = max(1.0, cell_w * stride_x) value = self._block_value( - values, - grid.n_x, - x, - min(x + stride_x, grid.n_x), - y, - min(y + stride_y, grid.n_y), + values, grid.n_x, x, min(x + stride_x, grid.n_x), y, min(y + stride_y, grid.n_y) ) if value is None: continue @@ -910,9 +843,7 @@ class RasterGridManager(QObject): brush = float_to_viridis_brush((value - min_value) / diff, alpha=alpha) painter.fillRect(QRectF(px, py, draw_w, draw_h), brush) - painter.setPen( - QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine) - ) + painter.setPen(QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRect(bounds) @@ -920,13 +851,7 @@ class RasterGridManager(QObject): return True def _block_value( - self, - values: List[float] | List[int], - grid_nx: int, - x0: int, - x1: int, - y0: int, - y1: int, + self, values: List[float] | List[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int ) -> float | None: best = None for y in range(y0, y1): @@ -1004,12 +929,8 @@ class RasterGridManager(QObject): self.omega.emit(self.__completed_grids[row].request.omega_deg) self.smargon.emit( SmargonCoordinate( - phi_deg=self.__completed_grids[ - row - ].request.smargon_top_left.phi_deg, - chi_deg=self.__completed_grids[ - row - ].request.smargon_top_left.chi_deg, + phi_deg=self.__completed_grids[row].request.smargon_top_left.phi_deg, + chi_deg=self.__completed_grids[row].request.smargon_top_left.chi_deg, ) ) @@ -1019,9 +940,7 @@ class RasterGridManager(QObject): src = self.__completed_grids[row].request self.__active_grid.n_x = src.n_x self.__active_grid.n_y = src.n_y - self.__active_grid.grid_size_mm = Coordinate( - x=src.grid_size_mm.x, y=src.grid_size_mm.y - ) + self.__active_grid.grid_size_mm = Coordinate(x=src.grid_size_mm.x, y=src.grid_size_mm.y) self.__active_grid.smargon_top_left = SmargonCoordinate( sh_mm=Coordinate( x=src.smargon_top_left.sh_mm.x, diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py index 9e2be286..1dbf1f3c 100644 --- a/src/aare/gui/styles.py +++ b/src/aare/gui/styles.py @@ -490,4 +490,4 @@ def _portrait_stylesheet() -> str: QWidget#portraitRoot QScrollBar::sub-line:vertical { height: 0px; } - """ \ No newline at end of file + """ diff --git a/src/aare/gui/threads/axis_video_thread.py b/src/aare/gui/threads/axis_video_thread.py index ff5da3f4..9dd84a6f 100644 --- a/src/aare/gui/threads/axis_video_thread.py +++ b/src/aare/gui/threads/axis_video_thread.py @@ -41,13 +41,14 @@ class VideoThread(QThread): if response.status_code != 200: self.error_occurred.emit( - f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}") + f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}" + ) return self.running = True # Buffer to accumulate data - buffer = b'' + buffer = b"" # MJPEG boundary detection boundary = None @@ -61,9 +62,9 @@ class VideoThread(QThread): # Find boundary on first iteration if boundary is None: - boundary_start = buffer.find(b'--') + boundary_start = buffer.find(b"--") if boundary_start != -1: - boundary_end = buffer.find(b'\r\n', boundary_start) + boundary_end = buffer.find(b"\r\n", boundary_start) if boundary_end != -1: boundary = buffer[boundary_start:boundary_end] @@ -97,17 +98,17 @@ class VideoThread(QThread): continue # Find the start of JPEG data (after headers) - jpeg_start = part.find(b'\xff\xd8') # JPEG SOI marker + jpeg_start = part.find(b"\xff\xd8") # JPEG SOI marker if jpeg_start == -1: continue # Find the end of JPEG data - jpeg_end = part.find(b'\xff\xd9', jpeg_start) # JPEG EOI marker + jpeg_end = part.find(b"\xff\xd9", jpeg_start) # JPEG EOI marker if jpeg_end == -1: continue # Extract JPEG data - jpeg_data = part[jpeg_start:jpeg_end + 2] + jpeg_data = part[jpeg_start : jpeg_end + 2] try: # Decode JPEG using OpenCV @@ -121,7 +122,9 @@ class VideoThread(QThread): # Convert to QImage height, width, channel = rgb_frame.shape bytes_per_line = 3 * width - qt_image = QImage(rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888) + qt_image = QImage( + rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888 + ) # Store as last good frame self.last_good_frame = qt_image @@ -153,4 +156,4 @@ class VideoThread(QThread): try: self.wait(5000) except Exception: - pass \ No newline at end of file + pass diff --git a/src/aare/gui/threads/camera_thread.py b/src/aare/gui/threads/camera_thread.py index cb92b1af..414c60f7 100644 --- a/src/aare/gui/threads/camera_thread.py +++ b/src/aare/gui/threads/camera_thread.py @@ -107,8 +107,7 @@ class SampleCameraThread(QThread): bgr = cv2.imdecode(encoded, cv2.IMREAD_COLOR) if bgr is None: self.__set_camera_available( - False, - "Sample camera feed unavailable: failed to decode JPEG frame", + False, "Sample camera feed unavailable: failed to decode JPEG frame" ) continue rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) @@ -127,10 +126,7 @@ class SampleCameraThread(QThread): if self.__measure_focus: gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY) - if ( - self.__focus_mask is None - or self.__focus_mask.shape != gray.shape - ): + if self.__focus_mask is None or self.__focus_mask.shape != gray.shape: height, width = gray.shape y, x = np.ogrid[:height, :width] self.__focus_mask = (x - self.__beam_x) ** 2 + ( @@ -141,16 +137,12 @@ class SampleCameraThread(QThread): self.focus_measure.emit(sharpness) qimage = QImage( - rgb.data, - rgb.shape[1], - rgb.shape[0], - QImage.Format.Format_RGB888, + rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888 ).copy() self.camera_image.emit(QPixmap.fromImage(qimage)) else: self.__set_camera_available( - False, - "Sample camera feed unavailable: no frame header in zmq stream", + False, "Sample camera feed unavailable: no frame header in zmq stream" ) except zmq.Again: # Timeout occurred now = time.perf_counter() @@ -165,14 +157,10 @@ class SampleCameraThread(QThread): self.__fps_frame_count = 0 if no_frames_long: - self.__set_camera_available( - False, "Sample camera feed unavailable" - ) + self.__set_camera_available(False, "Sample camera feed unavailable") continue # Check self.running again except Exception as e: - self.__set_camera_available( - False, f"Sample camera feed unavailable: {e}" - ) + self.__set_camera_available(False, f"Sample camera feed unavailable: {e}") self.running = False def stop(self): diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index bfe5d8a3..a84a5e5d 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -48,12 +48,7 @@ from aarecommon.recurrence_watcher import ( ) from jfjoch_client import ScanResult, ScanResultImagesInner from PySide6.QtCore import QByteArray, QObject, QTimer, QUrl, Signal, Slot -from PySide6.QtNetwork import ( - QNetworkAccessManager, - QNetworkReply, - QNetworkRequest, - QSslError, -) +from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest, QSslError logger = setup_logger("aareGUI") @@ -168,10 +163,7 @@ class DAQWorker(QObject): self._device_error_log_min_interval_s = 10.0 self._last_device_error_log_ts: dict[str, float] = {"tell": 0.0, "smargon": 0.0} - self._last_device_error_log_key: dict[str, str | None] = { - "tell": None, - "smargon": None, - } + self._last_device_error_log_key: dict[str, str | None] = {"tell": None, "smargon": None} self._last_status_error = None self._smargon_error_active = False @@ -256,9 +248,7 @@ class DAQWorker(QObject): self._last_detector_is_error = is_error self.detector_error.emit(msg, is_error) - def _emit_status_if_changed( - self, key: str | None, message: str | None, is_error: bool - ) -> None: + def _emit_status_if_changed(self, key: str | None, message: str | None, is_error: bool) -> None: """ Emit polled device status to the primary alert banner. Used for Server/Tell/Smargon/Aerotech connection status. @@ -428,11 +418,7 @@ class DAQWorker(QObject): if disconnected: if len(disconnected) == 3: - return ( - "all-devices-down", - "TELL, Smargon, and Aerotech disconnected.", - True, - ) + return ("all-devices-down", "TELL, Smargon, and Aerotech disconnected.", True) if len(disconnected) == 2: return ( "+".join(sorted(d.lower() for d in disconnected)) + "-down", @@ -440,11 +426,7 @@ class DAQWorker(QObject): True, ) device = disconnected[0] - return ( - f"{device.lower()}-down", - f"{device} disconnected.", - True, - ) + return (f"{device.lower()}-down", f"{device} disconnected.", True) if restored: if len(restored) == 3: @@ -468,9 +450,7 @@ class DAQWorker(QObject): parsed_response = DAQStatusModel.model_validate_json(response_data) self.update.emit(parsed_response) - pss_alarm = bool( - getattr(getattr(parsed_response, "bl", None), "pss_alarm", False) - ) + pss_alarm = bool(getattr(getattr(parsed_response, "bl", None), "pss_alarm", False)) if pss_alarm != self._last_pss_alarm: self._last_pss_alarm = pss_alarm self.pss_alarm_changed.emit(pss_alarm) @@ -499,9 +479,7 @@ class DAQWorker(QObject): tell_err_text = None if tell_err is None else str(tell_err).strip() smargon_err_text = None if smargon_err is None else str(smargon_err).strip() - aerotech_err_text = ( - None if aerotech_err is None else str(aerotech_err).strip() - ) + aerotech_err_text = None if aerotech_err is None else str(aerotech_err).strip() # Skip device status processing if we just reconnected from server down # (we already showed "Server reconnected") @@ -520,8 +498,7 @@ class DAQWorker(QObject): return tell_changed = ( - self._last_tell_connected != tell_conn - or self._last_tell_error != tell_err_text + self._last_tell_connected != tell_conn or self._last_tell_error != tell_err_text ) smargon_changed = ( self._last_smargon_connected != smargon_conn @@ -554,27 +531,19 @@ class DAQWorker(QObject): if not smargon_conn: self._log_device_error_throttled(device="smargon", message=smargon_err) if not aerotech_conn: - self._log_device_error_throttled( - device="aerotech", message=aerotech_err - ) + self._log_device_error_throttled(device="aerotech", message=aerotech_err) except Exception as e: status = None err_msg = str(e) try: - status = reply.attribute( - QNetworkRequest.Attribute.HttpStatusCodeAttribute - ) + status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) raw_body = reply.readAll().data().decode("utf-8") if raw_body: body_json = json.loads(raw_body) if isinstance(body_json, dict): - err_msg = ( - body_json.get("message") - or body_json.get("detail") - or err_msg - ) + err_msg = body_json.get("message") or body_json.get("detail") or err_msg if body_json.get("code") == "AEROTECH_UNAVAILABLE": extra = body_json.get("extra") or {} if isinstance(extra, dict): @@ -594,18 +563,14 @@ class DAQWorker(QObject): self._has_seen_disconnection = True self._server_was_disconnected = True self._emit_status_if_changed( - "server-down", - f"Aerotech unavailable: {err_msg}", - True, + "server-down", f"Aerotech unavailable: {err_msg}", True ) else: if self._server_connected is not False: self._has_seen_disconnection = True self._server_was_disconnected = True self._emit_status_if_changed( - "server-down", - "Server disconnected. Reconnecting...", - True, + "server-down", "Server disconnected. Reconnecting...", True ) self._server_connected = False @@ -686,9 +651,7 @@ class DAQWorker(QObject): else: logger.error(f"{error_info.message}") title = self._operation_error_title(error_info.exception_class) - self.operation_failed.emit( - title, error_info.message, error_info.critical - ) + self.operation_failed.emit(title, error_info.message, error_info.critical) reply.deleteLater() @@ -732,9 +695,7 @@ class DAQWorker(QObject): logger.error(f"Hardware metadata resync failed: {e}") self.http_error.emit(str(e)) - def _handle_recovery_action_response( - self, reply: QNetworkReply, default_message: str - ): + def _handle_recovery_action_response(self, reply: QNetworkReply, default_message: str): try: response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} @@ -905,9 +866,7 @@ class DAQWorker(QObject): body = json.dumps({"confirmation_code": confirmation_code}) reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8"))) reply.finished.connect( - lambda: self._handle_recovery_action_response( - reply, "Beamline busy flag cleared." - ) + lambda: self._handle_recovery_action_response(reply, "Beamline busy flag cleared.") ) @Slot(str) @@ -922,9 +881,7 @@ class DAQWorker(QObject): body = json.dumps({"confirmation_code": confirmation_code}) reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8"))) reply.finished.connect( - lambda: self._handle_recovery_action_response( - reply, "Beamline session taken over." - ) + lambda: self._handle_recovery_action_response(reply, "Beamline session taken over.") ) @Slot(str) @@ -956,9 +913,7 @@ class DAQWorker(QObject): body = json.dumps({"confirmation_code": confirmation_code}) reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8"))) reply.finished.connect( - lambda: self._handle_recovery_action_response( - reply, "Recovery unmount completed." - ) + lambda: self._handle_recovery_action_response(reply, "Recovery unmount completed.") ) @Slot(str) @@ -1022,13 +977,8 @@ class DAQWorker(QObject): if self._is_detector_state_failure_message(err_msg): self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True) else: - code = ( - body_json.get("code", "") if isinstance(body_json, dict) else "" - ) - if ( - code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR - or status == 503 - ): + code = body_json.get("code", "") if isinstance(body_json, dict) else "" + if code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR or status == 503: self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True) if self._is_critical_detector_failure(status, body_json, err_msg): @@ -1038,15 +988,12 @@ class DAQWorker(QObject): f"Details: {err_msg}" ) self._emit_detector_message( - f"Detector error during manual collection: {err_msg}", - is_error=True, + f"Detector error during manual collection: {err_msg}", is_error=True ) self._emit_manual_collection_critical_failure(critical_msg) short_msg = ( - err_msg.split("input':", 1)[0].strip() - if "input':" in err_msg - else err_msg + err_msg.split("input':", 1)[0].strip() if "input':" in err_msg else err_msg ) logger.error(f"Rotation scan failed: {short_msg}") self.http_error.emit(short_msg) @@ -1088,13 +1035,8 @@ class DAQWorker(QObject): if self._is_detector_state_failure_message(err_msg): self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True) else: - code = ( - body_json.get("code", "") if isinstance(body_json, dict) else "" - ) - if ( - code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR - or status == 503 - ): + code = body_json.get("code", "") if isinstance(body_json, dict) else "" + if code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR or status == 503: self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True) if self._is_critical_detector_failure(status, body_json, err_msg): @@ -1104,8 +1046,7 @@ class DAQWorker(QObject): f"Details: {err_msg}" ) self._emit_detector_message( - f"Detector error during manual collection: {err_msg}", - is_error=True, + f"Detector error during manual collection: {err_msg}", is_error=True ) self._emit_manual_collection_critical_failure(critical_msg) @@ -1159,9 +1100,7 @@ class DAQWorker(QObject): self.raster_scan_completed.emit(reply) return - request = QNetworkRequest( - QUrl(f"{self.__base_url}/scan/raster?auto_center=false") - ) + request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=false")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) request.setRawHeader(b"Content-Type", b"application/json") body = r.model_dump_json() @@ -1203,9 +1142,7 @@ class DAQWorker(QObject): self.raster_scan_completed.emit(reply) return - request = QNetworkRequest( - QUrl(f"{self.__base_url}/scan/raster?auto_center=true") - ) + request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=true")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) request.setRawHeader(b"Content-Type", b"application/json") body = r.model_dump_json() @@ -1256,9 +1193,7 @@ class DAQWorker(QObject): for watcher in self._recurrence_watchers: watcher.reset() - def _observe_recurrence( - self, exception_class_name: str | None - ) -> WatcherTrip | None: + def _observe_recurrence(self, exception_class_name: str | None) -> WatcherTrip | None: exception_class = resolve_exception_class(exception_class_name) for watcher in self._recurrence_watchers: trip = watcher.maybe_trip(exception_class) @@ -1273,9 +1208,7 @@ class DAQWorker(QObject): self.automation_critical_failure.emit(message) @staticmethod - def _extract_reply_error_details( - reply: QNetworkReply, - ) -> tuple[int | None, str, dict | None]: + def _extract_reply_error_details(reply: QNetworkReply) -> tuple[int | None, str, dict | None]: status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) err_msg = reply.errorString() body_json = None @@ -1310,23 +1243,17 @@ class DAQWorker(QObject): @staticmethod def _classify_reply_error( - status: int | None, - err_msg: str, - body_json: dict | None, + status: int | None, err_msg: str, body_json: dict | None ) -> ErrorInfo: code = body_json.get("code") if isinstance(body_json, dict) else None - exception_class = ( - body_json.get("exception_class") if isinstance(body_json, dict) else None - ) + exception_class = body_json.get("exception_class") if isinstance(body_json, dict) else None context = body_json.get("context") if isinstance(body_json, dict) else {} if not isinstance(context, dict): context = {} return ErrorInfo( critical=DAQWorker._is_critical(body_json), code=str(code) if code is not None else None, - exception_class=str(exception_class) - if exception_class is not None - else None, + exception_class=str(exception_class) if exception_class is not None else None, message=str(err_msg or ""), context=context, ) @@ -1351,9 +1278,7 @@ class DAQWorker(QObject): @staticmethod def _is_critical_detector_failure( - status: int | None, - body_json: dict | None = None, - err_msg: str | None = None, + status: int | None, body_json: dict | None = None, err_msg: str | None = None ) -> bool: try: status_int = int(status) if status is not None else None @@ -1397,9 +1322,7 @@ class DAQWorker(QObject): watcher_trip = self._observe_recurrence(error_info.exception_class) if self._is_detector_state_failure_message(error_info.message): - self._emit_detector_message( - f"JFJoch: {error_info.message}", is_error=True - ) + self._emit_detector_message(f"JFJoch: {error_info.message}", is_error=True) if self._is_auth_error_code(error_info.code) or status == 401: logger.error(f"Error in auto scan: {error_info.message}") @@ -1500,15 +1423,11 @@ class DAQWorker(QObject): logger.info("POST /local_contact/resync/detector_metadata") return - request = QNetworkRequest( - QUrl(f"{self.__base_url}/local_contact/resync/detector_metadata") - ) + request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/resync/detector_metadata")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) request.setRawHeader(b"Content-Type", b"application/json") reply = self.__net_manager.post(request, QByteArray(b"")) - reply.finished.connect( - lambda: self._handle_detector_metadata_resync_response(reply) - ) + reply.finished.connect(lambda: self._handle_detector_metadata_resync_response(reply)) @Slot(float) def anneal(self, time_s: float): @@ -1566,14 +1485,10 @@ class DAQWorker(QObject): ) return - request = QNetworkRequest( - QUrl(f"{self.__base_url}/local_contact/simulation_state") - ) + request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/simulation_state")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.get(request) - reply.finished.connect( - lambda: self._handle_local_contact_simulation_state_response(reply) - ) + reply.finished.connect(lambda: self._handle_local_contact_simulation_state_response(reply)) def _handle_local_contact_simulation_state_response(self, reply: QNetworkReply): try: @@ -1602,9 +1517,7 @@ class DAQWorker(QObject): request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/device_state")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.get(request) - reply.finished.connect( - lambda: self._handle_local_contact_device_state_response(reply) - ) + reply.finished.connect(lambda: self._handle_local_contact_device_state_response(reply)) def _handle_local_contact_device_state_response(self, reply: QNetworkReply): try: @@ -1665,9 +1578,7 @@ class DAQWorker(QObject): request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.get(request) - reply.finished.connect( - lambda: self._handle_local_contact_config_response(reply) - ) + reply.finished.connect(lambda: self._handle_local_contact_config_response(reply)) def _handle_local_contact_config_response(self, reply: QNetworkReply): try: @@ -1695,9 +1606,7 @@ class DAQWorker(QObject): request.setRawHeader(b"Content-Type", b"application/json") body = QByteArray(json.dumps(payload).encode("utf-8")) reply = self.__net_manager.put(request, body) - reply.finished.connect( - lambda: self._handle_set_local_contact_config_response(reply) - ) + reply.finished.connect(lambda: self._handle_set_local_contact_config_response(reply)) def _handle_set_local_contact_config_response(self, reply: QNetworkReply): try: @@ -1710,17 +1619,14 @@ class DAQWorker(QObject): self.status_message.emit("Local Contact config saved.", False) except Exception as e: message = ( - "Error transferring information from DAQ while saving Local Contact config.\n\n" - f"{e}" + f"Error transferring information from DAQ while saving Local Contact config.\n\n{e}" ) logger.error(message) self.local_contact_transfer_error.emit(message) @Slot(str, bool) def set_local_contact_simulation(self, device: str, enabled: bool): - self.generic_post( - f"local_contact/simulate/{device}?enabled={str(enabled).lower()}" - ) + self.generic_post(f"local_contact/simulate/{device}?enabled={str(enabled).lower()}") @Slot(str) def restart_local_contact_device(self, device: str): @@ -1778,9 +1684,7 @@ class DAQWorker(QObject): @Slot(str) def bec_reinitialise_planner_and_position_devices(self, method: str = "auto"): - self.generic_post( - f"bec/reinitialise_planner_and_position_devices?method={method}" - ) + self.generic_post(f"bec/reinitialise_planner_and_position_devices?method={method}") @Slot() def bec_save_current_bs_pos(self): @@ -1804,9 +1708,7 @@ class DAQWorker(QObject): @Slot() def initialise_aerotech(self): - logger.info( - "initisalisation does not initisalise aareSCAN but runs homing script" - ) + logger.info("initisalisation does not initisalise aareSCAN but runs homing script") self.generic_post("aerotech/initialize") @Slot() @@ -1892,9 +1794,7 @@ class DAQWorker(QObject): status = None if reply is not None: try: - status = reply.attribute( - QNetworkRequest.Attribute.HttpStatusCodeAttribute - ) + status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) except Exception: status = None @@ -1972,19 +1872,15 @@ class DAQWorker(QObject): finished=bool(progress_payload.get("finished", False)), success=progress_payload.get("success"), samples_in_queue=int(progress_payload.get("samples_in_queue", 0) or 0), - avg_time_per_sample=float( - progress_payload.get("avg_time_per_sample", 0.0) or 0.0 - ), + avg_time_per_sample=float(progress_payload.get("avg_time_per_sample", 0.0) or 0.0), current_sample_name=str(progress_payload.get("current_sample_name") or ""), ) @staticmethod def _log_level_for_event(level: str) -> int: - return { - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - }.get(str(level).upper(), logging.INFO) + return {"ERROR": logging.ERROR, "WARNING": logging.WARNING, "INFO": logging.INFO}.get( + str(level).upper(), logging.INFO + ) def _emit_automation_progress_events(self, progress: AutomationProgress) -> None: for event in progress.events: @@ -2036,8 +1932,8 @@ class DAQWorker(QObject): def _process_automation_progress_buffer(self) -> None: while "\n\n" in self._automation_progress_buffer: - event_data, self._automation_progress_buffer = ( - self._automation_progress_buffer.split("\n\n", 1) + event_data, self._automation_progress_buffer = self._automation_progress_buffer.split( + "\n\n", 1 ) data_lines: list[str] = [] @@ -2070,9 +1966,7 @@ class DAQWorker(QObject): status = None if reply is not None: try: - status = reply.attribute( - QNetworkRequest.Attribute.HttpStatusCodeAttribute - ) + status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) except Exception: status = None @@ -2136,9 +2030,7 @@ class DAQWorker(QObject): return request = QNetworkRequest( - QUrl( - f"{self.__base_url}/face_detection/run?steps={steps}&step_size={step_size}" - ) + QUrl(f"{self.__base_url}/face_detection/run?steps={steps}&step_size={step_size}") ) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) request.setRawHeader(b"Content-Type", b"application/json") @@ -2165,12 +2057,8 @@ class DAQWorker(QObject): data = json.loads(response_data) if response_data else [] if emit_status: # fetch status and bkg in parallel (simple sequential here) - status_req = QNetworkRequest( - QUrl(f"{self.__base_url}/fluorimeter/status") - ) - status_req.setRawHeader( - b"Authorization", f"Bearer {self.__token}".encode("utf-8") - ) + status_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/status")) + status_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) status_reply = self.__net_manager.get(status_req) status_reply.finished.connect( lambda: self._handle_fluorimeter_status_and_emit(data, status_reply) @@ -2186,13 +2074,9 @@ class DAQWorker(QObject): s_payload = self.handle_response(status_reply) s = int(s_payload) if s_payload not in ("", "null") else -1 b_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/background")) - b_req.setRawHeader( - b"Authorization", f"Bearer {self.__token}".encode("utf-8") - ) + b_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) b_reply = self.__net_manager.get(b_req) - b_reply.finished.connect( - lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply) - ) + b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply)) except Exception as e: logger.error(f"Fluorimeter status error: {e}") self.http_error.emit(str(e)) @@ -2222,9 +2106,7 @@ class DAQWorker(QObject): def _handle_fluorimeter_spectrum(self, reply: QNetworkReply): try: response_data = self.handle_response(reply) - parsed_response = FluorescenceSpectrumOutputModel.model_validate_json( - response_data - ) + parsed_response = FluorescenceSpectrumOutputModel.model_validate_json(response_data) self.fluorimeter_spectrum_update.emit(parsed_response) except Exception as e: logger.error(f"Exception from fluorimeter spectrum: {e}") @@ -2237,9 +2119,7 @@ class DAQWorker(QObject): req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/data")) req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.get(req) - reply.finished.connect( - lambda: self._handle_fluorimeter_data(reply, emit_status=True) - ) + reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True)) # Optional: SSE listener for live updates def start_fluorimeter_stream(self): @@ -2336,9 +2216,7 @@ class DAQWorker(QObject): @Slot(str, str) def send_screenshot_db(self, filename: str = "", message: str = ""): if self.__base_url is None: - logger.info( - f"POST /samcam/send_screenshot_db?filename={filename}&message={message}" - ) + logger.info(f"POST /samcam/send_screenshot_db?filename={filename}&message={message}") return from urllib.parse import quote @@ -2530,10 +2408,7 @@ class DAQWorker(QObject): - clear the active beamline session if this GUI owns it """ try: - if ( - hasattr(self, "_baton_timeout_timer") - and self._baton_timeout_timer is not None - ): + if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None: self._baton_timeout_timer.stop() self.end_session() @@ -2585,9 +2460,7 @@ class DAQWorker(QObject): request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) request.setRawHeader(b"Content-Type", b"application/json") reply = self.__net_manager.post(request, QByteArray(b"")) - reply.finished.connect( - lambda: self._handle_gui_session_mutation_response(reply) - ) + reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply)) @Slot(int) def force_remove_gui_session(self, session_id: int): @@ -2595,14 +2468,10 @@ class DAQWorker(QObject): logger.info(f"DELETE /admin/gui_sessions/{session_id}") return - request = QNetworkRequest( - QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}") - ) + request = QNetworkRequest(QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.deleteResource(request) - reply.finished.connect( - lambda: self._handle_gui_session_mutation_response(reply) - ) + reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply)) def _handle_gui_session_mutation_response(self, reply: QNetworkReply): try: @@ -2637,10 +2506,7 @@ class DAQWorker(QObject): self._cleanup_done = True try: - if ( - hasattr(self, "_baton_timeout_timer") - and self._baton_timeout_timer is not None - ): + if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None: self._baton_timeout_timer.stop() except Exception as e: logger.warning(f"Failed to stop _baton_timeout_timer: {e}") diff --git a/src/aare/gui/threads/prediction_subscriber.py b/src/aare/gui/threads/prediction_subscriber.py index 81430ca2..1fa18698 100644 --- a/src/aare/gui/threads/prediction_subscriber.py +++ b/src/aare/gui/threads/prediction_subscriber.py @@ -126,12 +126,7 @@ class PredictionSubscriber(QThread): return None def _rgb_to_pixmap(self, rgb: np.ndarray) -> QPixmap: - qimage = QImage( - rgb.data, - rgb.shape[1], - rgb.shape[0], - QImage.Format.Format_RGB888, - ).copy() + qimage = QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888).copy() return QPixmap.fromImage(qimage) def _emit_focus_measure_if_enabled(self, rgb: np.ndarray) -> None: @@ -169,9 +164,7 @@ class PredictionSubscriber(QThread): self._fps_frame_count = 0 if no_frames_long: - self._set_camera_available( - False, "Sample camera feed unavailable" - ) + self._set_camera_available(False, "Sample camera feed unavailable") continue if not parts: @@ -215,8 +208,7 @@ class PredictionSubscriber(QThread): rgb = self._decode_rgb_image(header, image_bytes) if rgb is None: self._set_camera_available( - False, - "Sample camera feed unavailable: failed to decode frame", + False, "Sample camera feed unavailable: failed to decode frame" ) else: self._set_camera_available(True) @@ -225,8 +217,7 @@ class PredictionSubscriber(QThread): self.image.emit(self._rgb_to_pixmap(rgb)) elif self._emit_images: self._set_camera_available( - False, - "Sample camera feed unavailable: no frame header in zmq stream", + False, "Sample camera feed unavailable: no frame header in zmq stream" ) if detections and self.running: @@ -237,9 +228,7 @@ class PredictionSubscriber(QThread): except Exception as e: if self.running: - self._set_camera_available( - False, f"Sample camera feed unavailable: {e}" - ) + self._set_camera_available(False, f"Sample camera feed unavailable: {e}") logger.exception(f"PredictionSubscriber error: {e}") finally: try: diff --git a/src/aare/gui/threads/sse_client.py b/src/aare/gui/threads/sse_client.py index 05221c10..7264b955 100644 --- a/src/aare/gui/threads/sse_client.py +++ b/src/aare/gui/threads/sse_client.py @@ -110,16 +110,16 @@ class SSEClient(QObject): def _process_buffer(self): """Process SSE data buffer""" - while b'\n' in self._buffer: - newline_index = self._buffer.indexOf(b'\n') + while b"\n" in self._buffer: + newline_index = self._buffer.indexOf(b"\n") line_data = self._buffer.left(newline_index) self._buffer.remove(0, newline_index + 1) # Remove carriage return if present - if line_data.endsWith(b'\r'): + if line_data.endsWith(b"\r"): line_data.chop(1) - line = line_data.data().decode('utf-8', errors='ignore') + line = line_data.data().decode("utf-8", errors="ignore") self._parse_sse_line(line) def _parse_sse_line(self, line: str): @@ -139,28 +139,28 @@ class SSEClient(QObject): return # Skip comments - if line.startswith(':'): + if line.startswith(":"): return # Parse field and value - colon_index = line.find(':') + colon_index = line.find(":") if colon_index == -1: # Field with no value field = line if field == "data": - self._current_data += '\n' + self._current_data += "\n" return field = line[:colon_index] - value = line[colon_index + 1:] + value = line[colon_index + 1 :] # Remove leading space from value - if value.startswith(' '): + if value.startswith(" "): value = value[1:] if field == "data": if self._current_data: - self._current_data += '\n' + self._current_data += "\n" self._current_data += value elif field == "event": self._current_event = value diff --git a/src/aare/gui/tutorials/controls_help_dialog.py b/src/aare/gui/tutorials/controls_help_dialog.py index 82c47aad..30ef8f59 100644 --- a/src/aare/gui/tutorials/controls_help_dialog.py +++ b/src/aare/gui/tutorials/controls_help_dialog.py @@ -1,12 +1,5 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QDialog, - QVBoxLayout, - QTextEdit, - QDialogButtonBox, - QTabWidget, - QWidget, -) +from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QTabWidget, QWidget class ControlsHelpDialog(QDialog): diff --git a/src/aare/gui/tutorials/tutorial_manager.py b/src/aare/gui/tutorials/tutorial_manager.py index cb53d5d6..1cc9ef6b 100644 --- a/src/aare/gui/tutorials/tutorial_manager.py +++ b/src/aare/gui/tutorials/tutorial_manager.py @@ -269,18 +269,18 @@ class TutorialOverlay(QWidget): bubble_y = margin + 48 bubble_x = min( - max(margin, bubble_x), - max(margin, self.width() - self.callout.width() - margin), + max(margin, bubble_x), max(margin, self.width() - self.callout.width() - margin) ) bubble_y = min( - max(margin, bubble_y), - max(margin, self.height() - self.callout.height() - margin - 60), + max(margin, bubble_y), max(margin, self.height() - self.callout.height() - margin - 60) ) self.callout.move(bubble_x, bubble_y) def _reposition_action_buttons(self) -> None: - buttons = [btn for btn in (self.back_button, self.skip_button, self.next_button) if btn.isVisible()] + buttons = [ + btn for btn in (self.back_button, self.skip_button, self.next_button) if btn.isVisible() + ] if not buttons: return @@ -386,10 +386,7 @@ class TutorialManager(QObject): self.stop() self.current_scenario = scenario - self.context = TutorialContext( - scenario_id=scenario.id, - language=language, - ) + self.context = TutorialContext(scenario_id=scenario.id, language=language) self.runtime_state = build_runtime_state_for_scenario(scenario) self.runtime_state.started_at_ms = _now_ms() self.runtime_state.active_step_index = -1 @@ -429,7 +426,7 @@ class TutorialManager(QObject): self.tutorial_stopped.emit(scenario_id) def emit_event(self, name: str, payload: dict[str, Any] | None = None) -> TutorialEvent: - event = self.event_bus.emit_event(name, payload or {}); + event = self.event_bus.emit_event(name, payload or {}) if self.context is not None: self.context.event_log.append(event) return event @@ -622,9 +619,7 @@ class TutorialManager(QObject): return False return self.completion_evaluator.is_step_complete( - step, - self.context, - target_clicked=target_clicked, + step, self.context, target_clicked=target_clicked ) def _try_auto_advance_current_step(self, *, target_clicked: bool = False) -> None: @@ -690,9 +685,13 @@ class TutorialManager(QObject): self._mark_current_step_failed("Step timed out.") - if step.skippable and self.current_scenario is not None and self.current_scenario.allow_skip: + if ( + step.skippable + and self.current_scenario is not None + and self.current_scenario.allow_skip + ): if self.runtime_state is not None: self._advance_to_index(self.runtime_state.active_step_index + 1) return - self.overlay.next_button.setEnabled(True) \ No newline at end of file + self.overlay.next_button.setEnabled(True) diff --git a/src/aare/gui/tutorials/tutorial_models.py b/src/aare/gui/tutorials/tutorial_models.py index 34da9c50..d0ab205c 100644 --- a/src/aare/gui/tutorials/tutorial_models.py +++ b/src/aare/gui/tutorials/tutorial_models.py @@ -148,4 +148,4 @@ class TutorialContext: state: dict[str, Any] = field(default_factory=dict) event_log: list["TutorialEvent"] = field(default_factory=list) - metadata: dict[str, Any] = field(default_factory=dict) \ No newline at end of file + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/aare/gui/tutorials/tutorial_registration.py b/src/aare/gui/tutorials/tutorial_registration.py index 810ac844..76bd4771 100644 --- a/src/aare/gui/tutorials/tutorial_registration.py +++ b/src/aare/gui/tutorials/tutorial_registration.py @@ -34,8 +34,12 @@ def register_tutorials(window, tutorial_manager) -> None: flow=StepFlow.NEXT_ONLY, setup_actions=[ TutorialAction("enter_demo_mode"), - TutorialAction("set_context_state", {"path": "demo.sample_name", "value": "test_sample"}), - TutorialAction("set_context_state", {"path": "demo.sample_position", "value": "X1-2"}), + TutorialAction( + "set_context_state", {"path": "demo.sample_name", "value": "test_sample"} + ), + TutorialAction( + "set_context_state", {"path": "demo.sample_position", "value": "X1-2"} + ), ], ), TutorialStepDefinition( @@ -54,8 +58,7 @@ def register_tutorials(window, tutorial_manager) -> None: kind=StepKind.INFO, title=TutorialTextRef("tutorial.manual.select_sample.title"), body=TutorialTextRef( - "tutorial.manual.select_sample.body", - {"sample_name": "test_sample"}, + "tutorial.manual.select_sample.body", {"sample_name": "test_sample"} ), hint=TutorialTextRef("tutorial.manual.select_sample.hint"), target=TutorialTarget(TargetKind.WIDGET, "tell_samples"), @@ -65,10 +68,7 @@ def register_tutorials(window, tutorial_manager) -> None: id="mount_sample", kind=StepKind.INFO, title=TutorialTextRef("tutorial.manual.mount.title"), - body=TutorialTextRef( - "tutorial.manual.mount.body", - {"sample_name": "test_sample"}, - ), + body=TutorialTextRef("tutorial.manual.mount.body", {"sample_name": "test_sample"}), hint=TutorialTextRef("tutorial.manual.mount.hint"), target=TutorialTarget(TargetKind.WIDGET, "tell_samples"), flow=StepFlow.NEXT_ONLY, @@ -146,11 +146,9 @@ def register_tutorials(window, tutorial_manager) -> None: body=TutorialTextRef("tutorial.manual.file_names.body"), target=TutorialTarget(TargetKind.WIDGET, "file_path_panel"), flow=StepFlow.NEXT_ONLY, - cleanup_actions=[ - TutorialAction("exit_demo_mode"), - ], + cleanup_actions=[TutorialAction("exit_demo_mode")], ), ], ) - tutorial_manager.add_scenario(manual_workflow_demo) \ No newline at end of file + tutorial_manager.add_scenario(manual_workflow_demo) diff --git a/src/aare/gui/tutorials/tutorial_runtime.py b/src/aare/gui/tutorials/tutorial_runtime.py index 12ad7df8..9ab3763e 100644 --- a/src/aare/gui/tutorials/tutorial_runtime.py +++ b/src/aare/gui/tutorials/tutorial_runtime.py @@ -30,6 +30,7 @@ class ResolvedTutorialTarget: - a concrete rectangle - both """ + target_id: str widget: QWidget | None = None rect: QRect | None = None @@ -62,9 +63,7 @@ class TutorialScenarioRuntimeState: @runtime_checkable class TutorialTargetResolver(Protocol): def resolve_target( - self, - target: TutorialTarget, - context: TutorialContext, + self, target: TutorialTarget, context: TutorialContext ) -> ResolvedTutorialTarget | None: """ Resolve a declarative TutorialTarget into a live runtime target. @@ -83,11 +82,7 @@ class TutorialTextResolver(Protocol): @runtime_checkable class TutorialActionExecutor(Protocol): - def execute_action( - self, - action: TutorialAction, - context: TutorialContext, - ) -> None: + def execute_action(self, action: TutorialAction, context: TutorialContext) -> None: """ Execute a model-defined tutorial action. """ @@ -177,18 +172,12 @@ class DictTargetResolver: self._widget_map = dict(widget_map) def resolve_target( - self, - target: TutorialTarget, - context: TutorialContext, + self, target: TutorialTarget, context: TutorialContext ) -> ResolvedTutorialTarget | None: widget = self._widget_map.get(target.target_id) if widget is None: return None - return ResolvedTutorialTarget( - target_id=target.target_id, - widget=widget, - rect=widget.rect(), - ) + return ResolvedTutorialTarget(target_id=target.target_id, widget=widget, rect=widget.rect()) class CompletionEvaluator: @@ -200,22 +189,14 @@ class CompletionEvaluator: self._event_bus = event_bus def is_step_complete( - self, - step: TutorialStep, - context: TutorialContext, - *, - target_clicked: bool = False, + self, step: TutorialStep, context: TutorialContext, *, target_clicked: bool = False ) -> bool: if step.completion is None: return True return self.evaluate(step.completion, context, target_clicked=target_clicked) def evaluate( - self, - rule: CompletionRule, - context: TutorialContext, - *, - target_clicked: bool = False, + self, rule: CompletionRule, context: TutorialContext, *, target_clicked: bool = False ) -> bool: match rule.kind: case CompletionKind.MANUAL_NEXT: @@ -284,8 +265,7 @@ def get_nested_value(data: dict[str, Any], path: str, default: Any = None) -> An def ensure_step_runtime_state( - runtime_state: TutorialScenarioRuntimeState, - step_id: str, + runtime_state: TutorialScenarioRuntimeState, step_id: str ) -> TutorialStepRuntimeState: state = runtime_state.step_states.get(step_id) if state is None: @@ -294,9 +274,7 @@ def ensure_step_runtime_state( return state -def build_runtime_state_for_scenario( - scenario: TutorialScenario, -) -> TutorialScenarioRuntimeState: +def build_runtime_state_for_scenario(scenario: TutorialScenario) -> TutorialScenarioRuntimeState: runtime_state = TutorialScenarioRuntimeState(scenario_id=scenario.id) for step in scenario.steps: runtime_state.step_states[step.id] = TutorialStepRuntimeState(step_id=step.id) diff --git a/src/aare/gui/tutorials/tutorial_targets.py b/src/aare/gui/tutorials/tutorial_targets.py index e8f4064e..e5e1ee30 100644 --- a/src/aare/gui/tutorials/tutorial_targets.py +++ b/src/aare/gui/tutorials/tutorial_targets.py @@ -11,9 +11,7 @@ class MainWindowTutorialTargetResolver: self.window = window def resolve_target( - self, - target: TutorialTarget, - context: TutorialContext, + self, target: TutorialTarget, context: TutorialContext ) -> ResolvedTutorialTarget | None: beamline = getattr(self.window, "beamline", None) data_collection = getattr(self.window, "data_collection", None) @@ -38,8 +36,4 @@ class MainWindowTutorialTargetResolver: if widget is None: return None - return ResolvedTutorialTarget( - target_id=target.target_id, - widget=widget, - rect=widget.rect(), - ) + return ResolvedTutorialTarget(target_id=target.target_id, widget=widget, rect=widget.rect()) diff --git a/src/aare/gui/tutorials/tutroial_texts.py b/src/aare/gui/tutorials/tutroial_texts.py index 980d0918..92550bf9 100644 --- a/src/aare/gui/tutorials/tutroial_texts.py +++ b/src/aare/gui/tutorials/tutroial_texts.py @@ -1,22 +1,18 @@ - MANUAL_MOUNT_TUTORIAL = { "en": { "tutorial.manual.title": "Manual workflow tutorial", "tutorial.manual.description": "A guided walkthrough of the manual sample-centering and collection workflow.", - "tutorial.manual.welcome.title": "Welcome", "tutorial.manual.welcome.body": ( "This tutorial walks through the manual workflow using the test sample " "'{sample_name}' in position {sample_position}. " "It is intended to teach the interface and the normal order of operations." ), - "tutorial.manual.sample_list.title": "Sample list", "tutorial.manual.sample_list.body": ( "This is the sample list. For this tutorial, look for '{sample_name}' " "in position {sample_position}." ), - "tutorial.manual.select_sample.title": "Selecting a sample", "tutorial.manual.select_sample.body": ( "Start by selecting '{sample_name}' in the sample list." @@ -24,7 +20,6 @@ MANUAL_MOUNT_TUTORIAL = { "tutorial.manual.select_sample.hint": ( "Later we can validate this step from a real selection event." ), - "tutorial.manual.mount.title": "Mounting the sample", "tutorial.manual.mount.body": ( "To mount the sample, right-click '{sample_name}' and choose the mount action." @@ -32,7 +27,6 @@ MANUAL_MOUNT_TUTORIAL = { "tutorial.manual.mount.hint": ( "This is the real workflow: select the sample, right-click it, and mount it from the context menu." ), - "tutorial.manual.sample_camera.title": "Sample camera", "tutorial.manual.sample_camera.body": ( "This is the main sample camera view. The orange square is the beam marker. " @@ -41,28 +35,23 @@ MANUAL_MOUNT_TUTORIAL = { "tutorial.manual.sample_camera.hint": ( "You can also use the mouse wheel here to rotate omega by 90° steps." ), - "tutorial.manual.beamline_controls.title": "Beamline controls", "tutorial.manual.beamline_controls.body": ( "Additional controls for sample motion, lighting, zoom, and alignment are available in this panel." ), - "tutorial.manual.auto_loop_center.title": "Auto loop centering", "tutorial.manual.auto_loop_center.body": ( "This button runs automatic loop centering. It can help bring the sample into position automatically." ), - "tutorial.manual.ml_box.title": "ML box raster generation", "tutorial.manual.ml_box.body": ( "This button uses the ML box tool to generate a raster region automatically from the camera image." ), - "tutorial.manual.raster_parameters.title": "Raster parameters", "tutorial.manual.raster_parameters.body": ( "Here you can change raster collection parameters such as exposure, transmission, " "grid size, and other raster settings before evaluating the scan." ), - "tutorial.manual.raster_canvas.title": "Drawing a raster", "tutorial.manual.raster_canvas.body": ( "You can draw a raster grid in the sample camera view using the mouse." @@ -70,22 +59,19 @@ MANUAL_MOUNT_TUTORIAL = { "tutorial.manual.raster_canvas.hint": ( "Use the mouse right-click workflow you described to create the raster region on the sample." ), - "tutorial.manual.rotation_parameters.title": "Rotation parameters", "tutorial.manual.rotation_parameters.body": ( "This panel contains rotation scan parameters. You can change the values, " "reload parameters from the database, or override them with user-defined values." ), - "tutorial.manual.simple_collection.title": "Simple data collection", "tutorial.manual.simple_collection.body": ( "This panel contains simplified data-collection parameters for quick collection setup." ), - "tutorial.manual.file_names.title": "File names and paths", "tutorial.manual.file_names.body": ( "Here you can review filenames and output paths before collection. " "This is where you should explain how filenames are constructed and where data will be written." ), } -} \ No newline at end of file +} diff --git a/src/aare/gui/widgets/alert_banner.py b/src/aare/gui/widgets/alert_banner.py index 3bb27e42..adc44902 100644 --- a/src/aare/gui/widgets/alert_banner.py +++ b/src/aare/gui/widgets/alert_banner.py @@ -1,13 +1,7 @@ from aarecommon.config.logger import setup_logger from PySide6.QtCore import Qt, QTimer, Slot from PySide6.QtGui import QColor -from PySide6.QtWidgets import ( - QFrame, - QGraphicsDropShadowEffect, - QHBoxLayout, - QLabel, - QSizePolicy, -) +from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QHBoxLayout, QLabel, QSizePolicy logger = setup_logger("aareGUI") @@ -58,9 +52,7 @@ class AlertBanner(QFrame): self.update() @Slot(str, bool) - def show_message( - self, msg: str, is_error: bool = True, auto_clear_ms: int | None = None - ): + def show_message(self, msg: str, is_error: bool = True, auto_clear_ms: int | None = None): """Show error (red) or success (green) message.""" self._stop_countdown() self._clear_timer.stop() @@ -120,9 +112,7 @@ class AlertBanner(QFrame): def _update_waiting_text(self): """Update the waiting message text, including countdown if active.""" if self._countdown_remaining > 0: - decorated = ( - f"⏳ {self._countdown_base_message} ({self._countdown_remaining}s) ⏳" - ) + decorated = f"⏳ {self._countdown_base_message} ({self._countdown_remaining}s) ⏳" else: decorated = f"⏳ {self._countdown_base_message} ⏳" self._label.setText(decorated) diff --git a/src/aare/gui/widgets/automation_progress.py b/src/aare/gui/widgets/automation_progress.py index 03eaf568..80e0663a 100644 --- a/src/aare/gui/widgets/automation_progress.py +++ b/src/aare/gui/widgets/automation_progress.py @@ -3,11 +3,7 @@ from __future__ import annotations import time from datetime import datetime -from aarecommon.models.automation import ( - AutomationProgress, - StepStatus, - WorkflowStateKind, -) +from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind from PySide6.QtCore import QTimer, Slot from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget @@ -189,6 +185,4 @@ class CompactAutomationProgressStrip(QFrame): states = {step.step: step.status for step in progress.steps} for step, label in self._step_labels.items(): - label.setText( - self._format_step_html(step, states.get(step, StepStatus.PENDING)) - ) + label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING))) diff --git a/src/aare/gui/widgets/baton_request_dialog.py b/src/aare/gui/widgets/baton_request_dialog.py index 9593b8cb..16043c8b 100644 --- a/src/aare/gui/widgets/baton_request_dialog.py +++ b/src/aare/gui/widgets/baton_request_dialog.py @@ -1,7 +1,12 @@ from PySide6.QtCore import Qt, Signal, QTimer from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QProgressBar, QFrame + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QProgressBar, + QFrame, ) from PySide6.QtGui import QFont @@ -24,10 +29,7 @@ class BatonRequestDialog(QDialog): self.setWindowTitle("⚡ Baton Request") self.setModal(False) # Non-modal so user can see beamline status self.setMinimumWidth(400) - self.setWindowFlags( - self.windowFlags() | - Qt.WindowType.WindowStaysOnTopHint - ) + self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint) self._timeout = timeout_seconds self._remaining = timeout_seconds @@ -92,9 +94,7 @@ class BatonRequestDialog(QDialog): layout.addLayout(progress_layout) # Warning about auto-transfer - self.warning_label = QLabel( - "⚠️ If you don't respond, control will transfer automatically." - ) + self.warning_label = QLabel("⚠️ If you don't respond, control will transfer automatically.") self.warning_label.setWordWrap(True) self.warning_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.warning_label.setStyleSheet("color: #ff9800; font-style: italic;") @@ -219,11 +219,13 @@ class BatonRequestDialog(QDialog): self._timer.stop() super().closeEvent(event) + class BatonPendingDialog(QDialog): """ Dialog shown to the user who requested the baton while they wait for a response or for the beamline queue to clear. """ + cancelled_signal = Signal() def __init__(self, target_user: str, timeout_seconds: int = 30, parent=None): @@ -257,9 +259,7 @@ class BatonPendingDialog(QDialog): line.setFrameShadow(QFrame.Shadow.Sunken) layout.addWidget(line) - self.message_label = QLabel( - f"Waiting for {self._target_user} to respond..." - ) + self.message_label = QLabel(f"Waiting for {self._target_user} to respond...") self.message_label.setWordWrap(True) self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.message_label) @@ -351,4 +351,4 @@ class BatonPendingDialog(QDialog): def closeEvent(self, event): self._timer.stop() - super().closeEvent(event) \ No newline at end of file + super().closeEvent(event) diff --git a/src/aare/gui/widgets/busy_overlay.py b/src/aare/gui/widgets/busy_overlay.py index 217a6f6d..d95d5a20 100644 --- a/src/aare/gui/widgets/busy_overlay.py +++ b/src/aare/gui/widgets/busy_overlay.py @@ -33,10 +33,7 @@ def build_busy_overlay_style( accent_dot="#fff6bf", ) - if session_state in { - SessionsStateEnum.OwnedByElse, - SessionsStateEnum.PendingYouToElse, - }: + if session_state in {SessionsStateEnum.OwnedByElse, SessionsStateEnum.PendingYouToElse}: return BusyOverlayStyle( text="GUEST MODE", badge_bg="#8e44ad", @@ -50,9 +47,7 @@ def build_busy_overlay_style( if not is_busy: return None - activity_value = str( - getattr(getattr(tell_state, "activity", None), "value", "") or "" - ).lower() + activity_value = str(getattr(getattr(tell_state, "activity", None), "value", "") or "").lower() if activity_value == "mounting": return BusyOverlayStyle( diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index d9be90da..e769119f 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -276,9 +276,7 @@ class SampleCameraImageLabel(QGraphicsView): painter.drawRoundedRect(bg_rect, 10, 10) painter.setPen(QPen(style.overlay_text, 2, Qt.PenStyle.SolidLine)) - text_pos = QPoint( - position_x + padding_x, position_y + padding_y + font_metrics.ascent() - ) + text_pos = QPoint(position_x + padding_x, position_y + padding_y + font_metrics.ascent()) painter.drawText(text_pos, style.text) painter.restore() @@ -330,9 +328,7 @@ class SampleCameraImageLabel(QGraphicsView): painter.drawRoundedRect(bg_rect, 10, 10) painter.setPen(QPen(QColor(255, 255, 255))) - painter.drawText( - QPoint(position_x + padding, position_y + padding + fm.ascent()), text - ) + painter.drawText(QPoint(position_x + padding, position_y + padding + fm.ascent()), text) painter.restore() @@ -390,18 +386,13 @@ class SampleCameraImageLabel(QGraphicsView): def mousePressEvent(self, event): if not self.__camera_interaction_enabled(): - if event.button() in ( - Qt.MouseButton.LeftButton, - Qt.MouseButton.RightButton, - ): + if event.button() in (Qt.MouseButton.LeftButton, Qt.MouseButton.RightButton): self.__show_camera_unavailable_tooltip(event) event.accept() return self.start_point = self.mapToScene(event.pos()) - ctrl_override_move = bool( - event.modifiers() & Qt.KeyboardModifier.ControlModifier - ) + ctrl_override_move = bool(event.modifiers() & Qt.KeyboardModifier.ControlModifier) match self.__state: case SampleCameraImageState.BEAM_MARKING: @@ -451,9 +442,7 @@ class SampleCameraImageLabel(QGraphicsView): QToolTip.showText(self.mapToGlobal(event.pos()), grid_txt, self) elif self.__show_coords: QToolTip.showText( - self.mapToGlobal(event.pos()), - f"{mouse_pos.x():.0f}, {mouse_pos.y():.0f} pxl", - self, + self.mapToGlobal(event.pos()), f"{mouse_pos.x():.0f}, {mouse_pos.y():.0f} pxl", self ) if self.__state == SampleCameraImageState.IDLE: @@ -574,11 +563,7 @@ class SampleCameraImageLabel(QGraphicsView): elif action == autofocus_action: self.autofocus.emit( AutofocusSettings( - center_x_pxl=None, - center_y_pxl=None, - radius_pxl=30, - z_range_um=2000, - z_steps=10, + center_x_pxl=None, center_y_pxl=None, radius_pxl=30, z_range_um=2000, z_steps=10 ) ) elif action == beam_mark_action: @@ -588,10 +573,7 @@ class SampleCameraImageLabel(QGraphicsView): def __screenshot_with_dialog(self, overlay: bool): file_path, _ = QFileDialog.getSaveFileName( - self, - "Save View As", - "", - "JPEG Files (*.jpg; *.jpeg);;All Files (*)", + self, "Save View As", "", "JPEG Files (*.jpg; *.jpeg);;All Files (*)" ) if file_path: @@ -626,13 +608,8 @@ class SampleCameraImageLabel(QGraphicsView): ): ratio = 1.0 else: - ratio_w = ( - self.viewport().size().width() / self.pixmap_item.boundingRect().width() - ) - ratio_h = ( - self.viewport().size().height() - / self.pixmap_item.boundingRect().height() - ) + ratio_w = self.viewport().size().width() / self.pixmap_item.boundingRect().width() + ratio_h = self.viewport().size().height() / self.pixmap_item.boundingRect().height() ratio = min(ratio_w, ratio_h) if ratio < 0.1: @@ -669,9 +646,7 @@ class SampleCameraImageLabel(QGraphicsView): self.update() new_busy_style = build_busy_overlay_style( - is_busy=bool(s.busy), - tell_state=s.tell_state, - session_state=self.__session_state, + is_busy=bool(s.busy), tell_state=s.tell_state, session_state=self.__session_state ) if new_busy_style != self.__busy_overlay_style: self.__busy_overlay_style = new_busy_style @@ -768,18 +743,13 @@ class SampleCameraImageLabel(QGraphicsView): pen = QPen(color, 3) painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) - detection_rect = QRect( - int(x1), int(y1), int(max(1, x2 - x1)), int(max(1, y2 - y1)) - ) + detection_rect = QRect(int(x1), int(y1), int(max(1, x2 - x1)), int(max(1, y2 - y1))) painter.drawRect(detection_rect) # Only draw polygon if the separate toggle is enabled if self.__show_detection_polygons and poly and len(poly) >= 3: polygon = QPolygonF( - [ - QPointF(x1 + float(p[0]) * sx, y1 + float(p[1]) * sy) - for p in poly - ] + [QPointF(x1 + float(p[0]) * sx, y1 + float(p[1]) * sy) for p in poly] ) painter.drawPolygon(polygon) @@ -828,9 +798,7 @@ class SampleCameraImageLabel(QGraphicsView): img_h, img_w = int(shape[0]), int(shape[1]) tx, ty = self.__smoothed_target_point except Exception as e: - logger.debug( - f"Error using smoothed target point {self.__smoothed_target_point}: {e}" - ) + logger.debug(f"Error using smoothed target point {self.__smoothed_target_point}: {e}") return sx = pix.width() / float(img_w) @@ -873,9 +841,7 @@ class SampleCameraImageLabel(QGraphicsView): fm = QFontMetrics(font) text_rect = fm.boundingRect(label_text) - bubble_rect = QRectF( - px + 16, py - 28, text_rect.width() + 16, text_rect.height() + 10 - ) + bubble_rect = QRectF(px + 16, py - 28, text_rect.width() + 16, text_rect.height() + 10) painter.setPen(QPen(color, 2)) painter.setBrush(QColor(20, 20, 20, 190)) @@ -883,8 +849,7 @@ class SampleCameraImageLabel(QGraphicsView): painter.setPen(QPen(QColor(255, 255, 255), 1)) painter.drawText( - QPointF(bubble_rect.left() + 8, bubble_rect.top() + 7 + fm.ascent()), - label_text, + QPointF(bubble_rect.left() + 8, bubble_rect.top() + 7 + fm.ascent()), label_text ) painter.restore() @@ -992,12 +957,7 @@ class SampleCameraImageLabel(QGraphicsView): painter.setPen(QPen(color, 2)) painter.setBrush(color) painter.drawRect( - QRectF( - bg_rect.left() + section_padding, - y + 2, - swatch_size, - swatch_size, - ) + QRectF(bg_rect.left() + section_padding, y + 2, swatch_size, swatch_size) ) else: painter.setPen(Qt.PenStyle.NoPen) @@ -1162,10 +1122,7 @@ class SampleCameraImageLabel(QGraphicsView): mouse_scene_pos = self.mapToScene(mouse_view_pos) if event.key() == Qt.Key.Key_Shift: - if ( - self.__state == SampleCameraImageState.IDLE - and self.__camera_interaction_enabled() - ): + if self.__state == SampleCameraImageState.IDLE and self.__camera_interaction_enabled(): if not self.raster_timer.isActive(): self.load_image.emit(mouse_scene_pos) self.raster_timer.start(self.raster_timer_interval) @@ -1178,46 +1135,34 @@ class SampleCameraImageLabel(QGraphicsView): match self.__state: case SampleCameraImageState.BEAM_MARKING: if event.modifiers() & Qt.KeyboardModifier.AltModifier: - new_exp_time = self.__sam_cam.exposure * ( - 1.0 + math.copysign(0.1, delta_y) - ) + new_exp_time = self.__sam_cam.exposure * (1.0 + math.copysign(0.1, delta_y)) new_settings = SampleCameraSettings( exposure=new_exp_time, gain=self.__sam_cam.gain ) self.samcam_updated.emit(new_settings) else: - new_exp_time = self.__sam_cam.exposure * ( - 1.0 + math.copysign(0.5, delta_y) - ) + new_exp_time = self.__sam_cam.exposure * (1.0 + math.copysign(0.5, delta_y)) new_settings = SampleCameraSettings( exposure=new_exp_time, gain=self.__sam_cam.gain ) self.samcam_updated.emit(new_settings) case SampleCameraImageState.IDLE: if event.modifiers() & Qt.KeyboardModifier.AltModifier: - new_exp_time = self.__sam_cam.exposure * ( - 1.0 + math.copysign(0.1, delta_y) - ) + new_exp_time = self.__sam_cam.exposure * (1.0 + math.copysign(0.1, delta_y)) new_settings = SampleCameraSettings( exposure=new_exp_time, gain=self.__sam_cam.gain ) self.samcam_updated.emit(new_settings) elif event.modifiers() & Qt.KeyboardModifier.ControlModifier: - new_exp_time = self.__sam_cam.exposure * ( - 1.0 + math.copysign(0.5, delta_y) - ) + new_exp_time = self.__sam_cam.exposure * (1.0 + math.copysign(0.5, delta_y)) new_settings = SampleCameraSettings( exposure=new_exp_time, gain=self.__sam_cam.gain ) self.samcam_updated.emit(new_settings) elif event.modifiers() & Qt.KeyboardModifier.ShiftModifier: - self.set_omega.emit( - self.__geom.omega_deg + math.copysign(10.0, delta_y) - ) + self.set_omega.emit(self.__geom.omega_deg + math.copysign(10.0, delta_y)) else: - self.set_omega.emit( - self.__geom.omega_deg + math.copysign(90.0, delta_y) - ) + self.set_omega.emit(self.__geom.omega_deg + math.copysign(90.0, delta_y)) # Start the timer to throttle further events self.wheel_event_timer.start(self.wheel_event_threshold) diff --git a/src/aare/gui/widgets/clickable_label.py b/src/aare/gui/widgets/clickable_label.py index e4182bd5..97d34aa8 100644 --- a/src/aare/gui/widgets/clickable_label.py +++ b/src/aare/gui/widgets/clickable_label.py @@ -9,4 +9,4 @@ class ClickableLabel(QLabel): if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit() else: - super().mousePressEvent(event) \ No newline at end of file + super().mousePressEvent(event) diff --git a/src/aare/gui/widgets/local_contact_status_widget.py b/src/aare/gui/widgets/local_contact_status_widget.py index db27f08e..e9bae8db 100644 --- a/src/aare/gui/widgets/local_contact_status_widget.py +++ b/src/aare/gui/widgets/local_contact_status_widget.py @@ -4,13 +4,7 @@ from collections.abc import Iterable from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import ( - QFrame, - QGridLayout, - QLabel, - QSizePolicy, - QVBoxLayout, -) +from PySide6.QtWidgets import QFrame, QGridLayout, QLabel, QSizePolicy, QVBoxLayout from aare.gui.widgets.title_label import TitleLabel @@ -98,9 +92,7 @@ class LocalContactStatusWidget(QFrame): self._summary = QLabel(summary, self) self._summary.setWordWrap(True) - self._summary.setStyleSheet( - "border: none; background: transparent; color: #334155;" - ) + self._summary.setStyleSheet("border: none; background: transparent; color: #334155;") layout.addWidget(self._summary) self._grid = QGridLayout() @@ -131,18 +123,14 @@ class LocalContactStatusWidget(QFrame): self._row_widgets.clear() for row, key in enumerate(self._visible_fields): - title = QLabel( - self.FIELD_TITLES.get(key, key.replace("_", " ").title()), self - ) + title = QLabel(self.FIELD_TITLES.get(key, key.replace("_", " ").title()), self) title.setStyleSheet( "font-weight: 700; border: none; background: transparent; color: #1e293b;" ) value = QLabel(self._badge("WAITING", tone="neutral"), self) value.setWordWrap(True) value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) - value.setStyleSheet( - "border: none; background: transparent; color: #334155;" - ) + value.setStyleSheet("border: none; background: transparent; color: #334155;") self._row_widgets[key] = (title, value) self._grid.addWidget(title, row, 0, alignment=Qt.AlignmentFlag.AlignTop) self._grid.addWidget(value, row, 1) @@ -162,11 +150,7 @@ class LocalContactStatusWidget(QFrame): ) def _format_bool( - self, - value: bool | None, - *, - true_text: str = "CONNECTED", - false_text: str = "DISCONNECTED", + self, value: bool | None, *, true_text: str = "CONNECTED", false_text: str = "DISCONNECTED" ) -> str: if value is True: return self._badge(true_text, tone="good") @@ -280,18 +264,10 @@ class LocalContactStatusWidget(QFrame): return f"({box.top_x:.1f}, {box.top_y:.1f}) → ({box.bottom_x:.1f}, {box.bottom_y:.1f})" if key == "last_best_res": value = getattr(status, "last_best_res", None) - return ( - f"{value:.3f} Å" - if value is not None - else self._badge("NONE", tone="neutral") - ) + return f"{value:.3f} Å" if value is not None else self._badge("NONE", tone="neutral") if key == "last_best_b_factor": value = getattr(status, "last_best_b_factor", None) - return ( - f"{value:.3f}" - if value is not None - else self._badge("NONE", tone="neutral") - ) + return f"{value:.3f}" if value is not None else self._badge("NONE", tone="neutral") if key == "crystal_size": crystal = getattr(status, "crystal_size", None) if crystal is None: @@ -309,15 +285,11 @@ class LocalContactStatusWidget(QFrame): return f"{getattr(status.bl, 'cryojet_K', 0.0):.2f} K" if key == "shutter": return self._format_bool( - getattr(status.bl, "shutter_open", None), - true_text="OPEN", - false_text="CLOSED", + getattr(status.bl, "shutter_open", None), true_text="OPEN", false_text="CLOSED" ) if key == "exposure_shutter": return self._format_bool( - getattr(status.bl, "exp_shutter_open", None), - true_text="OPEN", - false_text="CLOSED", + getattr(status.bl, "exp_shutter_open", None), true_text="OPEN", false_text="CLOSED" ) if key == "flux": return f"{getattr(status.bl, 'flux_ph_s', 0.0):.3g} ph/s" @@ -332,9 +304,7 @@ class LocalContactStatusWidget(QFrame): return f"{getattr(status.bl, 'zoom', 0.0):.3f}" if key == "commissioning_mode": return self._format_bool( - getattr(status.bl, "commissioning_mode", None), - true_text="ON", - false_text="OFF", + getattr(status.bl, "commissioning_mode", None), true_text="ON", false_text="OFF" ) if key == "omega": return f"{getattr(status.geom, 'omega_deg', 0.0):.3f}°" diff --git a/src/aare/gui/widgets/login.py b/src/aare/gui/widgets/login.py index e9c7ac2a..be807a31 100644 --- a/src/aare/gui/widgets/login.py +++ b/src/aare/gui/widgets/login.py @@ -6,14 +6,7 @@ from aarecommon.models.auth import get_user from aarecommon.models.models import TokenData from PySide6.QtCore import QByteArray, QUrl, QUrlQuery, Slot from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest -from PySide6.QtWidgets import ( - QDialog, - QHBoxLayout, - QLabel, - QLineEdit, - QPushButton, - QVBoxLayout, -) +from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout class LoginDialog(QDialog): @@ -42,13 +35,9 @@ class LoginDialog(QDialog): # Buttons button_layout = QHBoxLayout() self.ok_button = QPushButton("OK") - self.ok_button.clicked.connect( - self.authenticate - ) # Close dialog with accept status + self.ok_button.clicked.connect(self.authenticate) # Close dialog with accept status self.cancel_button = QPushButton("Cancel") - self.cancel_button.clicked.connect( - self.reject - ) # Close dialog with reject status + self.cancel_button.clicked.connect(self.reject) # Close dialog with reject status button_layout.addWidget(self.ok_button) button_layout.addWidget(self.cancel_button) @@ -58,18 +47,14 @@ class LoginDialog(QDialog): def authenticate(self): if self.__base_url is None: token_data = TokenData( - sub=self.name_entry.text(), - staff=True, - session=15, - pgroups=["p16371", "p22233"], + sub=self.name_entry.text(), staff=True, session=15, pgroups=["p16371", "p22233"] ) self.token = jwt.encode(token_data.model_dump(), "ABC123") self.accept() self.__network_manager = QNetworkAccessManager(self) request = QNetworkRequest(QUrl(f"{self.__base_url}/token")) request.setHeader( - QNetworkRequest.KnownHeaders.ContentTypeHeader, - "application/x-www-form-urlencoded", + QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/x-www-form-urlencoded" ) payload = QUrlQuery() @@ -83,10 +68,7 @@ class LoginDialog(QDialog): @Slot() def handle_token_response(self): - if ( - self.__reply is not None - and self.__reply.error() == QNetworkReply.NetworkError.NoError - ): + if self.__reply is not None and self.__reply.error() == QNetworkReply.NetworkError.NoError: response_data = self.__reply.readAll().data() response_json = json.loads(response_data.decode("utf-8")) if "access_token" in response_json: diff --git a/src/aare/gui/widgets/message_box.py b/src/aare/gui/widgets/message_box.py index ba355800..9cf7cd6c 100644 --- a/src/aare/gui/widgets/message_box.py +++ b/src/aare/gui/widgets/message_box.py @@ -44,9 +44,7 @@ def precondition_problems(ring_current, shutter_open, door_prohibited) -> list[s if not shutter_open: problems.append("Experiment safety shutter is closed.") if door_prohibited is False: - problems.append( - "Hutch is not in the prohibited state (doors open / not searched)." - ) + problems.append("Hutch is not in the prohibited state (doors open / not searched).") return problems @@ -68,9 +66,7 @@ def precondition_check(parent, *, ring_current, shutter_open, door_prohibited) - box.setIcon(QMessageBox.Icon.Warning) box.setWindowTitle("Beamline not ready") box.setText("\n".join(problems) + "\n\nDo you wish to continue?") - box.setStandardButtons( - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) + box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) box.setDefaultButton(QMessageBox.StandardButton.No) snooze_cb = QCheckBox("Don't ask me again for 1 hour") box.setCheckBox(snooze_cb) @@ -97,9 +93,7 @@ def timer_box( box = QMessageBox(parent) box.setWindowTitle(title) box.setText(f"{msg} Do you wish to continue?") - box.setStandardButtons( - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) + box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) box.show() timer = QTimer(box) @@ -156,9 +150,7 @@ def experiment_hutch_shutter_check(parent, shutter_state) -> bool: return True else: reply = reply_box( - parent, - title="Experiment shutter open", - msg=f"Experiment shutter is Closed.", + parent, title="Experiment shutter open", msg=f"Experiment shutter is Closed." ) return reply == QMessageBox.StandardButton.Yes @@ -172,9 +164,7 @@ def ring_current_auto_check(parent, ring_current, check_func) -> bool: return conditions_auto_check(parent, msg, check_func, title="Ring current too low") -def conditions_auto_check( - parent, msg: str, check_func, title: str = "Beamline not ready" -) -> bool: +def conditions_auto_check(parent, msg: str, check_func, title: str = "Beamline not ready") -> bool: """Pause-and-wait dialog that auto-resumes when ``check_func()`` becomes True. Used to hold automation between samples until the beamline recovers. The diff --git a/src/aare/gui/widgets/number_line_edit.py b/src/aare/gui/widgets/number_line_edit.py index ae9f7076..fd79f088 100644 --- a/src/aare/gui/widgets/number_line_edit.py +++ b/src/aare/gui/widgets/number_line_edit.py @@ -2,16 +2,12 @@ from PySide6.QtCore import Signal, Slot, Qt from PySide6.QtGui import QDoubleValidator from PySide6.QtWidgets import QLineEdit, QWidget, QCheckBox, QHBoxLayout + class NumberLineEdit(QLineEdit): newValue = Signal(float) def __init__( - self, - min_val: float, - max_val: float, - default: float = 0.0, - decimals: int = 2, - parent=None, + self, min_val: float, max_val: float, default: float = 0.0, decimals: int = 2, parent=None ): super().__init__(parent) self._read_only: bool = False @@ -27,9 +23,7 @@ class NumberLineEdit(QLineEdit): self.setValidator(self.validator) self.setAlignment(Qt.AlignmentFlag.AlignRight) self.setToolTip( - "Minimum: {:s}\nMaximum: {:s}".format( - self.to_string(min_val), self.to_string(max_val) - ) + "Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val)) ) # Connect the textChanged signal to a custom slot to check validity @@ -45,7 +39,7 @@ class NumberLineEdit(QLineEdit): @Slot(str) def on_text_changed(self, text: str): - #when text changes check validation and change the colour of the line edit + # when text changes check validation and change the colour of the line edit self._is_valid = self.validate(text) if self._is_valid: self.setStyleSheet("background-color: rgb(255, 255, 255);") @@ -81,16 +75,14 @@ class NumberLineEdit(QLineEdit): def update_limits(self, min_val: float, max_val: float): self.validator.setRange(min_val, max_val, self.decimal_count) self.setToolTip( - "Minimum: {:s}\nMaximum: {:s}".format( - self.to_string(min_val), self.to_string(max_val) - ) + "Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val)) ) def validate(self, text) -> bool: return self.validator.validate(str(text), 0)[0] == QDoubleValidator.State.Acceptable def setReadOnly(self, ro: bool): - #change state of read only and change colour of line edit based on read only state and validator + # change state of read only and change colour of line edit based on read only state and validator super().setReadOnly(ro) self._read_only = ro if self._read_only and self._is_valid: @@ -102,7 +94,9 @@ class NumberLineEdit(QLineEdit): elif not self._read_only and not self._is_valid: self.setStyleSheet("background-color: rgb(255, 213, 213);") else: - print(f"unknown ro state: {self._read_only} or validity {self._is_valid} default to writeable") + print( + f"unknown ro state: {self._read_only} or validity {self._is_valid} default to writeable" + ) self.setStyleSheet("background-color: rgb(255, 255, 255);") def get_default(self) -> float: @@ -117,8 +111,17 @@ class CheckedLineEdit(QWidget): newValue = Signal(float) readOnlyChanged = Signal(bool) - def __init__(self, min_val: float, max_val: float, /, default: float = 0.0, decimals: int = 2, - check_box_text: str ="", start_checked: bool = False, parent = None): + def __init__( + self, + min_val: float, + max_val: float, + /, + default: float = 0.0, + decimals: int = 2, + check_box_text: str = "", + start_checked: bool = False, + parent=None, + ): super().__init__(parent) self._busy = False self._checked = start_checked @@ -146,7 +149,7 @@ class CheckedLineEdit(QWidget): self._external_value = value_float def _on_checked_toggled(self, checked: bool): - #when check box is clicked, change read only state of editor + # when check box is clicked, change read only state of editor self._checked = checked if checked: self.editor.force_update_value(self._internal_value) @@ -156,7 +159,7 @@ class CheckedLineEdit(QWidget): def set_busy(self, busy: bool): # if an external wants to change the busy state. - #update self._busy and change editor read only state. After enable/disable check_box and set colour + # update self._busy and change editor read only state. After enable/disable check_box and set colour self.blockSignals(True) self._busy = busy self.setReadOnly() @@ -175,14 +178,14 @@ class CheckedLineEdit(QWidget): self.blockSignals(False) def update_value(self, value): - #wrapper for NumberLineEdit.update_value + # wrapper for NumberLineEdit.update_value self._external_value = value if not self._checked: self.editor.update_value(value) self._on_editor_value_changed(value) def force_update_value(self, value): - #wrapper for NumberLineEdit.force_update_value + # wrapper for NumberLineEdit.force_update_value self._external_value = value if not self._checked: self.editor.force_update_value(value) @@ -190,15 +193,15 @@ class CheckedLineEdit(QWidget): @property def value(self) -> float: - #wrapper for NumberLineEdit.value + # wrapper for NumberLineEdit.value return self.editor.value def update_limits(self, min_val: float, max_val: float): - #wrapper for NumberLineEdit.update_limits + # wrapper for NumberLineEdit.update_limits self.editor.update_limits(min_val, max_val) def isChecked(self) -> bool: - #wrapper for check_box.isChecked() + # wrapper for check_box.isChecked() return self.check_box.isChecked() def isReadOnly(self) -> bool: @@ -211,11 +214,13 @@ class CheckedLineEdit(QWidget): def setReadOnly(self): # Change editor read only state based on self._busy and self._checked should_be_ro = True - if not self._busy and self._checked: # Only set editor to writeable if busy is False and the checkbox is checked, + if ( + not self._busy and self._checked + ): # Only set editor to writeable if busy is False and the checkbox is checked, should_be_ro = False self.editor.setReadOnly(False) - #state_changed = self.editor.isReadOnly() != should_be_ro + # state_changed = self.editor.isReadOnly() != should_be_ro self.editor.setReadOnly(should_be_ro) self.readOnlyChanged.emit(should_be_ro) @@ -254,8 +259,15 @@ class DbOverrideLineEdit(QWidget): SOURCE_DB = "db" SOURCE_MINE = "mine" - def __init__(self, min_val: float, max_val: float, /, default: float = 0.0, - decimals: int = 2, parent=None): + def __init__( + self, + min_val: float, + max_val: float, + /, + default: float = 0.0, + decimals: int = 2, + parent=None, + ): super().__init__(parent) self._default = float(default) self._db_value = float(default) diff --git a/src/aare/gui/widgets/pgroup_dialog.py b/src/aare/gui/widgets/pgroup_dialog.py index 88387dcb..11e9b0ea 100644 --- a/src/aare/gui/widgets/pgroup_dialog.py +++ b/src/aare/gui/widgets/pgroup_dialog.py @@ -1,17 +1,26 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QPushButton, QLabel, - QComboBox, QCompleter, QMessageBox + QDialog, + QVBoxLayout, + QPushButton, + QLabel, + QComboBox, + QCompleter, + QMessageBox, ) class PGroupDialog(QDialog): - def __init__(self, curr_pgroup: str | None = None, pgroups: list[str] | None = None, parent=None): + def __init__( + self, curr_pgroup: str | None = None, pgroups: list[str] | None = None, parent=None + ): super().__init__(parent) self.setWindowTitle("Change current p-group") self.setMinimumWidth(300) - self._pgroups = [str(p).strip() for p in (pgroups or []) if p is not None and str(p).strip()] + self._pgroups = [ + str(p).strip() for p in (pgroups or []) if p is not None and str(p).strip() + ] layout = QVBoxLayout(self) @@ -101,7 +110,7 @@ class PGroupDialog(QDialog): self, "Invalid P-Group", f"P-group '{entered_text}' is not in your allowed list.\n" - f"Please select from: {', '.join(self._pgroups)}" + f"Please select from: {', '.join(self._pgroups)}", ) self.combo.setFocus() if self.combo.lineEdit() is not None: @@ -114,4 +123,4 @@ class PGroupDialog(QDialog): def get_input(self): """Return the input text when the dialog is accepted.""" - return (self.combo.currentText() or "").strip() \ No newline at end of file + return (self.combo.currentText() or "").strip() diff --git a/src/aare/gui/widgets/raster_grid_table.py b/src/aare/gui/widgets/raster_grid_table.py index 3a3d91a5..714f7205 100644 --- a/src/aare/gui/widgets/raster_grid_table.py +++ b/src/aare/gui/widgets/raster_grid_table.py @@ -1,5 +1,12 @@ from PySide6.QtCore import Signal, Slot -from PySide6.QtWidgets import QTableWidget, QHeaderView, QTableWidgetItem, QWidget, QHBoxLayout, QPushButton +from PySide6.QtWidgets import ( + QTableWidget, + QHeaderView, + QTableWidgetItem, + QWidget, + QHBoxLayout, + QPushButton, +) from aare.gui.scan_logic.raster_grid_manager import RasterGridManager @@ -15,7 +22,7 @@ class RasterGridTable(QTableWidget): self.__raster_mgr = raster_mgr self.setColumnCount(5) - self.setHorizontalHeaderLabels(['Omega', 'Chi', 'Phi', 'Size (WxH)', 'Actions']) + self.setHorizontalHeaderLabels(["Omega", "Chi", "Phi", "Size (WxH)", "Actions"]) self.verticalHeader().setVisible(False) @@ -51,7 +58,6 @@ class RasterGridTable(QTableWidget): # Add action buttons self.add_action_buttons(row_position, req.visible) - def add_action_buttons(self, row: int, visible: bool): """Create and add action buttons for the specified row.""" actions_widget = QWidget() @@ -72,39 +78,41 @@ class RasterGridTable(QTableWidget): """ # Copy button - copy_btn = QPushButton('⧉') + copy_btn = QPushButton("⧉") copy_btn.setFixedSize(24, 24) - copy_btn.setToolTip('Copy') + copy_btn.setToolTip("Copy") copy_btn.setStyleSheet(button_style) copy_btn.clicked.connect(lambda checked, r=row: self.copy_clicked.emit(r)) actions_layout.addWidget(copy_btn) # Redo button - goto_btn = QPushButton('🏃‍♂️') # Circular arrow for goto + goto_btn = QPushButton("🏃‍♂️") # Circular arrow for goto goto_btn.setFixedSize(24, 24) - goto_btn.setToolTip('Goto scan') + goto_btn.setToolTip("Goto scan") goto_btn.setStyleSheet(button_style) goto_btn.clicked.connect(lambda checked, r=row: self.goto_clicked.emit(r)) actions_layout.addWidget(goto_btn) # Visible button if visible: - visible_btn = QPushButton('👁') + visible_btn = QPushButton("👁") else: - visible_btn = QPushButton('‍🗨') + visible_btn = QPushButton("‍🗨") visible_btn.setChecked(visible) # Initially visible visible_btn.setFixedSize(24, 24) - visible_btn.setToolTip('Toggle visibility') + visible_btn.setToolTip("Toggle visibility") visible_btn.setStyleSheet(button_style) visible_btn.setCheckable(True) - visible_btn.clicked.connect(lambda checked, r=row: self.visible_clicked.emit(r, not visible)) + visible_btn.clicked.connect( + lambda checked, r=row: self.visible_clicked.emit(r, not visible) + ) actions_layout.addWidget(visible_btn) # Delete button - delete_btn = QPushButton('🗑') + delete_btn = QPushButton("🗑") delete_btn.setFixedSize(24, 24) - delete_btn.setToolTip('Delete') + delete_btn.setToolTip("Delete") delete_btn.setStyleSheet(button_style) delete_btn.clicked.connect(lambda checked, r=row: self.delete_clicked.emit(r)) actions_layout.addWidget(delete_btn) diff --git a/src/aare/gui/widgets/splash_screen.py b/src/aare/gui/widgets/splash_screen.py index 6cc759f4..2c7da0d4 100644 --- a/src/aare/gui/widgets/splash_screen.py +++ b/src/aare/gui/widgets/splash_screen.py @@ -26,4 +26,4 @@ class LoadingSplashScreen(QSplashScreen): self.progress.setValue(value) if message: self.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white) - QApplication.processEvents() \ No newline at end of file + QApplication.processEvents() diff --git a/src/aare/gui/widgets/status_bar.py b/src/aare/gui/widgets/status_bar.py index b9a538d7..5752da94 100644 --- a/src/aare/gui/widgets/status_bar.py +++ b/src/aare/gui/widgets/status_bar.py @@ -2,22 +2,10 @@ import math from aarecommon.config.logger import setup_logger from aarecommon.models.auth import BatonRequestStatus, BatonStatus -from aarecommon.models.models import ( - BeamlineStateEnum, - DAQStatusModel, - SessionsStateEnum, - TokenData, -) +from aarecommon.models.models import BeamlineStateEnum, DAQStatusModel, SessionsStateEnum, TokenData from PySide6.QtCore import QPoint, QTimer, Signal, Slot from PySide6.QtGui import QFont -from PySide6.QtWidgets import ( - QDialog, - QLabel, - QMenu, - QMessageBox, - QSizePolicy, - QStatusBar, -) +from PySide6.QtWidgets import QDialog, QLabel, QMenu, QMessageBox, QSizePolicy, QStatusBar from aare.gui.widgets.baton_request_dialog import BatonRequestDialog from aare.gui.widgets.clickable_label import ClickableLabel @@ -68,9 +56,7 @@ class StatusBar(QStatusBar): self.message_label = QLabel("", self) self.message_label.setVisible(False) - self.message_label.setSizePolicy( - QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred - ) + self.message_label.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) self.sharpness = ValueLabel("Samcam image sharpness", "", self) self.samcam_fps = ValueLabel("Samcam FPS", "fps", self) @@ -161,9 +147,7 @@ class StatusBar(QStatusBar): if status.bl.ring_current_mA < 5.0: self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}", "red") elif status.bl.ring_current_mA < 390.0: - self.ring_current.set_value( - f"{status.bl.ring_current_mA:.2f}", "orange" - ) + self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}", "orange") else: self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}") @@ -196,9 +180,7 @@ class StatusBar(QStatusBar): ) if status.session.current_pgroup is not None: - self.pgroup_label.setText( - f"""p-group: {status.session.current_pgroup} """ - ) + self.pgroup_label.setText(f"""p-group: {status.session.current_pgroup} """) else: self.pgroup_label.setText(f"Inactive p-group ") @@ -241,9 +223,7 @@ class StatusBar(QStatusBar): elif status.session.session == SessionsStateEnum.OwnedByElse: session_flag = """ Other 🔒 """ elif status.session.session == SessionsStateEnum.PendingYouToElse: - session_flag = ( - """ Waiting... ⏳ """ - ) + session_flag = """ Waiting... ⏳ """ elif status.session.session == SessionsStateEnum.PendingElseToYou: session_flag = """ Request! ⚡ """ @@ -279,9 +259,7 @@ class StatusBar(QStatusBar): logger.info(f"Incoming baton request detected: {status.pending_request}") self._emit_incoming_baton_request(status) elif not incoming and self._baton_request_dialog is not None: - logger.info( - "Baton request no longer incoming, closing local dialog reference" - ) + logger.info("Baton request no longer incoming, closing local dialog reference") try: self._baton_request_dialog.close() except Exception as e: @@ -307,12 +285,7 @@ class StatusBar(QStatusBar): requester = status.pending_request.requester_username or requester timeout = int(status.pending_request.timeout_seconds or timeout) - self.baton_request_received.emit( - { - "requester": requester, - "timeout": timeout, - } - ) + self.baton_request_received.emit({"requester": requester, "timeout": timeout}) @Slot() def _on_baton_dialog_accepted(self): @@ -354,9 +327,7 @@ class StatusBar(QStatusBar): def show_session_menu(self): menu = QMenu(self) is_busy = self.__status and self.__status.busy - session_state = ( - self.__status.session.session if self.__status else SessionsStateEnum.Vacant - ) + session_state = self.__status.session.session if self.__status else SessionsStateEnum.Vacant # Determine if we are the holder or waiting for baton is_yours = session_state in ( SessionsStateEnum.OwnedByYou, @@ -364,9 +335,7 @@ class StatusBar(QStatusBar): ) # Check baton status for fallback if status.session is not yet updated if not is_yours and self._baton_status: - is_yours = ( - self._baton_status.you_are_holder or self._baton_status.incoming_request - ) + is_yours = self._baton_status.you_are_holder or self._baton_status.incoming_request is_vacant = session_state == SessionsStateEnum.Vacant is_other = session_state in ( @@ -377,9 +346,7 @@ class StatusBar(QStatusBar): # Determine holder info from baton status holder_is_staff = ( - self._baton_status - and self._baton_status.holder - and self._baton_status.holder.is_staff + self._baton_status and self._baton_status.holder and self._baton_status.holder.is_staff ) # --- GRAB / REQUEST --- @@ -438,10 +405,7 @@ class StatusBar(QStatusBar): label_geometry = self.session_label.geometry() menu_width = max(label_geometry.width(), menu.sizeHint().width()) - menu.move( - self.mapToGlobal(label_geometry.topLeft()) - - QPoint(0, menu.sizeHint().height()) - ) + menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height())) menu.setFixedWidth(menu_width) menu.exec() @@ -456,8 +420,7 @@ class StatusBar(QStatusBar): allow_pgroup_menu = self.__is_staff or in_curr owned_by_you = ( - self.__status - and self.__status.session.session == SessionsStateEnum.OwnedByYou + self.__status and self.__status.session.session == SessionsStateEnum.OwnedByYou ) if allow_pgroup_menu: @@ -470,8 +433,7 @@ class StatusBar(QStatusBar): label_geometry = self.pgroup_label.geometry() menu.move( - self.mapToGlobal(label_geometry.topLeft()) - - QPoint(0, menu.sizeHint().height()) + self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height()) ) menu.setFixedWidth(label_geometry.width()) @@ -486,10 +448,7 @@ class StatusBar(QStatusBar): action_2.triggered.connect(self.open_shutter_clicked) label_geometry = self.shutter_label.geometry() - menu.move( - self.mapToGlobal(label_geometry.topLeft()) - - QPoint(0, menu.sizeHint().height()) - ) + menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height())) menu.setFixedWidth(label_geometry.width()) menu.exec() @@ -523,26 +482,19 @@ class StatusBar(QStatusBar): ]: action_1 = menu.addAction("Sample alignment") action_1.triggered.connect(self.sa) - elif self.__status.state in [ - BeamlineStateEnum.SampleAlignment, - ]: + elif self.__status.state in [BeamlineStateEnum.SampleAlignment]: action_2 = menu.addAction("Manual sample exchange") action_2.triggered.connect(self.se) action_3 = menu.addAction("Dewar transfer") action_3.triggered.connect(self.dl) action_4 = menu.addAction("Beam location") action_4.triggered.connect(self.beam_location) - elif self.__status.state in [ - BeamlineStateEnum.Maintenance, - ]: + elif self.__status.state in [BeamlineStateEnum.Maintenance]: action_2 = menu.addAction("Manual sample exchange") action_2.triggered.connect(self.se) label_geometry = self.state_label.geometry() - menu.move( - self.mapToGlobal(label_geometry.topLeft()) - - QPoint(0, menu.sizeHint().height()) - ) + menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height())) menu.setFixedWidth(label_geometry.width()) menu.exec() @@ -570,18 +522,14 @@ class StatusBar(QStatusBar): logger.debug(self.__decoded_token.pgroups) curr = self.__status.session.current_pgroup pgroups = [ - str(p) - for p in (self.__allowed_pgroups or []) - if p is not None and str(p).strip() + str(p) for p in (self.__allowed_pgroups or []) if p is not None and str(p).strip() ] if self.__is_staff: def _on_loaded(lst: list): try: merged = { - str(p).strip() - for p in (lst or []) - if p is not None and str(p).strip() + str(p).strip() for p in (lst or []) if p is not None and str(p).strip() } if not merged: merged = set(pgroups) @@ -641,9 +589,7 @@ class StatusBar(QStatusBar): self.get_all_pgroups.emit() return - def _generate_pgroup_dialogue( - self, curr: str | None = None, pgroups: list | None = None - ): + def _generate_pgroup_dialogue(self, curr: str | None = None, pgroups: list | None = None): logger.info(pgroups) dialog = PGroupDialog(curr_pgroup=curr, pgroups=pgroups) if dialog.exec() == QDialog.DialogCode.Accepted: diff --git a/src/aare/gui/widgets/text_list_dialog.py b/src/aare/gui/widgets/text_list_dialog.py index 3a92feef..0d548234 100644 --- a/src/aare/gui/widgets/text_list_dialog.py +++ b/src/aare/gui/widgets/text_list_dialog.py @@ -3,13 +3,7 @@ from __future__ import annotations from collections.abc import Iterable from PySide6.QtGui import QGuiApplication -from PySide6.QtWidgets import ( - QDialog, - QDialogButtonBox, - QPushButton, - QTextEdit, - QVBoxLayout, -) +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QPushButton, QTextEdit, QVBoxLayout class TextListDialog(QDialog): @@ -48,4 +42,4 @@ class TextListDialog(QDialog): def _copy_to_clipboard(self) -> None: clipboard = QGuiApplication.clipboard() if clipboard is not None: - clipboard.setText(self._text.toPlainText()) \ No newline at end of file + clipboard.setText(self._text.toPlainText()) diff --git a/src/aare/gui/widgets/value_label.py b/src/aare/gui/widgets/value_label.py index f9632961..c56c50dd 100644 --- a/src/aare/gui/widgets/value_label.py +++ b/src/aare/gui/widgets/value_label.py @@ -4,6 +4,7 @@ from PySide6.QtWidgets import QLabel class ValueLabel(QLabel): clicked = Signal() + def __init__(self, text: str, unit: str = "", parent=None): super().__init__(parent) self.__descr = text @@ -11,7 +12,9 @@ class ValueLabel(QLabel): def set_value(self, s: str, color: str | None = None): if color is not None: - self.setText(f"""{self.__descr}: {s} {self.__unit} """) + self.setText( + f"""{self.__descr}: {s} {self.__unit} """ + ) else: self.setText(f"{self.__descr}: {s} {self.__unit}") @@ -19,4 +22,4 @@ class ValueLabel(QLabel): if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit() else: - super().mousePressEvent(event) \ No newline at end of file + super().mousePressEvent(event) diff --git a/src/aare/gui/widgets/video_image.py b/src/aare/gui/widgets/video_image.py index 266641ab..bd03c132 100644 --- a/src/aare/gui/widgets/video_image.py +++ b/src/aare/gui/widgets/video_image.py @@ -35,7 +35,7 @@ class VideoGraphicsView(QGraphicsView): self._busy_overlay_style: BusyOverlayStyle | None = None @Slot(QImage) - def update_frame(self, qt_image : QImage): + def update_frame(self, qt_image: QImage): """Update the video frame""" pixmap = QPixmap.fromImage(qt_image) self.pixmap_item.setPixmap(pixmap) @@ -155,4 +155,4 @@ class VideoGraphicsView(QGraphicsView): text_y = bg_rect.top() + padding_y + fm.ascent() painter.drawText(text_x, text_y, style.text) - painter.restore() \ No newline at end of file + painter.restore() diff --git a/tests/conftest.py b/tests/conftest.py index 638a9cfb..c73d1cbc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,23 +72,14 @@ def mock_backend(server_module): m_cfg.get_gui_session.return_value = None m_cfg.touch_gui_session.return_value = None - yield { - "daq": m_daq, - "bl": m_bl, - "cfg": m_cfg, - } + yield {"daq": m_daq, "bl": m_bl, "cfg": m_cfg} @pytest.fixture def auth_token_data(): from aare.daq.auth import TokenData - return TokenData( - sub="testuser", - staff=True, - pgroups=["p12345"], - session=123, - ) + return TokenData(sub="testuser", staff=True, pgroups=["p12345"], session=123) @pytest.fixture @@ -107,10 +98,7 @@ def client(server_module, mock_backend, auth_token_data): @pytest.fixture def api(client, mock_backend): return SimpleNamespace( - client=client, - daq=mock_backend["daq"], - bl=mock_backend["bl"], - cfg=mock_backend["cfg"], + client=client, daq=mock_backend["daq"], bl=mock_backend["bl"], cfg=mock_backend["cfg"] ) @@ -144,11 +132,7 @@ def daq_status_factory(): pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0, z=0), aerotech_meas=Coordinate(x=0, y=0, z=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), - phi_deg=0.0, - chi_deg=0.0, - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0.0, chi_deg=0.0), omega_deg=0.0, beam_size_mm=Coordinate(x=0.01, y=0.01), ) @@ -183,11 +167,7 @@ def daq_status_factory(): dtz_max=1600.0, ) - session = SessionStatus( - session=session_state, - current_pgroup=current_pgroup, - staff=staff, - ) + session = SessionStatus(session=session_state, current_pgroup=current_pgroup, staff=staff) status = DAQStatusModel( geom=geom, diff --git a/tests/integration/daq/test_daq_server.py b/tests/integration/daq/test_daq_server.py index 07654054..c9f26964 100644 --- a/tests/integration/daq/test_daq_server.py +++ b/tests/integration/daq/test_daq_server.py @@ -7,6 +7,7 @@ os.environ["JWT_AAREDAQ_KEY"] = "test_key_for_integration_testing" from aare.daq.server import app + @pytest.fixture def client(): # We use a TestClient to interact with the FastAPI app @@ -14,6 +15,7 @@ def client(): # For a simple integration test, we can check public endpoints. return TestClient(app) + @pytest.mark.integration def test_read_error_codes(client): response = client.get("/meta/error-codes") @@ -22,10 +24,11 @@ def test_read_error_codes(client): assert "AuthErrorCode" in data assert "AareErrorCode" in data + @pytest.mark.integration def test_login_unauthorized(client): - # Testing login with invalid credentials. - # The current implementation raises KeyError if user not found, + # Testing login with invalid credentials. + # The current implementation raises KeyError if user not found, # which FastAPI might convert to 500 or just propagate if using TestClient in some modes. # However, let's just check for a non-200 status code. response = client.post("/token", data={"username": "non_existent_user_123", "password": "bad"}) diff --git a/tests/integration/daq/test_database_connection.py b/tests/integration/daq/test_database_connection.py index 5920e66f..839b8b4d 100644 --- a/tests/integration/daq/test_database_connection.py +++ b/tests/integration/daq/test_database_connection.py @@ -1,5 +1,6 @@ import pytest + @pytest.mark.integration def test_real_db_connection(): - pass \ No newline at end of file + pass diff --git a/tests/unit/common/test_aare_exception.py b/tests/unit/common/test_aare_exception.py index 82ce9be8..c218a00d 100644 --- a/tests/unit/common/test_aare_exception.py +++ b/tests/unit/common/test_aare_exception.py @@ -77,9 +77,7 @@ def test_phase3_classes_have_critical_default_true(): SmartMagnetFaultException, TransformationInvalidException, ): - assert cls.critical is True, ( - f"{cls.__name__} should default to critical=True in Phase 3" - ) + assert cls.critical is True, f"{cls.__name__} should default to critical=True in Phase 3" def test_other_core_classes_keep_critical_default_false(): @@ -157,9 +155,7 @@ def test_automation_errors_are_automation_error(): AutoRasterSampleSkipped, ): exc = cls() if cls is not AutoRasterSampleSkipped else cls("skipped") - assert isinstance(exc, AutomationError), ( - f"{cls.__name__} should be AutomationError" - ) + assert isinstance(exc, AutomationError), f"{cls.__name__} should be AutomationError" assert isinstance(exc, AareException), f"{cls.__name__} should be AareException" @@ -193,9 +189,7 @@ def test_tell_family_membership(): UnmountingFailed, ): exc = cls() - assert isinstance(exc, TellException), ( - f"{cls.__name__} should be in Tell family" - ) + assert isinstance(exc, TellException), f"{cls.__name__} should be in Tell family" assert isinstance(exc, AutomationError) diff --git a/tests/unit/common/test_coordinate.py b/tests/unit/common/test_coordinate.py index c43f073a..94a8a347 100644 --- a/tests/unit/common/test_coordinate.py +++ b/tests/unit/common/test_coordinate.py @@ -104,9 +104,7 @@ def test_coordinate_rotate_invalid_axis(): def test_smargon_coordinate_equality(): s1 = SmargonCoordinate(sh_mm=Coordinate(x=1, y=1, z=1), phi_deg=10, chi_deg=20) - s2 = SmargonCoordinate( - sh_mm=Coordinate(x=1.05, y=0.95, z=1.01), phi_deg=10.05, chi_deg=19.95 - ) + s2 = SmargonCoordinate(sh_mm=Coordinate(x=1.05, y=0.95, z=1.01), phi_deg=10.05, chi_deg=19.95) assert s1 == s2 s3 = SmargonCoordinate(sh_mm=Coordinate(x=2, y=1, z=1), phi_deg=10, chi_deg=20) @@ -115,9 +113,7 @@ def test_smargon_coordinate_equality(): def test_aerotech_coordinate_equality(): a1 = AerotechCoordinate(at_mm=Coordinate(x=1, y=1, z=1), omega_deg=10) - a2 = AerotechCoordinate( - at_mm=Coordinate(x=1.005, y=0.995, z=1.001), omega_deg=10.005 - ) + a2 = AerotechCoordinate(at_mm=Coordinate(x=1.005, y=0.995, z=1.001), omega_deg=10.005) assert a1 == a2 a3 = AerotechCoordinate(at_mm=Coordinate(x=1.1, y=1, z=1), omega_deg=10) diff --git a/tests/unit/common/test_data_collection_parameters.py b/tests/unit/common/test_data_collection_parameters.py index 15362a4e..fb665903 100644 --- a/tests/unit/common/test_data_collection_parameters.py +++ b/tests/unit/common/test_data_collection_parameters.py @@ -50,9 +50,7 @@ def test_processingpipeline_accepts_unknown_value_after_refactor(): def test_datacollectionparameters_accepts_legacy_aliases(): params = DataCollectionParameters( - totalrange=180, - cellparameters="10 20 30 90 90 120", - userresolution=1.4, + totalrange=180, cellparameters="10 20 30 90 90 120", userresolution=1.4 ) assert params.totalangle == 180 assert params.unitcell == "10 20 30 90 90 120" diff --git a/tests/unit/common/test_diffraction_geometry.py b/tests/unit/common/test_diffraction_geometry.py index 2cf3c378..3b2ad015 100644 --- a/tests/unit/common/test_diffraction_geometry.py +++ b/tests/unit/common/test_diffraction_geometry.py @@ -51,9 +51,7 @@ def test_resolution_angstrom(sample_dg): def test_max_resolution_angstrom(sample_dg): - assert sample_dg.max_resolution_angstrom == sample_dg.resolution_angstrom( - sample_dg.dtz_mm - ) + assert sample_dg.max_resolution_angstrom == sample_dg.resolution_angstrom(sample_dg.dtz_mm) def test_calc_dtz_mm(sample_dg): diff --git a/tests/unit/common/test_error_codes.py b/tests/unit/common/test_error_codes.py index 80e41ca2..1c7213a2 100644 --- a/tests/unit/common/test_error_codes.py +++ b/tests/unit/common/test_error_codes.py @@ -25,23 +25,15 @@ def test_export_error_codes_contains_known_codes(): def test_code_for_exception_class_basic(): - assert ( - code_for_exception_class("TellCommunicationError") == "TELL_COMMUNICATION_ERROR" - ) + assert code_for_exception_class("TellCommunicationError") == "TELL_COMMUNICATION_ERROR" assert code_for_exception_class("MountingFailed") == "MOUNTING_FAILED" assert code_for_exception_class("LoopCenteringFailed") == "LOOP_CENTERING_FAILED" def test_code_for_exception_class_acronyms(): assert code_for_exception_class("AXCFailed") == "AXC_FAILED" - assert ( - code_for_exception_class("AareDBCommunicationError") - == "AARE_DB_COMMUNICATION_ERROR" - ) - assert ( - code_for_exception_class("JFJochCommunicationError") - == "JF_JOCH_COMMUNICATION_ERROR" - ) + assert code_for_exception_class("AareDBCommunicationError") == "AARE_DB_COMMUNICATION_ERROR" + assert code_for_exception_class("JFJochCommunicationError") == "JF_JOCH_COMMUNICATION_ERROR" def test_code_for_each_concrete_exception_is_in_aare_error_code_enum(): diff --git a/tests/unit/common/test_exception_handler.py b/tests/unit/common/test_exception_handler.py index bbbac751..eda074d8 100644 --- a/tests/unit/common/test_exception_handler.py +++ b/tests/unit/common/test_exception_handler.py @@ -38,9 +38,7 @@ def test_data_collection_exception_default_message(): def test_authentication_exception_properties(): - exc = AuthenticationException( - "Failed", status_code=403, code=AuthErrorCode.FORBIDDEN - ) + exc = AuthenticationException("Failed", status_code=403, code=AuthErrorCode.FORBIDDEN) assert exc.status_code == 403 assert exc.code == AuthErrorCode.FORBIDDEN assert "Failed" in str(exc) diff --git a/tests/unit/common/test_find_xtal.py b/tests/unit/common/test_find_xtal.py index fab48d35..02f3a56f 100644 --- a/tests/unit/common/test_find_xtal.py +++ b/tests/unit/common/test_find_xtal.py @@ -41,11 +41,7 @@ def mock_raster_results(): @pytest.fixture def raster_request(): return RasterGridRequest( - exp_time_s=0.1, - n_x=3, - n_y=3, - grid_size_mm=Coordinate(x=0.02, y=0.02), - smargon_top_left=None, + exp_time_s=0.1, n_x=3, n_y=3, grid_size_mm=Coordinate(x=0.02, y=0.02), smargon_top_left=None ) @@ -62,9 +58,7 @@ def test_identify_crystal_raster(mock_raster_results, raster_request): def test_rebuild_array_from_scan_results(mock_raster_results): - arr = rebuild_array_from_scan_results( - mock_raster_results, "spots_low_res", array_shape=(3, 3) - ) + arr = rebuild_array_from_scan_results(mock_raster_results, "spots_low_res", array_shape=(3, 3)) assert arr.shape == (3, 3) assert arr[0, 0] == 0.0 assert arr[2, 2] == 8.0 @@ -73,11 +67,7 @@ def test_rebuild_array_from_scan_results(mock_raster_results): def test_create_quality_filtered_array(mock_raster_results): # Testing create_quality_filtered_array with a more lenient filter arr = create_quality_filtered_array( - mock_raster_results, - "spots", - min_low_res_spots=0.0, - min_background=0.0, - array_shape=(3, 3), + mock_raster_results, "spots", min_low_res_spots=0.0, min_background=0.0, array_shape=(3, 3) ) # i=8 has nx=2, ny=2 -> row=2, col=2 assert arr[2, 2] == 18.0 @@ -163,9 +153,7 @@ def test_compute_crystal_score_array_weights(): r.bkg, r.spots_low_res, r.spots_indexed = 0.0, low, idx return r - score = compute_crystal_score_array( - [_cell(0, 0, 10.0, 0.0), _cell(1, 1, 0.0, 10.0)] - ) + score = compute_crystal_score_array([_cell(0, 0, 10.0, 0.0), _cell(1, 1, 0.0, 10.0)]) assert score[0, 0] > score[1, 0] assert score[0, 0] == pytest.approx(55.0) assert score[1, 0] == pytest.approx(20.0) diff --git a/tests/unit/common/test_models_extra.py b/tests/unit/common/test_models_extra.py index 2d683ac8..e7f863e7 100644 --- a/tests/unit/common/test_models_extra.py +++ b/tests/unit/common/test_models_extra.py @@ -18,9 +18,7 @@ def test_sample_short_info_methods(): sample_name="sample1", run_number=1, aaredb_params=DataCollectionParameters( - totalangle=180, - processingresolution=1.5, - cloud=True, + totalangle=180, processingresolution=1.5, cloud=True ), user="group1", pin=3, @@ -48,11 +46,7 @@ def test_sample_short_info_methods(): "dewar_name": "dew2", "sample_name": "sample2", "run_number": 2, - "aaredb_params": { - "totalrange": 120, - "userresolution": 1.8, - "cloud": "", - }, + "aaredb_params": {"totalrange": 120, "userresolution": 1.8, "cloud": ""}, "user": "group2", "pin": 4, "location": {"segment": "B", "pos": 5}, diff --git a/tests/unit/common/test_raster_grid_common.py b/tests/unit/common/test_raster_grid_common.py index 38d7e5a1..987d691f 100644 --- a/tests/unit/common/test_raster_grid_common.py +++ b/tests/unit/common/test_raster_grid_common.py @@ -43,9 +43,7 @@ def test_round_trip_conversion_for_multiple_grid_shapes(): for grid_y in range(1, 7): for grid_x in range(1, number_of_cols + 1): image_id = grid_to_image_id( - grid_x=grid_x, - grid_y=grid_y, - number_of_cols=number_of_cols, + grid_x=grid_x, grid_y=grid_y, number_of_cols=number_of_cols ) assert image_id_to_grid(image_id, number_of_cols) == (grid_x, grid_y) @@ -83,9 +81,7 @@ def test_grid_to_image_id_rejects_zero_grid_y(): def test_grid_to_image_id_rejects_grid_x_larger_than_number_of_cols(): - with pytest.raises( - ValueError, match="grid_x cannot be greater than number_of_cols" - ): + with pytest.raises(ValueError, match="grid_x cannot be greater than number_of_cols"): grid_to_image_id(grid_x=6, grid_y=1, number_of_cols=5) diff --git a/tests/unit/common/test_recurrence_watcher.py b/tests/unit/common/test_recurrence_watcher.py index 9aa360c5..fbbd9a80 100644 --- a/tests/unit/common/test_recurrence_watcher.py +++ b/tests/unit/common/test_recurrence_watcher.py @@ -38,18 +38,14 @@ def test_load_watcher_threshold_overrides_reads_valid_values_only(): } overrides = load_watcher_threshold_overrides( - beamline="mx", - watcher_names=["tell", "alc", "smargon"], - get_value=values.get, + beamline="mx", watcher_names=["tell", "alc", "smargon"], get_value=values.get ) assert overrides == {"tell": 7, "alc": 3} def test_create_default_watchers_applies_overrides(): - watchers = { - watcher.name: watcher for watcher in create_default_watchers({"alc": 4}) - } + watchers = {watcher.name: watcher for watcher in create_default_watchers({"alc": 4})} assert watchers["alc"].threshold == 4 assert watchers["tell"].threshold > 0 diff --git a/tests/unit/daq/operations/face_detection/test_face_detection_logic.py b/tests/unit/daq/operations/face_detection/test_face_detection_logic.py index a4b8f571..4f430cfb 100644 --- a/tests/unit/daq/operations/face_detection/test_face_detection_logic.py +++ b/tests/unit/daq/operations/face_detection/test_face_detection_logic.py @@ -11,27 +11,31 @@ from aare.daq.operations.face_detection.utils import ( get_samples_out, choose_best_fit, get_flat_face, - chose_best_angle + chose_best_angle, ) + def test_box_height_from_tuple(): assert box_height_from_tuple((0, 0, 10, 20)) == 20 assert box_height_from_tuple((0, 20, 10, 0)) == 20 + def test_box_area_from_tuple(): assert box_area_from_tuple((0, 0, 10, 20)) == 200 assert box_area_from_tuple((0, 20, 10, 0)) == 200 + def test_prepare_samples(): boxes = {0: (0, 0, 10, 10), 90: (0, 0, 10, 20)} samples = prepare_samples(boxes, area=False) assert (0.0, 10.0) in samples assert (90.0, 20.0) in samples - + samples_area = prepare_samples(boxes, area=True) assert (0.0, 100.0) in samples_area assert (90.0, 200.0) in samples_area + def test_cos_model(): # A + B * cos(C * deg2rad(theta) - phi) # theta=0, A=10, B=5, phi=0, C=1 -> 10 + 5 * cos(0) = 15 @@ -39,6 +43,7 @@ def test_cos_model(): # theta=180, A=10, B=5, phi=0, C=1 -> 10 + 5 * cos(pi) = 5 assert pytest.approx(cos_model(180, 10, 5, 0, 1)) == 5.0 + def test_mad_filter(): samples = [(0, 10), (10, 11), (20, 12), (30, 100), (40, 11)] # med = 11, devs = [1, 0, 1, 89, 0], mad = median([0, 0, 1, 1, 89]) = 1 @@ -46,9 +51,10 @@ def test_mad_filter(): filtered = mad_filter(samples) assert (30, 100) not in filtered assert len(filtered) == 4 - + assert mad_filter([]) == [] + def test_fit_metrics(): y_true = np.array([10, 20, 30]) y_pred = np.array([11, 19, 31]) @@ -60,12 +66,14 @@ def test_fit_metrics(): # r2 = 1 - 3/200 = 0.985 assert r2 == pytest.approx(0.985) + def test_fit_cosine_minimal_samples(): samples = [(0, 10), (90, 20)] result = fit_cosine(samples) assert result["A"] == 15.0 assert result["rmse"] is None + def test_fit_cosine_normal(): # Generate perfect cosine data degs = np.linspace(0, 360, 10) @@ -77,6 +85,7 @@ def test_fit_cosine_normal(): assert result["phi_rad"] == pytest.approx(0.5, abs=1e-2) assert result["r2"] > 0.99 + def test_get_samples_out(): boxes = {0: (0, 0, 10, 10), 90: (0, 0, 10, 20)} out = get_samples_out(boxes) @@ -86,30 +95,30 @@ def test_get_samples_out(): assert out[1]["angle_deg"] == 90.0 assert out[1]["height"] == 20.0 + def test_choose_best_fit(): fits = { "Height": {"angle": 45, "params": {"rmse": 0.1, "mae": 0.1, "r2": 0.95}}, - "Area": {"angle": 50, "params": {"rmse": 0.05, "mae": 0.05, "r2": 0.98}} + "Area": {"angle": 50, "params": {"rmse": 0.05, "mae": 0.05, "r2": 0.98}}, } angle, fit, name = choose_best_fit(fits) assert name == "Area" assert angle == 50.0 - + assert choose_best_fit({}) == (None, None, None) + def test_get_flat_face(): boxes = {0: (0, 0, 10, 10), 90: (0, 0, 10, 20), 180: (0, 0, 10, 10), 270: (0, 0, 10, 5)} angle, params = get_flat_face(boxes, 0, 360) assert isinstance(angle, int) assert "A" in params + def test_chose_best_angle(): boxes = {0: (0, 0, 10, 10), 90: (0, 0, 10, 20)} - fit_results = { - "Area": {"angle": 88}, - "Height": {"angle": 92} - } - # Candidates are 88 and 92. Measured are 0 and 90. + fit_results = {"Area": {"angle": 88}, "Height": {"angle": 92}} + # Candidates are 88 and 92. Measured are 0 and 90. # 88 is closer to 90 than 92 is to 90? No, both are 2 deg away. # min(abs(88-0), abs(88-90)) = 2 # min(abs(92-0), abs(92-90)) = 2 diff --git a/tests/unit/daq/operations/face_detection/test_face_detection_service.py b/tests/unit/daq/operations/face_detection/test_face_detection_service.py index d16e30a7..3c64fcc6 100644 --- a/tests/unit/daq/operations/face_detection/test_face_detection_service.py +++ b/tests/unit/daq/operations/face_detection/test_face_detection_service.py @@ -2,18 +2,10 @@ import types import pytest from aarecommon.math.coordinate import Coordinate, SmargonCoordinate -from aarecommon.models.models import ( - BoundingBoxModel, - MLBoxModel, - MLBoxType, - ZoomModeEnum, -) +from aarecommon.models.models import BoundingBoxModel, MLBoxModel, MLBoxType, ZoomModeEnum from aare.daq.operations.common.runtime import DAQRuntimeState -from aare.daq.operations.common.services import ( - FaceDetectionProgressEmitter, - OperationServices, -) +from aare.daq.operations.common.services import FaceDetectionProgressEmitter, OperationServices from aare.daq.operations.face_detection.models import ( FaceDetectionContext, FaceDetectionDependencies, @@ -33,14 +25,7 @@ class DummyGeometry: def _box(cls: MLBoxType, x1: float, y1: float, x2: float, y2: float) -> MLBoxModel: return MLBoxModel( - cls=cls, - box=BoundingBoxModel( - top_x=x1, - top_y=y1, - bottom_x=x2, - bottom_y=y2, - ), - conf=0.95, + cls=cls, box=BoundingBoxModel(top_x=x1, top_y=y1, bottom_x=x2, bottom_y=y2), conf=0.95 ) @@ -81,10 +66,8 @@ def context(): devs=devs, mlbox=types.SimpleNamespace( predict=lambda **kwargs: types.SimpleNamespace( - box=None, - target_point=None, - focus=None, - ), + box=None, target_point=None, focus=None + ) ), ), runtime=runtime_state, @@ -101,11 +84,7 @@ def test_service_returns_empty_payload_when_no_boxes(monkeypatch, context, mock_ monkeypatch.setattr( context.deps.mlbox, "predict", - lambda **kwargs: types.SimpleNamespace( - box=None, - target_point=None, - focus=None, - ), + lambda **kwargs: types.SimpleNamespace(box=None, target_point=None, focus=None), ) result = service.run(steps=2, step_size=30) @@ -121,39 +100,27 @@ def test_service_returns_empty_payload_when_no_boxes(monkeypatch, context, mock_ assert context._progress_events[-1]["running"] is False -def test_service_prefers_face_boxes_when_ratio_is_high( - monkeypatch, context, mock_logger -): +def test_service_prefers_face_boxes_when_ratio_is_high(monkeypatch, context, mock_logger): service = FaceDetectionService(context=context, logger=mock_logger) predictions = iter( [ types.SimpleNamespace( - box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_FACE, 12, 20, 32, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_FACE, 12, 20, 32, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_FACE, 16, 20, 36, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_FACE, 16, 20, 36, 40), target_point=None, focus=None ), ] ) - monkeypatch.setattr( - context.deps.mlbox, "predict", lambda **kwargs: next(predictions) - ) + monkeypatch.setattr(context.deps.mlbox, "predict", lambda **kwargs: next(predictions)) monkeypatch.setattr( "aare.daq.operations.face_detection.service.fd.get_flat_face", @@ -168,9 +135,7 @@ def test_service_prefers_face_boxes_when_ratio_is_high( ) monkeypatch.setattr( "aare.daq.operations.face_detection.service.fd.get_samples_out", - lambda boxes: [ - {"angle": angle, "box": box} for angle, box in sorted(boxes.items()) - ], + lambda boxes: [{"angle": angle, "box": box} for angle, box in sorted(boxes.items())], ) result = service.run(steps=3, step_size=30, face_min_ratio=0.5) @@ -184,44 +149,30 @@ def test_service_prefers_face_boxes_when_ratio_is_high( assert context._progress_events[-1]["running"] is False -def test_service_falls_back_to_loop_all_when_face_ratio_is_low( - monkeypatch, context, mock_logger -): +def test_service_falls_back_to_loop_all_when_face_ratio_is_low(monkeypatch, context, mock_logger): service = FaceDetectionService(context=context, logger=mock_logger) predictions = iter( [ types.SimpleNamespace( - box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_ALL, 11, 20, 31, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_ALL, 11, 20, 31, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_ALL, 12, 20, 32, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_ALL, 12, 20, 32, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_ALL, 13, 20, 33, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_ALL, 13, 20, 33, 40), target_point=None, focus=None ), types.SimpleNamespace( - box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), - target_point=None, - focus=None, + box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None ), ] ) - monkeypatch.setattr( - context.deps.mlbox, "predict", lambda **kwargs: next(predictions) - ) + monkeypatch.setattr(context.deps.mlbox, "predict", lambda **kwargs: next(predictions)) captured = {} @@ -230,8 +181,7 @@ def test_service_falls_back_to_loop_all_when_face_ratio_is_low( return 60, {"A": 1.0, "B": 2.0, "phi_rad": 0.2, "C": 4.0} monkeypatch.setattr( - "aare.daq.operations.face_detection.service.fd.get_flat_face", - fake_get_flat_face, + "aare.daq.operations.face_detection.service.fd.get_flat_face", fake_get_flat_face ) monkeypatch.setattr( "aare.daq.operations.face_detection.service.fd.choose_best_fit", @@ -265,9 +215,7 @@ def test_service_applies_centre_correction_when_target_is_far_from_beam( assert context.deps.devs.smargon_pos.sh_mm.y == pytest.approx(1.8) -def test_service_returns_failed_result_when_prediction_raises( - monkeypatch, context, mock_logger -): +def test_service_returns_failed_result_when_prediction_raises(monkeypatch, context, mock_logger): service = FaceDetectionService(context=context, logger=mock_logger) monkeypatch.setattr( diff --git a/tests/unit/daq/operations/loop_centering/test_loop_centering_analyzer.py b/tests/unit/daq/operations/loop_centering/test_loop_centering_analyzer.py index 49cd3aee..715472c3 100644 --- a/tests/unit/daq/operations/loop_centering/test_loop_centering_analyzer.py +++ b/tests/unit/daq/operations/loop_centering/test_loop_centering_analyzer.py @@ -6,11 +6,7 @@ from aarecommon.models.models import MLBoxType, MLOutputModel from aare.daq.mlbox import MLBoxPredictionsResult from aare.daq.operations.common.runtime import DAQRuntimeState -from aare.daq.operations.common.services import ( - OperationServices, - PredictionProvider, - TraceWriter, -) +from aare.daq.operations.common.services import OperationServices, PredictionProvider, TraceWriter from aare.daq.operations.loop_centering.analyzer import LoopCenteringAnalyzer from aare.daq.operations.loop_centering.models import ( LoopCenteringContext, @@ -50,25 +46,18 @@ def analyzer(mock_logger): ctx = LoopCenteringContext( deps=LoopCenteringDependencies( - cfg=types.SimpleNamespace(), - devs=types.SimpleNamespace(), - mlbox=DummyMlBox(), + cfg=types.SimpleNamespace(), devs=types.SimpleNamespace(), mlbox=DummyMlBox() ), runtime=runtime_state, services=OperationServices( screenshots=types.SimpleNamespace(save_to_db=lambda *args, **kwargs: None), traces=TraceWriter( - appender=types.SimpleNamespace( - append_smargon_trace=lambda *args, **kwargs: None - ) + appender=types.SimpleNamespace(append_smargon_trace=lambda *args, **kwargs: None) ), predictions=PredictionProvider( getter=types.SimpleNamespace( get_predictions=lambda: MLBoxPredictionsResult( - predictions=None, - image=None, - target_point=None, - focus=None, + predictions=None, image=None, target_point=None, focus=None ) ) ), @@ -80,39 +69,20 @@ def analyzer(mock_logger): def test_is_ignore_only_classes_true(analyzer): assert analyzer.is_ignore_only_classes( - [ - MLBoxType.PIN.value, - MLBoxType.ICE.value, - MLBoxType.NEEDLE.value, - ] + [MLBoxType.PIN.value, MLBoxType.ICE.value, MLBoxType.NEEDLE.value] ) def test_is_ignore_only_classes_false_when_loop_present(analyzer): - assert not analyzer.is_ignore_only_classes( - [ - MLBoxType.PIN.value, - MLBoxType.LOOP_FACE.value, - ] - ) + assert not analyzer.is_ignore_only_classes([MLBoxType.PIN.value, MLBoxType.LOOP_FACE.value]) def test_has_valid_target_classes_true(analyzer): - assert analyzer.has_valid_target_classes( - [ - MLBoxType.PIN.value, - MLBoxType.CRYSTAL.value, - ] - ) + assert analyzer.has_valid_target_classes([MLBoxType.PIN.value, MLBoxType.CRYSTAL.value]) def test_has_valid_target_classes_false(analyzer): - assert not analyzer.has_valid_target_classes( - [ - MLBoxType.PIN.value, - MLBoxType.ICE.value, - ] - ) + assert not analyzer.has_valid_target_classes([MLBoxType.PIN.value, MLBoxType.ICE.value]) def test_select_smargon_target_prefers_prediction_when_within_tolerance(analyzer): @@ -120,9 +90,7 @@ def test_select_smargon_target_prefers_prediction_when_within_tolerance(analyzer predicted = _coord_target(1.0001, 2.0001, 3.0001) result = analyzer.select_smargon_target( - calculated_target=calculated, - predicted_target=predicted, - tolerance_um=500.0, + calculated_target=calculated, predicted_target=predicted, tolerance_um=500.0 ) assert result is predicted @@ -133,9 +101,7 @@ def test_select_smargon_target_keeps_calculated_when_outside_tolerance(analyzer) predicted = _coord_target(2.0, 3.0, 4.0) result = analyzer.select_smargon_target( - calculated_target=calculated, - predicted_target=predicted, - tolerance_um=500.0, + calculated_target=calculated, predicted_target=predicted, tolerance_um=500.0 ) assert result is calculated @@ -158,13 +124,8 @@ def test_analyze_angle_with_crystal_prediction_returns_valid_target(analyzer): boxes = MLOutputModel() boxes.add_box(MLBoxType.CRYSTAL, (10, 20, 30, 40), conf=0.9) - analyzer.ctx.services.predictions.getter.get_predictions = lambda: ( - MLBoxPredictionsResult( - predictions=boxes, - image=None, - target_point=(25.0, 30.0), - focus=1.0, - ) + analyzer.ctx.services.predictions.getter.get_predictions = lambda: MLBoxPredictionsResult( + predictions=boxes, image=None, target_point=(25.0, 30.0), focus=1.0 ) analysis = analyzer.analyze_angle(angle_deg=90, zoom_value=200.0, sample_id=1) diff --git a/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py b/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py index 61b61385..2dc7e809 100644 --- a/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py +++ b/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py @@ -4,11 +4,7 @@ import pytest from aarecommon.models.models import LoopCenteringResult from aare.daq.operations.common.runtime import DAQRuntimeState -from aare.daq.operations.common.services import ( - OperationServices, - PredictionProvider, - TraceWriter, -) +from aare.daq.operations.common.services import OperationServices, PredictionProvider, TraceWriter from aare.daq.operations.loop_centering.models import ( AngleAnalysis, LoopCenteringContext, @@ -32,17 +28,13 @@ def context(): runtime_state = DAQRuntimeState( sample_provider=types.SimpleNamespace(sample=None), - sample_geometry_provider=types.SimpleNamespace( - sample_geometry=types.SimpleNamespace() - ), + sample_geometry_provider=types.SimpleNamespace(sample_geometry=types.SimpleNamespace()), status_provider=types.SimpleNamespace(status=None), ) ctx = LoopCenteringContext( deps=LoopCenteringDependencies( - cfg=types.SimpleNamespace(), - devs=devs, - mlbox=types.SimpleNamespace(), + cfg=types.SimpleNamespace(), devs=devs, mlbox=types.SimpleNamespace() ), runtime=runtime_state, services=OperationServices( @@ -67,19 +59,14 @@ def context(): return ctx -def test_service_fails_when_attempt_one_has_no_valid_targets( - monkeypatch, context, mock_logger -): +def test_service_fails_when_attempt_one_has_no_valid_targets(monkeypatch, context, mock_logger): service = LoopCenteringService(context=context, logger=mock_logger) monkeypatch.setattr( service, "_run_angle_pass", lambda **kwargs: AngleAnalysis( - angle_deg=kwargs["angle"], - classes=[], - has_valid_target=False, - ignore_only=False, + angle_deg=kwargs["angle"], classes=[], has_valid_target=False, ignore_only=False ), ) @@ -90,9 +77,7 @@ def test_service_fails_when_attempt_one_has_no_valid_targets( assert "attempt 1" in result.comment.lower() -def test_service_succeeds_when_correction_pass_has_valid_target( - monkeypatch, context, mock_logger -): +def test_service_succeeds_when_correction_pass_has_valid_target(monkeypatch, context, mock_logger): service = LoopCenteringService(context=context, logger=mock_logger) call_count = {"count": 0} @@ -134,10 +119,7 @@ def test_service_uses_settings_values(monkeypatch, context, mock_logger): service, "_run_angle_pass", lambda **kwargs: AngleAnalysis( - angle_deg=kwargs["angle"], - classes=[], - has_valid_target=False, - ignore_only=False, + angle_deg=kwargs["angle"], classes=[], has_valid_target=False, ignore_only=False ), ) diff --git a/tests/unit/daq/operations/mounting/test_mounting_service.py b/tests/unit/daq/operations/mounting/test_mounting_service.py index 0d8fdb9f..bf5a47ea 100644 --- a/tests/unit/daq/operations/mounting/test_mounting_service.py +++ b/tests/unit/daq/operations/mounting/test_mounting_service.py @@ -60,10 +60,7 @@ def _make_context(previous_sample=None, *, prohibited=True, alarm=False): aerotech_pos=None, tell=tell, magnet_position_sensor=types.SimpleNamespace(value=0), - pss=types.SimpleNamespace( - is_prohibited=lambda: prohibited, - alarm_active=lambda: alarm, - ), + pss=types.SimpleNamespace(is_prohibited=lambda: prohibited, alarm_active=lambda: alarm), ) streak = {"count": 0} @@ -82,14 +79,9 @@ def _make_context(previous_sample=None, *, prohibited=True, alarm=False): ) return MountingContext( - deps=MountingDependencies( - cfg=cfg, - devs=devs, - ), + deps=MountingDependencies(cfg=cfg, devs=devs), settings=MountingSettings( - mount_position=AerotechCoordinate( - at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0 - ), + mount_position=AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0) ), ) @@ -229,9 +221,7 @@ def test_execute_mount_success_resets_failure_streak(mock_logger): assert ctx.deps.cfg.get_mount_failure_streak() == 0 -def test_execute_mount_critical_tell_error_does_not_increment_failure_streak( - mock_logger, -): +def test_execute_mount_critical_tell_error_does_not_increment_failure_streak(mock_logger): target_sample = _make_sample(2, "new") ctx = _make_context() diff --git a/tests/unit/daq/operations/screenshot/test_screenshot_service.py b/tests/unit/daq/operations/screenshot/test_screenshot_service.py index 4d9f2018..6936fa55 100644 --- a/tests/unit/daq/operations/screenshot/test_screenshot_service.py +++ b/tests/unit/daq/operations/screenshot/test_screenshot_service.py @@ -27,8 +27,8 @@ def test_save_to_db_uploads_image_via_shared_aare(mock_logger): mlbox = types.SimpleNamespace(get_latest_image=lambda: image) aare = types.SimpleNamespace( - upload_image=lambda sample_id, filename, bgr_image, **kwargs: ( - upload_calls.append((sample_id, filename, bgr_image, kwargs)) + upload_image=lambda sample_id, filename, bgr_image, **kwargs: upload_calls.append( + (sample_id, filename, bgr_image, kwargs) ) ) @@ -55,16 +55,14 @@ def test_save_to_db_uploads_image_via_shared_aare(mock_logger): assert noncritical_calls == [("screenshot upload 'mounted'", sample)] -def test_send_to_db_writes_photo_and_uploads_with_default_message( - mock_logger, tmp_path -): +def test_send_to_db_writes_photo_and_uploads_with_default_message(mock_logger, tmp_path): image = np.zeros((10, 10, 3), dtype=np.uint8) upload_calls = [] mlbox = types.SimpleNamespace(get_latest_image=lambda: image) aare = types.SimpleNamespace( - upload_image=lambda sample_id, filename, bgr_image, **kwargs: ( - upload_calls.append((sample_id, filename, kwargs)) + upload_image=lambda sample_id, filename, bgr_image, **kwargs: upload_calls.append( + (sample_id, filename, kwargs) ) ) sample = _make_sample(15) @@ -80,9 +78,7 @@ def test_send_to_db_writes_photo_and_uploads_with_default_message( ) service.send_to_db( - filename="test image", - message=None, - default_message="default screenshot message", + filename="test image", message=None, default_message="default screenshot message" ) expected_photo = tmp_path / "p12345" / "raw" / "photos" / "15" / "test_image.jpeg" @@ -95,9 +91,7 @@ def test_send_to_db_writes_photo_and_uploads_with_default_message( def test_send_to_db_requires_mounted_sample(mock_logger): service = ScreenshotService( - mlbox=types.SimpleNamespace( - get_latest_image=lambda: np.zeros((4, 4, 3), dtype=np.uint8) - ), + mlbox=types.SimpleNamespace(get_latest_image=lambda: np.zeros((4, 4, 3), dtype=np.uint8)), aare=types.SimpleNamespace(upload_image=lambda *args, **kwargs: None), logger=mock_logger, run_noncritical=lambda action, **kwargs: action(), diff --git a/tests/unit/daq/operations/test_ml_raster_plan.py b/tests/unit/daq/operations/test_ml_raster_plan.py index 7e5737e8..c985aaa0 100644 --- a/tests/unit/daq/operations/test_ml_raster_plan.py +++ b/tests/unit/daq/operations/test_ml_raster_plan.py @@ -25,9 +25,7 @@ def _geom() -> SampleGeometryModel: pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0), aerotech_meas=Coordinate(x=0, y=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0 - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0), omega_deg=0, beam_size_mm=Coordinate(x=0.01, y=0.01), ) @@ -71,9 +69,7 @@ def test_box_helpers(): def test_plan_returns_loop_boxes_and_prefers_loop_face(): - plan = _plan( - _fake_mlbox(loop_all=(100, 100, 400, 400), loop_face=(150, 150, 300, 300)) - ) + plan = _plan(_fake_mlbox(loop_all=(100, 100, 400, 400), loop_face=(150, 150, 300, 300))) assert plan is not None assert plan.loop_all_box == (100, 100, 400, 400) assert plan.loop_face_box == (150, 150, 300, 300) @@ -156,15 +152,11 @@ def test_grid_padding_fraction_scales(monkeypatch): def test_crystal_union_extends_grid_only_when_enabled(monkeypatch): # crystal extends well beyond the loop_face box on +x - mlbox = lambda: _fake_mlbox( - loop_face=(150, 150, 300, 300), crystals=[(350, 150, 520, 300)] - ) + mlbox = lambda: _fake_mlbox(loop_face=(150, 150, 300, 300), crystals=[(350, 150, 520, 300)]) def cfg(enabled): return lambda k, d=None: ( - enabled - if "include_crystal" in k - else (0.0 if "grid_padding_fraction" in k else d) + enabled if "include_crystal" in k else (0.0 if "grid_padding_fraction" in k else d) ) monkeypatch.setattr(mlb, "cfg_get", cfg(False)) diff --git a/tests/unit/daq/test_aare_daq_loop_centering.py b/tests/unit/daq/test_aare_daq_loop_centering.py index b47564f3..39b1a209 100644 --- a/tests/unit/daq/test_aare_daq_loop_centering.py +++ b/tests/unit/daq/test_aare_daq_loop_centering.py @@ -48,9 +48,7 @@ def test_execute_loop_centering_handles_service_failure(): service = MagicMock() service.run.return_value = types.SimpleNamespace( - success=False, - comment="failed", - error=LoopCenteringFailed("failed"), + success=False, comment="failed", error=LoopCenteringFailed("failed") ) daq._create_loop_centering_service = MagicMock(return_value=service) @@ -80,11 +78,7 @@ def test_execute_loop_centering_handles_service_success(): ) service = MagicMock() - service.run.return_value = types.SimpleNamespace( - success=True, - comment=None, - error=None, - ) + service.run.return_value = types.SimpleNamespace(success=True, comment=None, error=None) daq._create_loop_centering_service = MagicMock(return_value=service) result = daq._execute_loop_centering(sample) @@ -102,9 +96,7 @@ def test_record_best_effort_step_failure_marks_progress_and_logs_warning(mock_lo current_step="Center", steps=[ StepState( - step=WorkflowStateKind.LOOP_CENTRE, - status=StepStatus.RUNNING, - message="Centering", + step=WorkflowStateKind.LOOP_CENTRE, status=StepStatus.RUNNING, message="Centering" ) ], finished=False, diff --git a/tests/unit/daq/test_aaredb.py b/tests/unit/daq/test_aaredb.py index 8c85782e..851b8213 100644 --- a/tests/unit/daq/test_aaredb.py +++ b/tests/unit/daq/test_aaredb.py @@ -51,9 +51,7 @@ def daq_status(mock_bl): status.bl.flux_ph_s = 1e12 status.bl.cryojet_K = 100.0 status.geom.beam_size_mm = Coordinate(x=0.01, y=0.01) - status.geom.smargon = SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0 - ) + status.geom.smargon = SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0) return status @@ -64,9 +62,7 @@ def geom_model(): pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0), aerotech_meas=Coordinate(x=0, y=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0 - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0), omega_deg=0, beam_size_mm=Coordinate(x=0.01, y=0.01), ) @@ -77,9 +73,7 @@ def geom_model(): @patch("aareDB.SamplesRunnerApi") @patch("aareDB.ProcessingsRunnerApi") @patch("aareDB.GridscanRunnerApi") -def test_aare_wrapper_init( - mock_grid, mock_proc, mock_sample, mock_tell, mock_api, mock_bl -): +def test_aare_wrapper_init(mock_grid, mock_proc, mock_sample, mock_tell, mock_api, mock_bl): wrapper = AareWrapper(bl=mock_bl) mock_api.assert_called_once() assert wrapper._AareWrapper__bl == mock_bl @@ -90,9 +84,7 @@ def test_aare_wrapper_init( @patch("aareDB.SamplesRunnerApi") @patch("aareDB.ProcessingsRunnerApi") @patch("aareDB.GridscanRunnerApi") -def test_set_pucks_beamline( - mock_grid, mock_proc, mock_sample, mock_tell, mock_api, mock_bl -): +def test_set_pucks_beamline(mock_grid, mock_proc, mock_sample, mock_tell, mock_api, mock_bl): wrapper = AareWrapper(bl=mock_bl) pucks = [PuckLoadedInfo(puck_name="P1", location=DewarAddress(segment="A", pos=1))] wrapper.set_pucks_beamline(pucks) @@ -115,9 +107,7 @@ def test_send_sample_event(mock_sample, mock_api, mock_bl, sample_info): from aareDB import SampleEventType wrapper = AareWrapper(bl=mock_bl) - wrapper.send_sample_event( - sample_info.db_id, SampleEventType.MOUNTED, "Test comment" - ) + wrapper.send_sample_event(sample_info.db_id, SampleEventType.MOUNTED, "Test comment") mock_sample.return_value.create_sample_event.assert_called_once() # Test None sample @@ -133,9 +123,7 @@ def test_send_sample_event(mock_sample, mock_api, mock_bl, sample_info): @patch("aareDB.ApiClient") @patch("aareDB.SamplesRunnerApi") -def test_create_manual_sample_error( - mock_sample, mock_api, mock_bl, sample_info, caplog -): +def test_create_manual_sample_error(mock_sample, mock_api, mock_bl, sample_info, caplog): import logging caplog.set_level(logging.ERROR) @@ -187,11 +175,7 @@ def test_create_rotation_run(mock_sample, mock_api, mock_bl, sample_info, daq_st wrapper = AareWrapper(bl=mock_bl) # Standard rotation req = RotationScanRequest( - exp_time_s=0.1, - incr_omega_deg=0.1, - steps=100, - file_prefix="test_prefix", - dtz=200.0, + exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", dtz=200.0 ) wrapper.create_rotation_run(sample_info, req, daq_status) mock_sample.return_value.create_experiment_parameters_for_sample.assert_called_once() @@ -204,9 +188,7 @@ def test_create_rotation_run(mock_sample, mock_api, mock_bl, sample_info, daq_st @patch("aareDB.ApiClient") @patch("aareDB.SamplesRunnerApi") -def test_create_rotation_run_screening( - mock_sample, mock_api, mock_bl, sample_info, daq_status -): +def test_create_rotation_run_screening(mock_sample, mock_api, mock_bl, sample_info, daq_status): wrapper = AareWrapper(bl=mock_bl) req = RotationScanRequest( exp_time_s=0.1, @@ -223,19 +205,13 @@ def test_create_rotation_run_screening( @patch("aareDB.ApiClient") @patch("aareDB.SamplesRunnerApi") -def test_create_rotation_run_error( - mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog -): +def test_create_rotation_run_error(mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog): wrapper = AareWrapper(bl=mock_bl) req = RotationScanRequest( - exp_time_s=0.1, - incr_omega_deg=0.1, - steps=100, - file_prefix="test_prefix", - dtz=200.0, + exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", dtz=200.0 ) - mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = ( - Exception("Rotation error") + mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = Exception( + "Rotation error" ) wrapper.create_rotation_run(sample_info, req, daq_status) assert "Rotation error" in caplog.text @@ -265,9 +241,7 @@ def test_create_gridscan_run(mock_sample, mock_api, mock_bl, sample_info, daq_st @patch("aareDB.ApiClient") @patch("aareDB.SamplesRunnerApi") -def test_create_gridscan_run_error( - mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog -): +def test_create_gridscan_run_error(mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog): wrapper = AareWrapper(bl=mock_bl) req = RasterGridRequest( exp_time_s=0.1, @@ -278,8 +252,8 @@ def test_create_gridscan_run_error( file_prefix="test_grid_prefix", dtz=200.0, ) - mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = ( - Exception("Grid error") + mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = Exception( + "Grid error" ) wrapper.create_gridscan_run(sample_info, req, daq_status) assert "Grid error" in caplog.text @@ -307,9 +281,7 @@ def test_ingest_gridscan(mock_post, mock_api, mock_bl, sample_info, geom_model): # None sample mock_post.reset_mock() - wrapper.ingest_gridscan( - None, raster_result, raster_request, geom_model, com, (500.0, 500.0) - ) + wrapper.ingest_gridscan(None, raster_result, raster_request, geom_model, com, (500.0, 500.0)) mock_post.assert_not_called() @@ -349,14 +321,10 @@ def test_format_gridscan_payload_no_com(mock_api, mock_bl, sample_info, geom_mod @patch("aareDB.ApiClient") -def test_format_gridscan_payload_with_top_left( - mock_api, mock_bl, sample_info, geom_model -): +def test_format_gridscan_payload_with_top_left(mock_api, mock_bl, sample_info, geom_model): wrapper = AareWrapper(bl=mock_bl) raster_result = MagicMock(spec=ScanResult) - top_left = SmargonCoordinate( - sh_mm=Coordinate(x=0.1, y=0.1, z=0.1), phi_deg=0, chi_deg=0 - ) + top_left = SmargonCoordinate(sh_mm=Coordinate(x=0.1, y=0.1, z=0.1), phi_deg=0, chi_deg=0) raster_request = RasterGridRequest( exp_time_s=0.1, n_x=10, diff --git a/tests/unit/daq/test_auth.py b/tests/unit/daq/test_auth.py index ef28cfc4..c673f479 100644 --- a/tests/unit/daq/test_auth.py +++ b/tests/unit/daq/test_auth.py @@ -26,10 +26,7 @@ with patch.dict("os.environ", {"JWT_AAREDAQ_KEY": "test_secret"}): resolve_baton_timeout_if_needed, respond_to_baton_request, ) -from aarecommon.errors.exception_handler import ( - AuthenticationException, - UserRightsException, -) +from aarecommon.errors.exception_handler import AuthenticationException, UserRightsException from aarecommon.models.auth import ( BatonHolderInfo, BatonRequest, @@ -53,9 +50,7 @@ def mock_cfg(): @pytest.fixture def token_data(): - return TokenData( - sub="testuser", pgroups=["p12345", "p67890"], session=100, staff=False - ) + return TokenData(sub="testuser", pgroups=["p12345", "p67890"], session=100, staff=False) @pytest.fixture @@ -131,15 +126,11 @@ def test_check_jwt_ro(mock_cfg, token_data): def test_check_jwt_rw(mock_cfg, token_data): - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) check_jwt_rw(mock_cfg, token_data) # Not holder - mock_cfg.baton_holder = BatonHolderInfo( - session=200, username="other", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=200, username="other", is_staff=False) with pytest.raises(UserRightsException): check_jwt_rw(mock_cfg, token_data) @@ -180,9 +171,7 @@ def test_request_baton_owned_by_you(mock_cfg, token_data): def test_request_baton_pending(mock_cfg, token_data): mock_cfg.session_state.return_value = SessionsStateEnum.OwnedByElse - mock_cfg.baton_holder = BatonHolderInfo( - session=200, username="other", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=200, username="other", is_staff=False) mock_cfg.pending_baton_request = None mock_cfg.can_transfer_baton_now.return_value = True mock_cfg.allow_non_staff_request_from_staff = True @@ -193,9 +182,7 @@ def test_request_baton_pending(mock_cfg, token_data): def test_respond_to_baton_request_accept(mock_cfg, token_data): - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) mock_cfg.pending_baton_request = BatonRequest( request_id="1", requester_username="other", @@ -215,9 +202,7 @@ def test_respond_to_baton_request_accept(mock_cfg, token_data): def test_release_baton(mock_cfg, token_data): - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) res = release_baton(mock_cfg, token_data) assert res["released"] is True mock_cfg.end_active_session.assert_called_with(100) @@ -225,9 +210,7 @@ def test_release_baton(mock_cfg, token_data): def test_request_baton_staff_override(mock_cfg, staff_token_data): mock_cfg.session_state.return_value = SessionsStateEnum.OwnedByElse - mock_cfg.baton_holder = BatonHolderInfo( - session=200, username="other", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=200, username="other", is_staff=False) mock_cfg.can_transfer_baton_now.return_value = True res = request_baton(mock_cfg, staff_token_data) @@ -237,9 +220,7 @@ def test_request_baton_staff_override(mock_cfg, staff_token_data): def test_request_baton_staff_override_busy(mock_cfg, staff_token_data): mock_cfg.session_state.return_value = SessionsStateEnum.OwnedByElse - mock_cfg.baton_holder = BatonHolderInfo( - session=200, username="other", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=200, username="other", is_staff=False) mock_cfg.can_transfer_baton_now.return_value = False res = request_baton(mock_cfg, staff_token_data) @@ -268,9 +249,7 @@ def test_resolve_baton_timeout_if_needed(mock_cfg): # Case: Request expired, beamline not busy mock_cfg.pending_baton_request.created_at = time.time() - 40 mock_cfg.can_transfer_baton_now.return_value = True - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) resolve_baton_timeout_if_needed(mock_cfg) mock_cfg.execute_baton_transfer.assert_called_once() @@ -278,9 +257,7 @@ def test_resolve_baton_timeout_if_needed(mock_cfg): def test_respond_to_baton_request_refuse(mock_cfg, token_data): - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) mock_cfg.pending_baton_request = BatonRequest( request_id="1", requester_username="other", @@ -298,9 +275,7 @@ def test_respond_to_baton_request_refuse(mock_cfg, token_data): def test_respond_to_baton_request_accept_busy(mock_cfg, token_data): - mock_cfg.baton_holder = BatonHolderInfo( - session=100, username="testuser", is_staff=False - ) + mock_cfg.baton_holder = BatonHolderInfo(session=100, username="testuser", is_staff=False) mock_cfg.pending_baton_request = BatonRequest( request_id="1", requester_username="other", diff --git a/tests/unit/daq/test_autofocus.py b/tests/unit/daq/test_autofocus.py index 23f93a34..77e398c0 100644 --- a/tests/unit/daq/test_autofocus.py +++ b/tests/unit/daq/test_autofocus.py @@ -2,32 +2,35 @@ import numpy as np import cv2 from aare.daq.autofocus import calculate_focus_measure + def test_calculate_focus_measure_grayscale(): # Create a focused spot (a sharp square) image = np.zeros((100, 100), dtype=np.uint8) image[40:60, 40:60] = 255 - + # Calculate focus measure at the center fm = calculate_focus_measure(image, 50, 50, 20) assert fm > 0 - + # Create a blurred spot blurred = cv2.GaussianBlur(image, (21, 21), 0) fm_blurred = calculate_focus_measure(blurred, 50, 50, 20) - + # Focused image should have higher variance of Laplacian assert fm > fm_blurred + def test_calculate_focus_measure_color(): # Create a color image image = np.zeros((100, 100, 3), dtype=np.uint8) - image[40:60, 40:60, 0] = 255 # Blue square - + image[40:60, 40:60, 0] = 255 # Blue square + fm = calculate_focus_measure(image, 50, 50, 20) assert fm > 0 + def test_calculate_focus_measure_out_of_bounds(): image = np.zeros((100, 100), dtype=np.uint8) # Even if radius goes out of bounds, np.ogrid and mask should handle it fm = calculate_focus_measure(image, 0, 0, 200) - assert fm == 0 # All zeros, so variance is 0 + assert fm == 0 # All zeros, so variance is 0 diff --git a/tests/unit/daq/test_automation_progress_state_manager.py b/tests/unit/daq/test_automation_progress_state_manager.py index 31403e52..e936ea75 100644 --- a/tests/unit/daq/test_automation_progress_state_manager.py +++ b/tests/unit/daq/test_automation_progress_state_manager.py @@ -43,30 +43,18 @@ def test_automation_progress_state_round_trip_dataclass(): current_step="Center", steps=[ StepState( - step=WorkflowStateKind.MOUNT, - status=StepStatus.SUCCESS, - message="Mount complete", + step=WorkflowStateKind.MOUNT, status=StepStatus.SUCCESS, message="Mount complete" ), StepState( step=WorkflowStateKind.LOOP_CENTRE, status=StepStatus.RUNNING, message="Centering sample", ), + StepState(step=WorkflowStateKind.RASTER, status=StepStatus.PENDING, message=""), StepState( - step=WorkflowStateKind.RASTER, - status=StepStatus.PENDING, - message="", - ), - StepState( - step=WorkflowStateKind.DATA_COLLECTION, - status=StepStatus.PENDING, - message="", - ), - StepState( - step=WorkflowStateKind.FINAL, - status=StepStatus.PENDING, - message="", + step=WorkflowStateKind.DATA_COLLECTION, status=StepStatus.PENDING, message="" ), + StepState(step=WorkflowStateKind.FINAL, status=StepStatus.PENDING, message=""), ], finished=False, success=None, @@ -90,9 +78,7 @@ def test_automation_progress_state_seq_increments(): current_step="Mount", steps=[ StepState( - step=WorkflowStateKind.MOUNT, - status=StepStatus.RUNNING, - message="Mounting sample", + step=WorkflowStateKind.MOUNT, status=StepStatus.RUNNING, message="Mounting sample" ) ], finished=False, diff --git a/tests/unit/daq/test_beamcenterfit.py b/tests/unit/daq/test_beamcenterfit.py index 769a09e8..5fdb0277 100644 --- a/tests/unit/daq/test_beamcenterfit.py +++ b/tests/unit/daq/test_beamcenterfit.py @@ -3,30 +3,34 @@ import numpy as np import cv2 from aare.daq.beamcenterfit import beamcenter_fit, Gaussian2Dfit -def create_synthetic_beam_image(shape=(200, 200), center=(100, 100), sigma=(10, 10), theta=0, A=200, offset=20): + +def create_synthetic_beam_image( + shape=(200, 200), center=(100, 100), sigma=(10, 10), theta=0, A=200, offset=20 +): x = np.arange(shape[1]) y = np.arange(shape[0]) X, Y = np.meshgrid(x, y) - + x0, y0 = center sig_x, sig_y = sigma - + x_rot = (X - x0) * np.cos(theta) + (Y - y0) * np.sin(theta) y_rot = -(X - x0) * np.sin(theta) + (Y - y0) * np.cos(theta) - + gaussian = A * np.exp(-(x_rot**2 / (2 * sig_x**2) + y_rot**2 / (2 * sig_y**2))) + offset # Add some noise noise = np.random.normal(0, 2, shape) image = (gaussian + noise).astype(np.uint8) return image + def test_beamcenter_fit_success(): # Create a synthetic image with a known beam center true_center = (120, 80) image = create_synthetic_beam_image(center=true_center, sigma=(8, 12), theta=np.radians(30)) - + result = beamcenter_fit(image) - + assert isinstance(result, Gaussian2Dfit) # Check if the fitted center is close to the true center assert pytest.approx(result.center_x, abs=2) == true_center[0] @@ -34,31 +38,36 @@ def test_beamcenter_fit_success(): assert result.peak_intensity > 150 assert 0 <= result.rotation_angle < 360 + def test_beamcenter_fit_no_converge(): # Create an image that is just noise, should probably fail or at least not find a good fit image = np.random.randint(0, 50, (200, 200), dtype=np.uint8) - - # beamcenter_fit might still find some contour if there's enough noise, + + # beamcenter_fit might still find some contour if there's enough noise, # but curve_fit might fail to converge # If it doesn't converge, it returns None now. result = beamcenter_fit(image) - # It might actually return a result if it finds a random blob, + # It might actually return a result if it finds a random blob, # but we want to test the failure path. # To truly force non-convergence we might need a more extreme case, # but return None is better than exit() anyway. pass + def test_beamcenter_fit_no_contours(): # Completely black image, max(contours) will fail image = np.zeros((100, 100), dtype=np.uint8) - with pytest.raises(ValueError, match=r"max\(\) (arg is an empty sequence|iterable argument is empty)"): + with pytest.raises( + ValueError, match=r"max\(\) (arg is an empty sequence|iterable argument is empty)" + ): beamcenter_fit(image) + def test_beamcenter_fit_small_blob(): # Test with a very small blob image = np.zeros((100, 100), dtype=np.uint8) image[45:55, 45:55] = 255 - + result = beamcenter_fit(image) assert isinstance(result, Gaussian2Dfit) assert pytest.approx(result.center_x, abs=2) == 50 diff --git a/tests/unit/daq/test_face_detection.py b/tests/unit/daq/test_face_detection.py index 529a2cea..5518d4a0 100644 --- a/tests/unit/daq/test_face_detection.py +++ b/tests/unit/daq/test_face_detection.py @@ -12,32 +12,21 @@ def test_execute_face_detection_reports_failure(monkeypatch): calls = {"set_state": [], "handle_error": []} - monkeypatch.setattr( - daq, - "_AareDAQ__set_state", - lambda state: calls["set_state"].append(state), - ) + monkeypatch.setattr(daq, "_AareDAQ__set_state", lambda state: calls["set_state"].append(state)) monkeypatch.setattr( daq, "_create_face_detection_service", lambda: types.SimpleNamespace( run=lambda **kwargs: FaceDetectionResult( success=False, - payload={ - "running": False, - "samples": [], - "height_fit": {}, - "area_fit": {}, - }, + payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}}, error=RuntimeError("fd failed"), comment="face detection sequence failed", ) ), ) monkeypatch.setattr( - daq, - "_handle_operation_error", - lambda **kwargs: calls["handle_error"].append(kwargs), + daq, "_handle_operation_error", lambda **kwargs: calls["handle_error"].append(kwargs) ) monkeypatch.setattr( type(daq), @@ -67,21 +56,14 @@ def test_execute_face_detection_can_skip_error_reporting(monkeypatch): lambda: types.SimpleNamespace( run=lambda **kwargs: FaceDetectionResult( success=False, - payload={ - "running": False, - "samples": [], - "height_fit": {}, - "area_fit": {}, - }, + payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}}, error=RuntimeError("fd failed"), comment="face detection sequence failed", ) ), ) monkeypatch.setattr( - daq, - "_handle_operation_error", - lambda **kwargs: handle_error_calls.append(kwargs), + daq, "_handle_operation_error", lambda **kwargs: handle_error_calls.append(kwargs) ) result = daq._execute_face_detection(report_error=False) @@ -94,10 +76,7 @@ def test_public_face_detection_uses_execute_face_detection(monkeypatch): from aare.daq.daq import AareDAQ daq = object.__new__(AareDAQ) - cfg = types.SimpleNamespace( - try_set_busy=lambda timeout=360: None, - state_busy=False, - ) + cfg = types.SimpleNamespace(try_set_busy=lambda timeout=360: None, state_busy=False) setattr(daq, "_AareDAQ__cfg", cfg) setattr( diff --git a/tests/unit/daq/test_gui_timeout.py b/tests/unit/daq/test_gui_timeout.py index 5d89f73b..a8381dc6 100644 --- a/tests/unit/daq/test_gui_timeout.py +++ b/tests/unit/daq/test_gui_timeout.py @@ -2,18 +2,13 @@ from types import SimpleNamespace from unittest.mock import patch -def test_status_renews_gui_session_with_gui_timeout( - client, mock_backend, daq_status_factory -): +def test_status_renews_gui_session_with_gui_timeout(client, mock_backend, daq_status_factory): mock_cfg = mock_backend["cfg"] mock_daq = mock_backend["daq"] from aarecommon.models.models import SessionsStateEnum - mock_daq.status = daq_status_factory( - current_pgroup="p12345", - staff=True, - ) + mock_daq.status = daq_status_factory(current_pgroup="p12345", staff=True) mock_daq.status.sample = None mock_daq.status.box = None mock_daq.status.last_best_res = None @@ -30,10 +25,7 @@ def test_status_renews_gui_session_with_gui_timeout( assert response.status_code == 200 mock_cfg.touch_gui_session.assert_called_once_with( - session=123, - username="testuser", - staff=True, - expiry_sec=10, + session=123, username="testuser", staff=True, expiry_sec=10 ) @@ -44,16 +36,10 @@ def test_status_hides_open_guis_for_non_staff(client, mock_backend, daq_status_f from aare.daq.auth import TokenData mock_parse.return_value = TokenData( - sub="user1", - staff=False, - pgroups=["p12345"], - session=123, + sub="user1", staff=False, pgroups=["p12345"], session=123 ) - mock_backend["daq"].status = daq_status_factory( - current_pgroup="p12345", - staff=False, - ) + mock_backend["daq"].status = daq_status_factory(current_pgroup="p12345", staff=False) mock_backend["daq"].status.sample = None mock_backend["daq"].status.box = None mock_backend["daq"].status.last_best_res = None @@ -62,15 +48,10 @@ def test_status_hides_open_guis_for_non_staff(client, mock_backend, daq_status_f mock_backend["cfg"].pgroup = "p12345" mock_backend["cfg"].session_state.return_value = SessionsStateEnum.OwnedByElse mock_backend["cfg"].get_open_gui_sessions.return_value = [ - OpenGuiSessionInfo( - session=1, username="staff1", last_seen_ts=1.0, staff=True - ) + OpenGuiSessionInfo(session=1, username="staff1", last_seen_ts=1.0, staff=True) ] mock_backend["cfg"].get_gui_session.return_value = OpenGuiSessionInfo( - session=123, - username="user1", - last_seen_ts=2.0, - staff=False, + session=123, username="user1", last_seen_ts=2.0, staff=False ) response = client.get("/status", headers={"Authorization": "Bearer fake-token"}) @@ -94,9 +75,7 @@ def test_admin_gui_sessions_includes_baton_holder_flag(client, mock_backend): ] mock_cfg.baton_holder = SimpleNamespace(session=22) - response = client.get( - "/admin/gui_sessions", headers={"Authorization": "Bearer fake-token"} - ) + response = client.get("/admin/gui_sessions", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 payload = response.json() diff --git a/tests/unit/daq/test_mlbox.py b/tests/unit/daq/test_mlbox.py index 5459204c..c789def4 100644 --- a/tests/unit/daq/test_mlbox.py +++ b/tests/unit/daq/test_mlbox.py @@ -97,8 +97,7 @@ def test_preferred_class_prefers_loop_over_pin_when_margin_not_exceeded(): boxes.add_box(MLBoxType.LOOP_FACE, (1, 1, 9, 9), 0.75) best = MlBox.get_preferred_class_box_with_confidence_threshold( - boxes, - loop_preference_margin=0.1, + boxes, loop_preference_margin=0.1 ) assert best is not None diff --git a/tests/unit/daq/test_mount.py b/tests/unit/daq/test_mount.py index 4e5a1b46..144ffb14 100644 --- a/tests/unit/daq/test_mount.py +++ b/tests/unit/daq/test_mount.py @@ -3,10 +3,7 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from aarecommon.errors.exception_handler import ( - BECCommunicationError, - JFJochCommunicationError, -) +from aarecommon.errors.exception_handler import BECCommunicationError, JFJochCommunicationError from aarecommon.models.models import DAQOperation, DewarAddress, SampleShortInfo from aarecommon.models.tell import TellActivityEnum, TellPhaseEnum, TellStateModel from aareDB import SampleEventType @@ -113,13 +110,9 @@ def test_execute_mount_and_prepare_success_uses_mounting_result_fields(): target_sample.db_id, f"{target_sample.db_id}_mounted" ) assert ( - daq._AareDAQ__set_state.call_args_list[0].args[0] - == BeamlineStateEnum.RobotSampleExchange - ) - assert ( - daq._AareDAQ__set_state.call_args_list[-1].args[0] - == BeamlineStateEnum.SampleAlignment + daq._AareDAQ__set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange ) + assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment def test_execute_mount_and_prepare_marks_previous_sample_unmounted_when_mount_fails_after_auto_unmount(): @@ -151,24 +144,14 @@ def test_execute_mount_and_prepare_marks_previous_sample_unmounted_when_mount_fa assert send_calls[2].args[0] == previous_sample.db_id assert send_calls[2].args[1] == SampleEventType.UNMOUNTED - assert ( - send_calls[2].kwargs["comment"] == "Auto-unmount succeeded before mount failed" - ) + assert send_calls[2].kwargs["comment"] == "Auto-unmount succeeded before mount failed" daq._handle_operation_error.assert_called_once() - assert ( - daq._handle_operation_error.call_args.kwargs["operation"] == DAQOperation.MOUNT - ) + assert daq._handle_operation_error.call_args.kwargs["operation"] == DAQOperation.MOUNT assert daq._handle_operation_error.call_args.kwargs["sample"] == target_sample - assert ( - daq._handle_operation_error.call_args.kwargs["event_type"] - == SampleEventType.MOUNTFAILED - ) + assert daq._handle_operation_error.call_args.kwargs["event_type"] == SampleEventType.MOUNTFAILED - assert ( - daq._AareDAQ__set_state.call_args_list[-1].args[0] - == BeamlineStateEnum.SampleAlignment - ) + assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment def test_execute_mount_and_prepare_does_not_mark_previous_sample_unmounted_when_not_confirmed(): @@ -199,10 +182,7 @@ def test_execute_mount_and_prepare_does_not_mark_previous_sample_unmounted_when_ assert send_calls[1].args[1] == SampleEventType.MOUNTING daq._handle_operation_error.assert_called_once() - assert ( - daq._handle_operation_error.call_args.kwargs["event_type"] - == SampleEventType.MOUNTFAILED - ) + assert daq._handle_operation_error.call_args.kwargs["event_type"] == SampleEventType.MOUNTFAILED def test_execute_mount_and_prepare_unmount_success_uses_unmount_operation(): @@ -231,13 +211,9 @@ def test_execute_mount_and_prepare_unmount_success_uses_unmount_operation(): daq.save_screenshot_db.assert_not_called() assert ( - daq._AareDAQ__set_state.call_args_list[0].args[0] - == BeamlineStateEnum.RobotSampleExchange - ) - assert ( - daq._AareDAQ__set_state.call_args_list[-1].args[0] - == BeamlineStateEnum.SampleAlignment + daq._AareDAQ__set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange ) + assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment def test_raise_if_critical_jfjoch_detector_error_preserves_jfjoch_exception_family(): @@ -255,10 +231,7 @@ def test_raise_if_critical_jfjoch_detector_error_preserves_jfjoch_exception_fami daq._raise_if_critical_jfjoch_detector_error(original, command="wait_till_done") exc = exc_info.value - assert ( - "Critical detector error while running JFJoch command 'wait_till_done'" - in str(exc) - ) + assert "Critical detector error while running JFJoch command 'wait_till_done'" in str(exc) assert exc.operation == "measure" assert exc.endpoint == "/measurement/start" assert exc.base_url == "http://detector" @@ -280,14 +253,10 @@ def test_raise_if_critical_bec_error_preserves_bec_exception_family(): ) with pytest.raises(BECCommunicationError) as exc_info: - daq._raise_if_critical_bec_error( - original, command="planner.move_to:data_collection" - ) + daq._raise_if_critical_bec_error(original, command="planner.move_to:data_collection") exc = exc_info.value - assert "Critical BEC error while running 'planner.move_to:data_collection'" in str( - exc - ) + assert "Critical BEC error while running 'planner.move_to:data_collection'" in str(exc) assert exc.operation == "planner.move_to:data_collection" assert exc.endpoint == "/bec" assert exc.base_url == "redis://bec" @@ -306,10 +275,7 @@ def test_raise_if_critical_jfjoch_detector_error_ignores_non_critical_jfjoch_err status_code=503, ) - assert ( - daq._raise_if_critical_jfjoch_detector_error(original, command="wait_till_done") - is None - ) + assert daq._raise_if_critical_jfjoch_detector_error(original, command="wait_till_done") is None def test_create_loop_centering_service_uses_shared_screenshot_service(): diff --git a/tests/unit/daq/test_raster_logic.py b/tests/unit/daq/test_raster_logic.py index 6099849f..6b34dbc6 100644 --- a/tests/unit/daq/test_raster_logic.py +++ b/tests/unit/daq/test_raster_logic.py @@ -9,11 +9,7 @@ from jfjoch_client.exceptions import NotFoundException from aare.daq.operations.common.runtime import DAQRuntimeState from aare.daq.operations.common.services import OperationServices -from aare.daq.operations.raster.models import ( - RasterContext, - RasterDependencies, - RasterSettings, -) +from aare.daq.operations.raster.models import RasterContext, RasterDependencies, RasterSettings from aare.daq.operations.raster.service import RasterService @@ -39,10 +35,7 @@ def _make_raster_context(*, jfjoch, aare, sample=None): abr_meas_pos=types.SimpleNamespace(at_mm=Coordinate(x=0.0, y=0.0, z=0.0)), get_beam_mark=lambda zoom: (0.0, 0.0), ), - devs=types.SimpleNamespace( - aerotech_omega=0.0, - zoom=100.0, - ), + devs=types.SimpleNamespace(aerotech_omega=0.0, zoom=100.0), mlbox=types.SimpleNamespace(), jfjoch=jfjoch, aare=aare, @@ -50,14 +43,12 @@ def _make_raster_context(*, jfjoch, aare, sample=None): runtime = DAQRuntimeState( sample_provider=types.SimpleNamespace(sample=sample), - sample_geometry_provider=types.SimpleNamespace( - sample_geometry=types.SimpleNamespace() - ), + sample_geometry_provider=types.SimpleNamespace(sample_geometry=types.SimpleNamespace()), status_provider=types.SimpleNamespace(status=None), ) services = OperationServices( - screenshots=types.SimpleNamespace(save_to_db=lambda *args, **kwargs: None), + screenshots=types.SimpleNamespace(save_to_db=lambda *args, **kwargs: None) ) settings = RasterSettings( @@ -66,12 +57,7 @@ def _make_raster_context(*, jfjoch, aare, sample=None): auto_raster_skip_if_exceed_max_image_threshold=True, ) - return RasterContext( - deps=deps, - runtime=runtime, - services=services, - settings=settings, - ) + return RasterContext(deps=deps, runtime=runtime, services=services, settings=settings) def make_service() -> RasterService: @@ -84,9 +70,7 @@ def make_service() -> RasterService: def test_grid_image_id_from_centre_offset_single_cell(): request = make_request(1, 1) image_id = RasterService._grid_image_id_from_centre_offset( - x_mm=request.grid_size_mm.x / 2.0, - y_mm=request.grid_size_mm.y / 2.0, - request=request, + x_mm=request.grid_size_mm.x / 2.0, y_mm=request.grid_size_mm.y / 2.0, request=request ) assert image_id == 0 @@ -98,9 +82,7 @@ def test_grid_image_id_from_centre_offset_odd_grid(): y_mm = ((request.n_y - 1) * request.grid_size_mm.y) / 2.0 image_id = RasterService._grid_image_id_from_centre_offset( - x_mm=x_mm, - y_mm=y_mm, - request=request, + x_mm=x_mm, y_mm=y_mm, request=request ) assert image_id == 7 @@ -113,9 +95,7 @@ def test_grid_image_id_from_centre_offset_even_grid(): y_mm = ((request.n_y - 1) * request.grid_size_mm.y) / 2.0 image_id = RasterService._grid_image_id_from_centre_offset( - x_mm=x_mm, - y_mm=y_mm, - request=request, + x_mm=x_mm, y_mm=y_mm, request=request ) assert image_id == 6 @@ -128,9 +108,7 @@ def test_grid_image_id_from_centre_offset_vertical_scan(): y_mm = ((request.n_y - 1) * request.grid_size_mm.y) / 2.0 image_id = RasterService._grid_image_id_from_centre_offset( - x_mm=x_mm, - y_mm=y_mm, - request=request, + x_mm=x_mm, y_mm=y_mm, request=request ) assert image_id == 24 @@ -141,11 +119,7 @@ def test_grid_image_id_from_centre_offset_rejects_non_positive_cell_size(): request.grid_size_mm = Coordinate(x=0.0, y=0.01) with pytest.raises(ValueError, match="grid_size_mm must be positive"): - RasterService._grid_image_id_from_centre_offset( - x_mm=0.0, - y_mm=0.0, - request=request, - ) + RasterService._grid_image_id_from_centre_offset(x_mm=0.0, y_mm=0.0, request=request) def test_grid_image_id_from_centre_offset_rejects_zero_dimensions(): @@ -153,11 +127,7 @@ def test_grid_image_id_from_centre_offset_rejects_zero_dimensions(): request.n_x = 0 with pytest.raises(ValueError, match="Raster grid dimensions must be >= 1"): - RasterService._grid_image_id_from_centre_offset( - x_mm=0.0, - y_mm=0.0, - request=request, - ) + RasterService._grid_image_id_from_centre_offset(x_mm=0.0, y_mm=0.0, request=request) def test_upload_raster_diffraction_preview_skips_out_of_range_image_id(): @@ -168,11 +138,7 @@ def test_upload_raster_diffraction_preview_skips_out_of_range_image_id(): scan_result.images = [MagicMock(), MagicMock(), MagicMock(), MagicMock()] service._upload_raster_diffraction_preview( - sample_id=123, - filename="preview", - image_id=10, - scan_result=scan_result, - request=request, + sample_id=123, filename="preview", image_id=10, scan_result=scan_result, request=request ) service.ctx.deps.jfjoch.get_diffraction_image.assert_not_called() @@ -188,11 +154,7 @@ def test_upload_raster_diffraction_preview_ignores_not_found(): service.ctx.deps.jfjoch.get_diffraction_image.side_effect = NotFoundException() service._upload_raster_diffraction_preview( - sample_id=123, - filename="preview", - image_id=2, - scan_result=scan_result, - request=request, + sample_id=123, filename="preview", image_id=2, scan_result=scan_result, request=request ) service.ctx.deps.jfjoch.get_diffraction_image.assert_called_once_with( @@ -212,11 +174,7 @@ def test_upload_raster_diffraction_preview_uploads_when_present(): service.ctx.deps.jfjoch.get_diffraction_image.return_value = b"jpeg-bytes" service._upload_raster_diffraction_preview( - sample_id=123, - filename="preview", - image_id=2, - scan_result=scan_result, - request=request, + sample_id=123, filename="preview", image_id=2, scan_result=scan_result, request=request ) service.ctx.deps.jfjoch.get_diffraction_image.assert_called_once_with( diff --git a/tests/unit/daq/test_rotation_screening_diffraction.py b/tests/unit/daq/test_rotation_screening_diffraction.py index 3b4f1be2..414f0839 100644 --- a/tests/unit/daq/test_rotation_screening_diffraction.py +++ b/tests/unit/daq/test_rotation_screening_diffraction.py @@ -44,8 +44,7 @@ def test_screening_diffraction_skips_images_without_id(): svc = _make_service(jfjoch, aare) svc._ingest_screening_diffraction( - SimpleNamespace(db_id=7), - SimpleNamespace(images=[SimpleNamespace(number=None), _img(3)]), + SimpleNamespace(db_id=7), SimpleNamespace(images=[SimpleNamespace(number=None), _img(3)]) ) jfjoch.get_diffraction_image.assert_called_once_with( @@ -61,8 +60,7 @@ def test_screening_diffraction_tolerates_fetch_failure(): svc = _make_service(jfjoch, aare) svc._ingest_screening_diffraction( - SimpleNamespace(db_id=9), - SimpleNamespace(images=[_img(5), _img(6)]), + SimpleNamespace(db_id=9), SimpleNamespace(images=[_img(5), _img(6)]) ) # first wedge failed and was skipped; second still uploaded diff --git a/tests/unit/daq/test_server.py b/tests/unit/daq/test_server.py index 5b7b11f1..eb965d3a 100644 --- a/tests/unit/daq/test_server.py +++ b/tests/unit/daq/test_server.py @@ -19,12 +19,8 @@ def test_status(api, daq_status_factory, monkeypatch): from aare.daq import server - monkeypatch.setattr( - server.auth, "resolve_baton_timeout_if_needed", lambda cfg: None - ) - monkeypatch.setattr( - server.auth, "get_baton_status", lambda cfg, data: {"dummy": "status"} - ) + monkeypatch.setattr(server.auth, "resolve_baton_timeout_if_needed", lambda cfg: None) + monkeypatch.setattr(server.auth, "get_baton_status", lambda cfg, data: {"dummy": "status"}) api.cfg.pending_baton_request = None api.cfg.queued_baton_transfer = None @@ -32,9 +28,7 @@ def test_status(api, daq_status_factory, monkeypatch): api.cfg.allow_non_staff_request_from_staff = False api.daq.status = daq_status_factory( - state=BeamlineStateEnum.Maintenance, - current_pgroup="p12345", - staff=True, + state=BeamlineStateEnum.Maintenance, current_pgroup="p12345", staff=True ) # ... rest of the setup api.daq.status.sample = None @@ -77,27 +71,20 @@ def test_login_success(client): headers={"X-Remote-User": "user"}, ) assert response.status_code == 200 - assert response.json() == { - "access_token": "fake-access-token", - "token_type": "bearer", - } + assert response.json() == {"access_token": "fake-access-token", "token_type": "bearer"} def test_get_image(client, mock_backend): mock_daq = mock_backend["daq"] mock_daq.camera_image = np.zeros((100, 100, 3), dtype=np.uint8) - response = client.get( - "/beamline/image", headers={"Authorization": "Bearer fake-token"} - ) + response = client.get("/beamline/image", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" assert len(response.content) > 0 -def test_mount_returns_tell_exception_when_mount_precheck_fails( - client, mock_backend, monkeypatch -): +def test_mount_returns_tell_exception_when_mount_precheck_fails(client, mock_backend, monkeypatch): from aarecommon.errors.exception_handler import TellCommunicationError from aare.daq import server @@ -110,8 +97,7 @@ def test_mount_returns_tell_exception_when_mount_precheck_fails( ) response = client.post( - "/sample/mount?dbid=1&reference=false", - headers={"Authorization": "Bearer fake-token"}, + "/sample/mount?dbid=1&reference=false", headers={"Authorization": "Bearer fake-token"} ) assert response.status_code == 503 @@ -121,9 +107,7 @@ def test_mount_returns_tell_exception_when_mount_precheck_fails( assert "Mount can't start:" in payload["message"] -def test_mount_calls_tell_mount_precheck_before_mount( - client, mock_backend, monkeypatch -): +def test_mount_calls_tell_mount_precheck_before_mount(client, mock_backend, monkeypatch): from aare.daq import server monkeypatch.setattr(server.auth, "check_jwt_rw", lambda *_args, **_kwargs: None) @@ -135,8 +119,7 @@ def test_mount_calls_tell_mount_precheck_before_mount( mock_daq.reference_tools = SimpleNamespace(s=[]) response = client.post( - "/sample/mount?dbid=1&reference=false", - headers={"Authorization": "Bearer fake-token"}, + "/sample/mount?dbid=1&reference=false", headers={"Authorization": "Bearer fake-token"} ) assert response.status_code == 200 @@ -169,11 +152,7 @@ def test_auto_scan_returns_tell_exception_when_mount_precheck_fails( "location": {"segment": "A", "pos": 1}, } - response = client.post( - "/scan/auto", - json=body, - headers={"Authorization": "Bearer fake-token"}, - ) + response = client.post("/scan/auto", json=body, headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 503 payload = response.json() @@ -182,9 +161,7 @@ def test_auto_scan_returns_tell_exception_when_mount_precheck_fails( assert "Mount can't start:" in payload["message"] -def test_auto_scan_calls_tell_mount_precheck_before_measure( - client, mock_backend, monkeypatch -): +def test_auto_scan_calls_tell_mount_precheck_before_measure(client, mock_backend, monkeypatch): from aare.daq import server monkeypatch.setattr(server.auth, "check_jwt_rw", lambda *_args, **_kwargs: None) @@ -203,11 +180,7 @@ def test_auto_scan_calls_tell_mount_precheck_before_measure( "location": {"segment": "A", "pos": 1}, } - response = client.post( - "/scan/auto", - json=body, - headers={"Authorization": "Bearer fake-token"}, - ) + response = client.post("/scan/auto", json=body, headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 mock_daq.check_tell_mount_start_conditions.assert_called_once_with() @@ -216,9 +189,7 @@ def test_auto_scan_calls_tell_mount_precheck_before_measure( def test_get_pgroup(api): api.cfg.pgroup = "p12345" - response = api.client.get( - "/access/pgroup", headers={"Authorization": "Bearer fake-token"} - ) + response = api.client.get("/access/pgroup", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.json() == "p12345" @@ -240,8 +211,7 @@ def test_delete_pgroup(api): def test_set_commissioning_mode(api): response = api.client.put( - "/beamline/commissioning_mode?val=true", - headers={"Authorization": "Bearer fake-token"}, + "/beamline/commissioning_mode?val=true", headers={"Authorization": "Bearer fake-token"} ) assert response.status_code == 200 assert response.json() == "OK" @@ -253,9 +223,7 @@ def test_get_settings(api): mock_settings = BeamlineSettingsModel() api.cfg.settings = mock_settings - response = api.client.get( - "/beamline/settings", headers={"Authorization": "Bearer fake-token"} - ) + response = api.client.get("/beamline/settings", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.json() == mock_settings.model_dump() @@ -265,9 +233,7 @@ def test_put_settings(api): settings_data = BeamlineSettingsModel().model_dump() response = api.client.put( - "/beamline/settings", - json=settings_data, - headers={"Authorization": "Bearer fake-token"}, + "/beamline/settings", json=settings_data, headers={"Authorization": "Bearer fake-token"} ) assert response.status_code == 200 assert api.cfg.settings.model_dump() == settings_data @@ -290,9 +256,7 @@ def test_put_cryo_settings(api): cryo_data = CryojetSettingsModel().model_dump() response = api.client.put( - "/beamline/cryo_settings", - json=cryo_data, - headers={"Authorization": "Bearer fake-token"}, + "/beamline/cryo_settings", json=cryo_data, headers={"Authorization": "Bearer fake-token"} ) assert response.status_code == 200 assert api.cfg.cryojet_settings.model_dump() == cryo_data @@ -306,9 +270,7 @@ def test_baton_status(api): api.cfg.baton_holder = None api.cfg.queued_baton_transfer = None api.cfg.allow_non_staff_request_from_staff = True - response = api.client.get( - "/baton/status", headers={"Authorization": "Bearer fake-token"} - ) + response = api.client.get("/baton/status", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.json() == mock_baton.model_dump() @@ -316,12 +278,8 @@ def test_baton_status(api): def test_baton_request(api, monkeypatch): from aare.daq import server - monkeypatch.setattr( - server.auth, "request_baton", lambda cfg, data: {"granted": True} - ) - response = api.client.post( - "/baton/request", headers={"Authorization": "Bearer fake-token"} - ) + monkeypatch.setattr(server.auth, "request_baton", lambda cfg, data: {"granted": True}) + response = api.client.post("/baton/request", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.json() == {"granted": True} @@ -329,11 +287,7 @@ def test_baton_request(api, monkeypatch): def test_baton_release(api, monkeypatch): from aare.daq import server - monkeypatch.setattr( - server.auth, "release_baton", lambda cfg, data: {"released": True} - ) - response = api.client.post( - "/baton/release", headers={"Authorization": "Bearer fake-token"} - ) + monkeypatch.setattr(server.auth, "release_baton", lambda cfg, data: {"released": True}) + response = api.client.post("/baton/release", headers={"Authorization": "Bearer fake-token"}) assert response.status_code == 200 assert response.json() == {"released": True} diff --git a/tests/unit/daq/test_server_exception_handler.py b/tests/unit/daq/test_server_exception_handler.py index 45287694..94c66ac2 100644 --- a/tests/unit/daq/test_server_exception_handler.py +++ b/tests/unit/daq/test_server_exception_handler.py @@ -77,9 +77,7 @@ async def test_automation_error_not_critical_returns_422(app, mock_request): @pytest.mark.asyncio -async def test_automation_error_critical_via_instance_flag_returns_503( - app, mock_request -): +async def test_automation_error_critical_via_instance_flag_returns_503(app, mock_request): # Instance-level override: critical=True exc = MountingFailed("threshold breached", critical=True) handler = app.exception_handlers[AutomationError] @@ -90,9 +88,7 @@ async def test_automation_error_critical_via_instance_flag_returns_503( @pytest.mark.asyncio -async def test_automation_error_context_contains_endpoint_and_operation( - app, mock_request -): +async def test_automation_error_context_contains_endpoint_and_operation(app, mock_request): exc = TellCommunicationError( "timeout", endpoint="/state", operation="GET", base_url="http://tell:8000" ) @@ -106,9 +102,7 @@ async def test_automation_error_context_contains_endpoint_and_operation( @pytest.mark.asyncio -async def test_automation_error_excludes_critical_and_headers_from_context( - app, mock_request -): +async def test_automation_error_excludes_critical_and_headers_from_context(app, mock_request): # critical kwarg should not appear in context (it has its own field) exc = MountingFailed("x", critical=True) handler = app.exception_handlers[AutomationError] @@ -244,9 +238,7 @@ async def test_unhandled_exception_is_critical_500(app, mock_request): @pytest.mark.asyncio -async def test_unhandled_exception_empty_message_falls_back_to_class_name( - app, mock_request -): +async def test_unhandled_exception_empty_message_falls_back_to_class_name(app, mock_request): exc = ValueError() handler = app.exception_handlers[Exception] response = await handler(mock_request, exc) @@ -281,9 +273,7 @@ def test_only_four_root_handlers_plus_fallbacks(app): registered = set(app.exception_handlers.keys()) expected = {AutomationError, AareUserError, AareAuthError, HTTPException, Exception} - assert expected.issubset(registered), ( - f"Missing required handlers: {expected - registered}" - ) + assert expected.issubset(registered), f"Missing required handlers: {expected - registered}" # No leftover per-class handlers from the old design forbidden = { @@ -318,9 +308,7 @@ async def test_automation_error_logs_error_when_critical(app, mock_request, capl @pytest.mark.asyncio -async def test_automation_error_logs_warning_when_not_critical( - app, mock_request, caplog -): +async def test_automation_error_logs_warning_when_not_critical(app, mock_request, caplog): exc = MountingFailed("benign") handler = app.exception_handlers[AutomationError] caplog.clear() @@ -345,10 +333,7 @@ async def test_response_body_shape_uniform(app, mock_request): cases: list[tuple[type, Exception]] = [ (AutomationError, MountingFailed("x")), (AutomationError, LoopCenteringFailed("y")), - ( - AutomationError, - SmargonCommunicationError("z", operation="GET", endpoint="/e"), - ), + (AutomationError, SmargonCommunicationError("z", operation="GET", endpoint="/e")), (AutomationError, AareDBCommunicationError("db", critical=True)), (AareUserError, ManualMountException("m")), (AareUserError, SampleException("s")), diff --git a/tests/unit/daq/test_spreadsheetupdater.py b/tests/unit/daq/test_spreadsheetupdater.py index 9f36e93e..0e995ebf 100644 --- a/tests/unit/daq/test_spreadsheetupdater.py +++ b/tests/unit/daq/test_spreadsheetupdater.py @@ -5,11 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from aarecommon.models.models import SampleShortInfoList -from aare.daq.spreadsheetupdater import ( - get_ws_headers, - on_message, - set_spreadsheet_in_redis, -) +from aare.daq.spreadsheetupdater import get_ws_headers, on_message, set_spreadsheet_in_redis @pytest.fixture @@ -83,24 +79,14 @@ def test_on_message_success(mock_config): mock_pucks = [ SimpleNamespace( - puck_name="P1", - dewar_name="D1", - tell_position="A1", - samples=[normal_sample], - ), - SimpleNamespace( - puck_name="Ref", - dewar_name="D1", - tell_position="X1", - samples=[ref_sample], + puck_name="P1", dewar_name="D1", tell_position="A1", samples=[normal_sample] ), + SimpleNamespace(puck_name="Ref", dewar_name="D1", tell_position="X1", samples=[ref_sample]), ] message = json.dumps({"samples": [{}, {}]}) - with patch( - "aare.daq.spreadsheetupdater.PuckWithTellPosition", side_effect=mock_pucks - ): + with patch("aare.daq.spreadsheetupdater.PuckWithTellPosition", side_effect=mock_pucks): on_message(None, message) calls = mock_config._BeamlineConfig__client.set.call_args_list diff --git a/tests/unit/daq/test_tell_state_updater.py b/tests/unit/daq/test_tell_state_updater.py index 7b3664d1..e20ee32e 100644 --- a/tests/unit/daq/test_tell_state_updater.py +++ b/tests/unit/daq/test_tell_state_updater.py @@ -7,9 +7,7 @@ def test_mount_with_auto_unmount_uses_internal_phases(): state = initial_tell_state() state = advance_tell_state( - state, - "shell", - 'mount("F", 2, 1, force=1, read_dm=0, auto_unmount=1)', + state, "shell", 'mount("F", 2, 1, force=1, read_dm=0, auto_unmount=1)' ) assert state.activity == TellActivityEnum.MOUNTING assert state.operation == "mount" @@ -103,9 +101,7 @@ def test_got_result_false_during_auto_unmount_is_not_error(): state = initial_tell_state() state = advance_tell_state( - state, - "shell", - 'mount("D", 5, 14, force=1, read_dm=0, auto_unmount=1)', + state, "shell", 'mount("D", 5, 14, force=1, read_dm=0, auto_unmount=1)' ) assert state.activity == TellActivityEnum.MOUNTING assert state.operation == "mount" @@ -140,9 +136,7 @@ def test_mount_phase_after_old_sample_returned_confirms_auto_unmount_completed() state = initial_tell_state() state = advance_tell_state( - state, - "shell", - 'mount("F", 2, 1, force=1, read_dm=0, auto_unmount=1)', + state, "shell", 'mount("F", 2, 1, force=1, read_dm=0, auto_unmount=1)' ) state = advance_tell_state(state, "shell", "unmount: None None None True") state = advance_tell_state(state, "Motion Sync", "Sample get from Gonio") diff --git a/tests/unit/daq/test_tellupdater.py b/tests/unit/daq/test_tellupdater.py index 0d59fee3..22e7d264 100644 --- a/tests/unit/daq/test_tellupdater.py +++ b/tests/unit/daq/test_tellupdater.py @@ -3,6 +3,7 @@ import json from unittest.mock import MagicMock, patch from aare.daq import tellupdater + def test_compare_and_report_change(): class MockPuck: def __init__(self, puck_name, dewar_id, pos): @@ -17,6 +18,7 @@ def test_compare_and_report_change(): assert joined == {("p3", 1, "A3")} assert left == {("p1", 1, "A1")} + def test_compare_and_report_change_ignore_x1(): class MockPuck: def __init__(self, puck_name, dewar_id, pos): @@ -31,6 +33,7 @@ def test_compare_and_report_change_ignore_x1(): assert joined == set() assert left == set() + @patch("aare.daq.tellupdater.tell_client") @patch("aare.daq.tellupdater.aare_db") def test_handle_tell_change_event(mock_db, mock_tell): @@ -39,37 +42,40 @@ def test_handle_tell_change_event(mock_db, mock_tell): mock_tell.get_detected_pucks.assert_called_once() mock_db.set_pucks_beamline.assert_called_with(["puck1", "puck2"]) + @patch("aare.daq.tellupdater.tell_client") def test_ws_update_samples_info(mock_tell): pucks = [{"id": "1"}] tellupdater.ws_update_samples_info(pucks) mock_tell.set_samples_info.assert_called_with(pucks) + def test_on_sse_event(): class MockEvent: def __init__(self, event, data): self.event = event self.data = data - + event = MockEvent("DewarContentUpdate", "some data") with patch("aare.daq.tellupdater.handle_tell_change_event") as mock_handle: tellupdater.on_sse_event(event) mock_handle.assert_called_once() + def test_extract_tracked_tell_event_with_json_payload(): tracked = tellupdater.extract_tracked_tell_event( - "Gripper detection", - json.dumps({"value": "Pin still in Gripper"}), + "Gripper detection", json.dumps({"value": "Pin still in Gripper"}) ) assert tracked == ("Gripper detection", "Pin still in Gripper") + def test_extract_tracked_tell_event_for_standalone_message(): tracked = tellupdater.extract_tracked_tell_event( - "Motion Sync", - json.dumps({"message": "Sample put on Gonio"}), + "Motion Sync", json.dumps({"message": "Sample put on Gonio"}) ) assert tracked == ("Motion Sync", "Sample put on Gonio") + def test_on_sse_event_tracks_interesting_event(): class MockEvent: def __init__(self, event, data): @@ -85,6 +91,7 @@ def test_on_sse_event_tracks_interesting_event(): assert tellupdater.latest_tell_events["Motion Task"] == "dry" + def test_record_tell_event_logs_to_journal(): tellupdater.latest_tell_events.clear() tellupdater.tell_event_history.clear() @@ -93,9 +100,8 @@ def test_record_tell_event_logs_to_journal(): tellupdater.record_tell_event("Motion Sync", "Sample put on Gonio") assert tellupdater.latest_tell_events["Motion Sync"] == "Sample put on Gonio" - mock_info.assert_called_once_with( - "[TELL][JOURNAL] event=Motion Sync value=Sample put on Gonio" - ) + mock_info.assert_called_once_with("[TELL][JOURNAL] event=Motion Sync value=Sample put on Gonio") + def test_record_tell_event_writes_history_to_redis(): tellupdater.latest_tell_events.clear() @@ -117,6 +123,7 @@ def test_record_tell_event_writes_history_to_redis(): assert payload[0]["event"] == "dry" assert "timestamp" in payload[0] + def test_record_tell_event_keeps_last_25_events(): tellupdater.latest_tell_events.clear() tellupdater.tell_event_history.clear() @@ -136,13 +143,13 @@ def test_record_tell_event_keeps_last_25_events(): assert payload[0]["event"] == "event-5" assert payload[-1]["event"] == "event-29" + def test_on_message_logs_json_decode_error(): with patch("aare.daq.tellupdater.logger.exception") as mock_exception: tellupdater.on_message(None, "{invalid") - mock_exception.assert_called_once_with( - "[WS] Failed to process message" - ) + mock_exception.assert_called_once_with("[WS] Failed to process message") + @patch("aare.daq.tellupdater.tell_client") @patch("aare.daq.tellupdater.aare_db") @@ -152,24 +159,27 @@ def test_handle_tell_change_event_logs_failure(mock_db, mock_tell): with patch("aare.daq.tellupdater.logger.exception") as mock_exception: tellupdater.handle_tell_change_event() - mock_exception.assert_called_once_with( - "[SSE] Failed to update puck state after TELL change" - ) + mock_exception.assert_called_once_with("[SSE] Failed to update puck state after TELL change") + @patch("aare.daq.tellupdater.websocket.WebSocketApp") def test_on_message(mock_ws_app): - message = json.dumps([{ - "id": 1, - "barcode": "B1", - "position": "P1", - "puck_name": "Puck1", - "puck_type": "Type1", - "puck_location_in_dewar": 1, - "dewar_id": 1, - "dewar_name": "Dewar1", - "pgroup": "p12345", - "tell_position": "A1" - }]) + message = json.dumps( + [ + { + "id": 1, + "barcode": "B1", + "position": "P1", + "puck_name": "Puck1", + "puck_type": "Type1", + "puck_location_in_dewar": 1, + "dewar_id": 1, + "dewar_name": "Dewar1", + "pgroup": "p12345", + "tell_position": "A1", + } + ] + ) with patch("aare.daq.tellupdater.ws_update_samples_info") as mock_update: with patch("aare.daq.tellupdater.handle_tell_change_event"): tellupdater.current_pucks = [] diff --git a/tests/unit/daq/test_workflows.py b/tests/unit/daq/test_workflows.py index 42670b0e..a62d3514 100644 --- a/tests/unit/daq/test_workflows.py +++ b/tests/unit/daq/test_workflows.py @@ -47,13 +47,9 @@ def mock_cfg(): def _assert_bec_moved(devs, state): planner_calls = ( - devs.bec_worker.planner.move_to.call_args_list - if devs.bec_worker is not None - else [] - ) - direct_calls = ( - devs.bec_worker.move_to.call_args_list if devs.bec_worker is not None else [] + devs.bec_worker.planner.move_to.call_args_list if devs.bec_worker is not None else [] ) + direct_calls = devs.bec_worker.move_to.call_args_list if devs.bec_worker is not None else [] assert any(call.args == (state,) for call in planner_calls) or any( call.args == (state,) for call in direct_calls @@ -72,9 +68,7 @@ def test_common_2rse(mock_devs, mock_cfg): assert mock_devs.aerotech_pos == ABR_POS_MOUNT -def test_common_2rse_moves_detector_to_safe_position_when_configured( - mock_devs, mock_cfg -): +def test_common_2rse_moves_detector_to_safe_position_when_configured(mock_devs, mock_cfg): mock_devs.dtz = 200 mock_cfg.dtz_safe_position = 600 diff --git a/tests/unit/devices/test_aerotech.py b/tests/unit/devices/test_aerotech.py index 5bc97208..53cad3c6 100644 --- a/tests/unit/devices/test_aerotech.py +++ b/tests/unit/devices/test_aerotech.py @@ -22,8 +22,7 @@ def mock_aerotech_api(): @pytest.fixture def aerotech_controller(mock_aerotech_api): with patch( - "aare.devices.aerotech.cfg_get", - return_value="http://mx-x10sa-queue-01.psi.ch:5234", + "aare.devices.aerotech.cfg_get", return_value="http://mx-x10sa-queue-01.psi.ch:5234" ): controller = AerotechController(MXBeamline.X10SA) @@ -34,13 +33,10 @@ def aerotech_controller(mock_aerotech_api): def test_init_x10sa(mock_aerotech_api): with patch( - "aare.devices.aerotech.cfg_get", - return_value="http://mx-x10sa-queue-01.psi.ch:5234", + "aare.devices.aerotech.cfg_get", return_value="http://mx-x10sa-queue-01.psi.ch:5234" ): controller = AerotechController(MXBeamline.X10SA) - assert ( - controller._AerotechController__base == "http://mx-x10sa-queue-01.psi.ch:5234" - ) + assert controller._AerotechController__base == "http://mx-x10sa-queue-01.psi.ch:5234" assert controller._AerotechController__simulated is False @@ -105,9 +101,7 @@ def test_grid_scan(aerotech_controller, mock_aerotech_api): def test_screening_scan(aerotech_controller, mock_aerotech_api): - aerotech_controller.screening_scan( - rotation_deg=10, wedge_deg=2, time_sec=1, steps=5 - ) + aerotech_controller.screening_scan(rotation_deg=10, wedge_deg=2, time_sec=1, steps=5) mock_aerotech_api.screening_post.assert_called_once() diff --git a/tests/unit/devices/test_enum_pv.py b/tests/unit/devices/test_enum_pv.py index b34ebdf7..b8c613be 100644 --- a/tests/unit/devices/test_enum_pv.py +++ b/tests/unit/devices/test_enum_pv.py @@ -4,6 +4,7 @@ from enum import Enum from aare.devices.enum_pv import EnumPV from aare.devices.set_get_pv import MoveResult + class MockPV: def __init__(self, name): self.pvname = name @@ -18,26 +19,30 @@ class MockPV: def put(self, value): self.value = value + @pytest.fixture def mock_pvs(): - with patch('aare.devices.set_get_pv.PV') as mock_pv: + with patch("aare.devices.set_get_pv.PV") as mock_pv: set_pv = MockPV("SET") get_pv = MockPV("GET") mock_pv.side_effect = lambda name: set_pv if name == "SET" else get_pv yield set_pv, get_pv + def test_enum_pv_init_fail(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = None with pytest.raises(RuntimeError): EnumPV("test", "SET", "GET") + def test_enum_pv_init_success(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") assert epv.name == "test" + def test_enum_pv_position(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") @@ -45,39 +50,43 @@ def test_enum_pv_position(mock_pvs): epv = EnumPV("test", "SET", "GET") assert epv.position == "State1" + def test_enum_pv_resolve_enum(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") - + class MyEnum(Enum): State1 = 1 - + res = epv._resolve(MyEnum.State1) assert res.target == "State1" + def test_enum_pv_resolve_int(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") - + res = epv._resolve(0) assert res.target == "State1" - + with pytest.raises(ValueError): epv._resolve(5) + def test_enum_pv_resolve_str(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = (" State1 ", "State2") epv = EnumPV("test", "SET", "GET") - + res = epv._resolve("state1") assert res.target == " State1 " - + with pytest.raises(ValueError): epv._resolve("Missing") + def test_enum_pv_resolve_invalid_type(mock_pvs): set_pv, get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") diff --git a/tests/unit/devices/test_jfjoch.py b/tests/unit/devices/test_jfjoch.py index 8498cc6d..8250bcc4 100644 --- a/tests/unit/devices/test_jfjoch.py +++ b/tests/unit/devices/test_jfjoch.py @@ -197,9 +197,7 @@ def test_measure_raster(jfjoch_wrapper, mock_jfjoch_client): def test_wait_till_done(jfjoch_wrapper, mock_jfjoch_client): mock_jfjoch_client.result_scan_get.return_value = MagicMock() result = jfjoch_wrapper.wait_till_done(timeout=10) - mock_jfjoch_client.wait_till_done_post_with_http_info.assert_called_once_with( - timeout=10 - ) + mock_jfjoch_client.wait_till_done_post_with_http_info.assert_called_once_with(timeout=10) assert result is not None @@ -219,10 +217,7 @@ def test_get_diffraction_image(jfjoch_wrapper, mock_jfjoch_client): def test_get_diffraction_image_retry(jfjoch_wrapper, mock_jfjoch_client): - mock_jfjoch_client.image_buffer_image_jpeg_get.side_effect = [ - Exception("Error"), - b"image_data", - ] + mock_jfjoch_client.image_buffer_image_jpeg_get.side_effect = [Exception("Error"), b"image_data"] img = jfjoch_wrapper.get_diffraction_image(image_id=1, wait_between_retries_s=0.001) assert img == b"image_data" assert mock_jfjoch_client.image_buffer_image_jpeg_get.call_count == 2 diff --git a/tests/unit/devices/test_mx_lib.py b/tests/unit/devices/test_mx_lib.py index a65ac24b..e7158f1c 100644 --- a/tests/unit/devices/test_mx_lib.py +++ b/tests/unit/devices/test_mx_lib.py @@ -22,6 +22,7 @@ def test_is_epics_type(): class FakeType: pass + assert is_epics_type(mock_pv, FakeType) is False @@ -87,4 +88,4 @@ def test_pv_wait_unknown_type(): mock_pv.type = "unknown" mock_pv.pvname = "TEST:PV" with pytest.raises(ValueError): - pv_wait(mock_pv, 1.0) \ No newline at end of file + pv_wait(mock_pv, 1.0) diff --git a/tests/unit/devices/test_my_motor.py b/tests/unit/devices/test_my_motor.py index 251f7339..575b3f4a 100644 --- a/tests/unit/devices/test_my_motor.py +++ b/tests/unit/devices/test_my_motor.py @@ -2,138 +2,153 @@ import pytest from unittest.mock import MagicMock, patch, PropertyMock from aare.devices.my_motor import MyMotor + @pytest.fixture def mock_motor_base(): - with patch('epics.motor.Motor.__init__', return_value=None) as mock_init: + with patch("epics.motor.Motor.__init__", return_value=None) as mock_init: # We need to yield Mocks that will be used by the instance methods mock_get = MagicMock() mock_put = MagicMock() mock_move = MagicMock() - + # Patch them on the base class epics.motor.Motor - with patch('epics.motor.Motor.get', mock_get), \ - patch('epics.motor.Motor.put', mock_put), \ - patch('epics.motor.Motor.move', mock_move), \ - patch('epics.motor.Motor.PV'), \ - patch('epics.motor.Motor.readback', create=True), \ - patch('epics.motor.Motor.drive', create=True), \ - patch('epics.motor.Motor.stop_motor', create=True), \ - patch('epics.motor.Motor.moving_flag', create=True): + with ( + patch("epics.motor.Motor.get", mock_get), + patch("epics.motor.Motor.put", mock_put), + patch("epics.motor.Motor.move", mock_move), + patch("epics.motor.Motor.PV"), + patch("epics.motor.Motor.readback", create=True), + patch("epics.motor.Motor.drive", create=True), + patch("epics.motor.Motor.stop_motor", create=True), + patch("epics.motor.Motor.moving_flag", create=True), + ): yield mock_init, mock_get, mock_put, mock_move + def test_my_motor_init(mock_motor_base): mock_init, _, _, _ = mock_motor_base m = MyMotor("X10SA-DI-MTR-01") mock_init.assert_called_with("X10SA-DI-MTR-01", timeout=5.0) + def test_my_motor_speed(mock_motor_base): _, mock_get, mock_put, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.return_value = 1.0 assert m.speed == 1.0 - mock_get.assert_called_with('VELO') - + mock_get.assert_called_with("VELO") + # When m.speed = 2.0 is called, it might use epics.motor.Motor.put # Let's see if we can just test that it doesn't crash for now if we can't capture the call m.speed = 2.0 + def test_my_motor_properties(mock_motor_base): m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' - with patch('epics.motor.Motor.readback', new_callable=PropertyMock, create=True) as mock_rb: + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" + with patch("epics.motor.Motor.readback", new_callable=PropertyMock, create=True) as mock_rb: mock_rb.return_value = 10.0 assert m.position == 10.0 - - with patch('epics.motor.Motor.drive', new_callable=PropertyMock, create=True) as mock_drive: + + with patch("epics.motor.Motor.drive", new_callable=PropertyMock, create=True) as mock_drive: mock_drive.return_value = 5.0 assert m.value == 5.0 - # m.value = 6.0 calls self.drive = 6.0. + # m.value = 6.0 calls self.drive = 6.0. # In epics.Motor, drive is a PVProperty. m.value = 6.0 + def test_my_motor_stop(mock_motor_base): m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' - with patch('epics.motor.Motor.stop_motor', create=True) as mock_stop: + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" + with patch("epics.motor.Motor.stop_motor", create=True) as mock_stop: m.stop() mock_stop.assert_called_once() + def test_my_motor_moving(mock_motor_base): _, mock_get, _, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.return_value = 1 assert m.moving is True mock_get.return_value = 0 assert m.moving is False + def test_my_motor_units(mock_motor_base): _, mock_get, _, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.return_value = "mm" assert m.units == "mm" mock_get.assert_called_with("EGU", as_string=True) + def test_my_motor_limits(mock_motor_base): _, mock_get, mock_put, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.side_effect = [100, 0] assert m.limits == (100, 0) - + m.limits = (0, 100) + def test_my_motor_move_motor(mock_motor_base): _, _, _, mock_move = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" m.move_motor(10, relative=True, wait=True) mock_move.assert_called_with(10, relative=True, wait=True, timeout=300.0) + def test_my_motor_home(mock_motor_base): _, _, mock_put, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' - m.home(direction='forward') - mock_put.assert_called_with('HOMF', 1) - - m.home(direction='reverse') - mock_put.assert_called_with('HOMR', 1) + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" + m.home(direction="forward") + mock_put.assert_called_with("HOMF", 1) + + m.home(direction="reverse") + mock_put.assert_called_with("HOMR", 1) + def test_my_motor_wait_for_stop(mock_motor_base): _, mock_get, _, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" # Simulate moving then stopping # First 2 calls return 1 (moving), 3rd call returns 0 (stopped) mock_get.side_effect = [1, 1, 0] m.wait_for_stop(timeout=1.0, poll_rate=0.001) + def test_my_motor_wait_for_stop_timeout(mock_motor_base): _, mock_get, _, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.return_value = 1 with pytest.raises(RuntimeError): m.wait_for_stop(timeout=0.01, poll_rate=0.001) + @pytest.mark.asyncio async def test_my_motor_wait_for_stop_async(mock_motor_base): _, mock_get, _, _ = mock_motor_base m = MyMotor("MTR") - with patch.object(MyMotor, 'name', create=True, new_callable=PropertyMock) as mock_name: - mock_name.return_value = 'MTR' + with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: + mock_name.return_value = "MTR" mock_get.side_effect = [1, 0] await m.wait_for_stop_async(timeout=1.0, poll_rate=0.001) diff --git a/tests/unit/devices/test_tell_client.py b/tests/unit/devices/test_tell_client.py index aa4a0b0b..85a671c9 100644 --- a/tests/unit/devices/test_tell_client.py +++ b/tests/unit/devices/test_tell_client.py @@ -68,9 +68,7 @@ def test_mount_state_event_succeeds_when_command_completes( mock_backend.get_result.assert_called() # check_command_ok consulted the result -def test_mount_state_event_reports_no_pin_when_nothing_mounted( - mock_beamline, mock_backend -): +def test_mount_state_event_reports_no_pin_when_nothing_mounted(mock_beamline, mock_backend): """If the command completes but the robot reports nothing mounted, the gripper was empty: mount() must return NO_PIN_IN_GRIPPER (so the service raises MountingFailed) rather than reporting success.""" @@ -89,9 +87,7 @@ def test_mount_state_event_reports_no_pin_when_nothing_mounted( assert result == TellEventValueEnum.NO_PIN_IN_GRIPPER -def test_mount_state_event_raises_when_command_not_completed( - mock_beamline, mock_backend -): +def test_mount_state_event_raises_when_command_not_completed(mock_beamline, mock_backend): """A genuine failure still surfaces: if the command did not complete, check_command_ok raises (MountingFailed) and mount() re-raises it as a critical TellCommunicationError, even though the event value was "Busy".""" diff --git a/tests/unit/devices/test_workflow_tools.py b/tests/unit/devices/test_workflow_tools.py index 6388eda2..486f7e9a 100644 --- a/tests/unit/devices/test_workflow_tools.py +++ b/tests/unit/devices/test_workflow_tools.py @@ -3,33 +3,40 @@ from unittest.mock import MagicMock from aare.devices.workflow_tools import wait_position import time + class MockMotor: def __init__(self, position): self.readback = position self.name = "MockMotor" + def __str__(self): return self.name + def test_wait_position_float_no_tolerance(): motor = MockMotor(10.0) wait_position(motor, 10.0) + def test_wait_position_float_with_tolerance(): motor = MockMotor(10.05) wait_position(motor, 10.0, tolerance=0.1) + def test_wait_position_bytes(): motor = MockMotor(b"Open") wait_position(motor, "Open") wait_position(motor, b"open") + def test_wait_position_timeout(): motor = MockMotor(10.0) # We want it to fail, so we set a target it will never reach with pytest.raises(RuntimeError, match="Timeout"): wait_position(motor, 20.0, tolerance=0.1, timeout=0.1) + def test_wait_position_invalid_type(): - motor = MockMotor([1,2]) + motor = MockMotor([1, 2]) with pytest.raises(RuntimeError, match="could not understand arguments"): wait_position(motor, 10.0) diff --git a/tests/unit/gui/test_auth_mock.py b/tests/unit/gui/test_auth_mock.py index d34b6475..a339df5b 100644 --- a/tests/unit/gui/test_auth_mock.py +++ b/tests/unit/gui/test_auth_mock.py @@ -9,9 +9,7 @@ from aare.gui.auth import auth def test_auth_success(mocker): mock_run = mocker.patch("aare.gui.auth.subprocess.run") mock_run.return_value = mocker.Mock( - returncode=0, - stdout=json.dumps({"access_token": "fake_token_abc.123.xyz"}), - stderr="", + returncode=0, stdout=json.dumps({"access_token": "fake_token_abc.123.xyz"}), stderr="" ) token = auth("http://test-server", "/tmp/test-cert.pem") @@ -42,5 +40,5 @@ def test_auth_no_url_returns_dummy_jwt(mocker): token = auth(None, None) assert isinstance(token, str) - assert token.count('.') == 2 - mock_run.assert_not_called() \ No newline at end of file + assert token.count(".") == 2 + mock_run.assert_not_called() diff --git a/tests/unit/gui/test_automation_progress_parser.py b/tests/unit/gui/test_automation_progress_parser.py index 49f973ab..f948d585 100644 --- a/tests/unit/gui/test_automation_progress_parser.py +++ b/tests/unit/gui/test_automation_progress_parser.py @@ -11,9 +11,7 @@ jfjoch_client_scan_result_module.ScanResult = object sys.modules.setdefault("jfjoch_client", jfjoch_client_module) sys.modules.setdefault("jfjoch_client.models", jfjoch_client_models_module) -sys.modules.setdefault( - "jfjoch_client.models.scan_result", jfjoch_client_scan_result_module -) +sys.modules.setdefault("jfjoch_client.models.scan_result", jfjoch_client_scan_result_module) from aarecommon.models.automation import StepStatus, WorkflowStateKind @@ -151,9 +149,7 @@ def test_handle_automation_progress_event_dedups_events_by_timestamp(caplog): def test_handle_automation_progress_event_trips_recurrence_watcher(): worker = DAQWorker(base_url=None, token="test-token") - worker._recurrence_watchers = [ - w for w in worker._recurrence_watchers if w.name == "alc" - ] + worker._recurrence_watchers = [w for w in worker._recurrence_watchers if w.name == "alc"] trips: list[str] = [] worker.automation_critical_failure.connect(trips.append) @@ -204,9 +200,7 @@ def test_process_automation_progress_buffer_handles_split_sse_event(): worker.automation_progress.connect(emitted.append) - first_chunk = ( - 'data: {"seq":1,"progress":{"current_step":"Center","steps":[{"step":"mount",' - ) + first_chunk = 'data: {"seq":1,"progress":{"current_step":"Center","steps":[{"step":"mount",' second_chunk = ( '"status":"success","message":"Mount complete"},{"step":"loop_centre",' '"status":"running","message":"Centering sample"}],' diff --git a/tests/unit/gui/test_axis_video_thread.py b/tests/unit/gui/test_axis_video_thread.py index ef2f5951..828fb594 100644 --- a/tests/unit/gui/test_axis_video_thread.py +++ b/tests/unit/gui/test_axis_video_thread.py @@ -5,33 +5,38 @@ from unittest.mock import MagicMock, patch from PySide6.QtGui import QImage from aare.gui.threads.axis_video_thread import VideoThread + @pytest.fixture def video_thread(qtbot): thread = VideoThread("127.0.0.1", camera=1) return thread + def test_init(video_thread): assert video_thread.camera_ip == "127.0.0.1" assert video_thread.camera == 1 assert video_thread.running is False assert video_thread.is_busy is False + def test_set_camera_ip(video_thread): video_thread.set_camera_ip("192.168.1.1") assert video_thread.camera_ip == "192.168.1.1" + def test_set_busy(video_thread): video_thread.set_busy(True) assert video_thread.is_busy is True + def test_process_buffer_success(video_thread, qtbot): # Create a small valid JPEG img = np.zeros((10, 10, 3), dtype=np.uint8) - _, jpeg_bytes = cv2.imencode('.jpg', img) + _, jpeg_bytes = cv2.imencode(".jpg", img) jpeg_bytes = jpeg_bytes.tobytes() - boundary = b'--boundary' - buffer = b'--boundary\r\nContent-Type: image/jpeg\r\n\r\n' + jpeg_bytes + b'\r\n--boundary' + boundary = b"--boundary" + buffer = b"--boundary\r\nContent-Type: image/jpeg\r\n\r\n" + jpeg_bytes + b"\r\n--boundary" with qtbot.waitSignal(video_thread.frame_ready, timeout=1000) as blocker: video_thread._process_buffer(buffer, boundary) @@ -40,13 +45,15 @@ def test_process_buffer_success(video_thread, qtbot): assert blocker.args[0].width() == 10 assert blocker.args[0].height() == 10 + def test_process_buffer_invalid_jpeg(video_thread): - boundary = b'--boundary' - buffer = b'--boundary\r\nContent-Type: image/jpeg\r\n\r\nNOT_A_JPEG\r\n--boundary' + boundary = b"--boundary" + buffer = b"--boundary\r\nContent-Type: image/jpeg\r\n\r\nNOT_A_JPEG\r\n--boundary" # Should not emit anything or raise exception video_thread._process_buffer(buffer, boundary) + def test_stop(video_thread): mock_session = MagicMock() video_thread.session = mock_session @@ -62,7 +69,8 @@ def test_stop(video_thread): video_thread.quit.assert_called_once() video_thread.wait.assert_called_once_with(5000) -@patch('requests.Session') + +@patch("requests.Session") def test_run_connection_error(mock_session_class, video_thread, qtbot): mock_session = mock_session_class.return_value mock_session.get.side_effect = Exception("Connection Refused") @@ -70,4 +78,4 @@ def test_run_connection_error(mock_session_class, video_thread, qtbot): with qtbot.waitSignal(video_thread.error_occurred, timeout=1000) as blocker: video_thread.run() - assert "Unexpected error" in blocker.args[0] \ No newline at end of file + assert "Unexpected error" in blocker.args[0] diff --git a/tests/unit/gui/test_data_collection_settings.py b/tests/unit/gui/test_data_collection_settings.py index e7f2fbae..a9924864 100644 --- a/tests/unit/gui/test_data_collection_settings.py +++ b/tests/unit/gui/test_data_collection_settings.py @@ -105,9 +105,7 @@ def panel(qapp, diffraction): def test_editing_dtz_updates_resolution_and_switches_to_mine(panel, diffraction): _edit(panel.dtz_enter, "250.00") assert panel._source == DbOverrideLineEdit.SOURCE_MINE - assert ( - abs(panel.high_res_enter.value - diffraction.resolution_angstrom(250.0)) < 0.01 - ) + assert abs(panel.high_res_enter.value - diffraction.resolution_angstrom(250.0)) < 0.01 def test_editing_resolution_updates_dtz(panel, diffraction): @@ -145,11 +143,7 @@ def test_user_override_persists_across_samples(panel, diffraction): # Sample 1 loads a database resolution. panel._sample = types.SimpleNamespace(db_id=1) panel._params = types.SimpleNamespace( - targetresolution=2.5, - transmission=0.5, - totalrange=180.0, - oscillation=0.1, - exposure=0.02, + targetresolution=2.5, transmission=0.5, totalrange=180.0, oscillation=0.1, exposure=0.02 ) panel.update_data_collection_parameters() @@ -160,11 +154,7 @@ def test_user_override_persists_across_samples(panel, diffraction): # Sample 2 arrives with a different database exposure. panel._sample = types.SimpleNamespace(db_id=2) panel._params = types.SimpleNamespace( - targetresolution=1.8, - transmission=1.0, - totalrange=360.0, - oscillation=0.2, - exposure=0.01, + targetresolution=1.8, transmission=1.0, totalrange=360.0, oscillation=0.2, exposure=0.01 ) panel.update_data_collection_parameters() @@ -187,9 +177,7 @@ def raster_panel(qapp, diffraction): pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0, z=0), aerotech_meas=Coordinate(x=0, y=0, z=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0.0, chi_deg=0.0 - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0.0, chi_deg=0.0), omega_deg=0.0, beam_size_mm=Coordinate(x=0.01, y=0.01), ) diff --git a/tests/unit/gui/test_login.py b/tests/unit/gui/test_login.py index 22f92505..0b761a1f 100644 --- a/tests/unit/gui/test_login.py +++ b/tests/unit/gui/test_login.py @@ -4,38 +4,43 @@ from aare.gui.widgets.login import LoginDialog from unittest.mock import MagicMock, patch import jwt + @pytest.fixture def login_dialog(qtbot): dialog = LoginDialog(base_url=None) qtbot.add_widget(dialog) return dialog + def test_login_dialog_init(login_dialog): assert login_dialog.windowTitle() == "User Authentication" assert login_dialog.name_entry.text() != "" + def test_login_dialog_authenticate_no_url(login_dialog, qtbot): login_dialog.name_entry.setText("testuser") - + with qtbot.wait_signal(login_dialog.accepted): qtbot.mouseClick(login_dialog.ok_button, Qt.LeftButton) - + assert login_dialog.token != "" decoded = jwt.decode(login_dialog.token, "ABC123", algorithms=["HS256"]) assert decoded["sub"] == "testuser" + @patch("aare.gui.widgets.login.QNetworkAccessManager") def test_login_dialog_authenticate_with_url(mock_nam, qtbot): dialog = LoginDialog(base_url="http://test") qtbot.add_widget(dialog) - + mock_instance = mock_nam.return_value mock_reply = MagicMock() mock_instance.post.return_value = mock_reply - + dialog.authenticate() mock_instance.post.assert_called_once() - + + def test_login_dialog_cancel(login_dialog, qtbot): with qtbot.wait_signal(login_dialog.rejected): qtbot.mouseClick(login_dialog.cancel_button, Qt.LeftButton) diff --git a/tests/unit/gui/test_message_box.py b/tests/unit/gui/test_message_box.py index bf8ca4ea..61a3f5eb 100644 --- a/tests/unit/gui/test_message_box.py +++ b/tests/unit/gui/test_message_box.py @@ -1,62 +1,77 @@ import pytest from unittest.mock import MagicMock, patch from PySide6.QtWidgets import QMessageBox -from aare.gui.widgets.message_box import reply_box, timer_box, ring_current_low_check, experiment_hutch_shutter_check, ring_current_auto_check +from aare.gui.widgets.message_box import ( + reply_box, + timer_box, + ring_current_low_check, + experiment_hutch_shutter_check, + ring_current_auto_check, +) + def test_reply_box(qtbot): parent = MagicMock() - with patch('PySide6.QtWidgets.QMessageBox.question') as mock_question: + with patch("PySide6.QtWidgets.QMessageBox.question") as mock_question: mock_question.return_value = QMessageBox.StandardButton.Yes res = reply_box(parent, "Title", "Message") assert res == QMessageBox.StandardButton.Yes mock_question.assert_called_once() + def test_timer_box_auto_accept(qtbot): parent = None condition_func = MagicMock(return_value=True) - + # We need to process events for timer to fire box = timer_box(parent, condition_func=condition_func) - + # Wait until box is closed by check() qtbot.waitUntil(lambda: not box.isVisible(), timeout=2000) - + assert box.result() == QMessageBox.StandardButton.Yes + def test_ring_current_low_check_ok(qtbot): # Should return True immediately if current is high enough assert ring_current_low_check(None, 200.0) is True + def test_ring_current_low_check_low_yes(qtbot): - with patch('aare.gui.widgets.message_box.reply_box') as mock_reply: + with patch("aare.gui.widgets.message_box.reply_box") as mock_reply: mock_reply.return_value = QMessageBox.StandardButton.Yes assert ring_current_low_check(None, 50.0) is True mock_reply.assert_called_once() + def test_ring_current_low_check_low_no(qtbot): - with patch('aare.gui.widgets.message_box.reply_box') as mock_reply: + with patch("aare.gui.widgets.message_box.reply_box") as mock_reply: mock_reply.return_value = QMessageBox.StandardButton.No assert ring_current_low_check(None, 50.0) is False + def test_experiment_hutch_shutter_check_open(qtbot): assert experiment_hutch_shutter_check(None, True) is True + def test_experiment_hutch_shutter_check_closed_yes(qtbot): - with patch('aare.gui.widgets.message_box.reply_box') as mock_reply: + with patch("aare.gui.widgets.message_box.reply_box") as mock_reply: mock_reply.return_value = QMessageBox.StandardButton.Yes assert experiment_hutch_shutter_check(None, False) is True + def test_ring_current_auto_check_yes(qtbot): # This one uses a nested event loop, which can be tricky to test. # We'll mock timer_box to return a box that we can close manually. - with patch('aare.gui.widgets.message_box.timer_box') as mock_timer_box: + with patch("aare.gui.widgets.message_box.timer_box") as mock_timer_box: box = QMessageBox() box.setStandardButtons(QMessageBox.StandardButton.Yes) mock_timer_box.return_value = box - + # We need to close the box after some time to break the loop from PySide6.QtCore import QTimer + QTimer.singleShot(100, lambda: box.done(QMessageBox.StandardButton.Yes)) - + res = ring_current_auto_check(None, 50.0, lambda: False) assert res is True diff --git a/tests/unit/gui/test_models.py b/tests/unit/gui/test_models.py index d5b3624b..718ae6e0 100644 --- a/tests/unit/gui/test_models.py +++ b/tests/unit/gui/test_models.py @@ -113,13 +113,8 @@ def test_sample_queue_model_set_running(sample_list): def test_sample_queue_model_header(sample_list): model = SampleQueueSpreadsheet(samples=sample_list[:2]) - assert ( - model.headerData(0, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole) - == "Dewar" - ) - assert ( - model.headerData(1, Qt.Orientation.Vertical, Qt.ItemDataRole.DisplayRole) == "2" - ) + assert model.headerData(0, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole) == "Dewar" + assert model.headerData(1, Qt.Orientation.Vertical, Qt.ItemDataRole.DisplayRole) == "2" def test_sample_queue_model_flags(sample_list): diff --git a/tests/unit/gui/test_panels.py b/tests/unit/gui/test_panels.py index 1b208b93..a481a93b 100644 --- a/tests/unit/gui/test_panels.py +++ b/tests/unit/gui/test_panels.py @@ -50,9 +50,7 @@ def mock_daq_status(): pixel_in_mm=0.001, aerotech=Coordinate(x=0, y=0), aerotech_meas=Coordinate(x=0, y=0), - smargon=SmargonCoordinate( - sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0 - ), + smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0), omega_deg=10.5, beam_size_mm=Coordinate(x=0.02, y=0.01), ), diff --git a/tests/unit/gui/test_sse_client.py b/tests/unit/gui/test_sse_client.py index 32769361..7fcca073 100644 --- a/tests/unit/gui/test_sse_client.py +++ b/tests/unit/gui/test_sse_client.py @@ -4,15 +4,18 @@ from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkRe from aare.gui.threads.sse_client import SSEClient from unittest.mock import MagicMock, patch + @pytest.fixture def sse_client(qtbot): client = SSEClient() return client + def test_sse_client_init(sse_client): assert not sse_client.is_connected() assert sse_client._reconnect_delay == 1000 + def test_sse_client_parse_message(sse_client, qtbot): # Test simple message with qtbot.wait_signal(sse_client.message_received) as blocker: @@ -20,6 +23,7 @@ def test_sse_client_parse_message(sse_client, qtbot): sse_client._parse_sse_line("") assert blocker.args == ["hello"] + def test_sse_client_parse_event(sse_client, qtbot): # Test event with data with qtbot.wait_signal(sse_client.event_received) as blocker: @@ -28,6 +32,7 @@ def test_sse_client_parse_event(sse_client, qtbot): sse_client._parse_sse_line("") assert blocker.args == ["update", "some data"] + def test_sse_client_multiline_data(sse_client, qtbot): with qtbot.wait_signal(sse_client.message_received) as blocker: sse_client._parse_sse_line("data: line1") @@ -35,32 +40,35 @@ def test_sse_client_multiline_data(sse_client, qtbot): sse_client._parse_sse_line("") assert blocker.args == ["line1\nline2"] + def test_sse_client_buffer_processing(sse_client, qtbot): sse_client._buffer = QByteArray(b"data: chunk1\n\n") with qtbot.wait_signal(sse_client.message_received, timeout=1000) as blocker: sse_client._process_buffer() assert blocker.args == ["chunk1"] - + sse_client._buffer = QByteArray(b"data: chunk2\n\n") with qtbot.wait_signal(sse_client.message_received, timeout=1000) as blocker: sse_client._process_buffer() assert blocker.args == ["chunk2"] + def test_sse_client_retry_parsing(sse_client): sse_client._parse_sse_line("retry: 5000") assert sse_client._reconnect_delay == 5000 - + sse_client._parse_sse_line("retry: invalid") assert sse_client._reconnect_delay == 5000 + def test_sse_client_disconnect(sse_client, qtbot): # Mock a reply mock_reply = MagicMock(spec=QNetworkReply) sse_client._reply = mock_reply sse_client._connected = True - + with qtbot.wait_signal(sse_client.disconnected): sse_client.disconnect_from_sse() - + assert not sse_client._connected mock_reply.abort.assert_called_once() diff --git a/tests/unit/gui/test_tutorials.py b/tests/unit/gui/test_tutorials.py index f9d6a60b..3608aa4c 100644 --- a/tests/unit/gui/test_tutorials.py +++ b/tests/unit/gui/test_tutorials.py @@ -3,16 +3,27 @@ from unittest.mock import MagicMock from PySide6.QtWidgets import QWidget from aare.gui.tutorials.tutorial_manager import TutorialManager from aare.gui.tutorials.tutorial_models import ( - TutorialScenario, TutorialStepDefinition, TutorialMode, StepKind, TutorialTextRef, TutorialTarget, TargetKind + TutorialScenario, + TutorialStepDefinition, + TutorialMode, + StepKind, + TutorialTextRef, + TutorialTarget, + TargetKind, ) from aare.gui.tutorials.tutorial_runtime import ( - DictionaryTextResolver, NoOpActionExecutor, DictTargetResolver, TutorialEventBus + DictionaryTextResolver, + NoOpActionExecutor, + DictTargetResolver, + TutorialEventBus, ) + @pytest.fixture def mock_window(qapp): return QWidget() + @pytest.fixture def tutorial_scenario(): step1 = TutorialStepDefinition( @@ -20,42 +31,45 @@ def tutorial_scenario(): kind=StepKind.INFO, title=TutorialTextRef(key="step1_title"), body=TutorialTextRef(key="step1_body"), - target=TutorialTarget(kind=TargetKind.NONE, target_id="") + target=TutorialTarget(kind=TargetKind.NONE, target_id=""), ) scenario = TutorialScenario( id="test_scenario", title=TutorialTextRef(key="scenario_title"), description=TutorialTextRef(key="scenario_desc"), mode=TutorialMode.LINEAR, - steps=[step1] + steps=[step1], ) return scenario + def test_tutorial_manager_start_stop(qtbot, mock_window, tutorial_scenario): - text_resolver = DictionaryTextResolver({ - "en": { - "step1_title": "Step 1", - "step1_body": "This is step 1", - "scenario_title": "Test", - "scenario_desc": "Test Desc" + text_resolver = DictionaryTextResolver( + { + "en": { + "step1_title": "Step 1", + "step1_body": "This is step 1", + "scenario_title": "Test", + "scenario_desc": "Test Desc", + } } - }) + ) target_resolver = DictTargetResolver({}) action_executor = NoOpActionExecutor() event_bus = TutorialEventBus() - + manager = TutorialManager( parent_window=mock_window, target_resolver=target_resolver, text_resolver=text_resolver, action_executor=action_executor, - event_bus=event_bus + event_bus=event_bus, ) manager.add_scenario(tutorial_scenario) - + manager.start("test_scenario") assert manager.get_current_step() is not None assert manager.get_current_step().id == "step1" - + manager.stop() assert manager.get_current_step() is None diff --git a/tests/unit/gui/test_widgets.py b/tests/unit/gui/test_widgets.py index ed4c0160..92746fa4 100644 --- a/tests/unit/gui/test_widgets.py +++ b/tests/unit/gui/test_widgets.py @@ -3,44 +3,47 @@ from PySide6.QtCore import Qt from aare.gui.widgets.alert_banner import AlertBanner from aare.gui.widgets.status_label import StatusLabel + def test_alert_banner_show_message(qtbot): banner = AlertBanner() qtbot.addWidget(banner) - + assert not banner.isVisible() - + banner.show_message("Test Error", is_error=True) assert banner.isVisible() assert "Test Error" in banner._label.text() assert "🛑" in banner._label.text() - + banner.show_message("Test Success", is_error=False) assert banner.isVisible() assert "Test Success" in banner._label.text() assert "✅" in banner._label.text() - + banner.clear_message() assert not banner.isVisible() + def test_alert_banner_waiting(qtbot): banner = AlertBanner() qtbot.addWidget(banner) - + banner.show_waiting("Working", countdown_seconds=10) assert banner.isVisible() assert "Working" in banner._label.text() assert "⏳" in banner._label.text() assert "(10s)" in banner._label.text() - + # Tick manually if we wanted to test timer, but usually we just test state banner._tick_countdown() assert "(9s)" in banner._label.text() + def test_status_label(qtbot): label = StatusLabel(val=1.234, decimals=2) qtbot.addWidget(label) - + assert label.text() == "1.23" - + label.new_value(5.6) assert label.text() == "5.60"