From a1606c71eb392e4b10ce31c71631b1c397033b06 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 16 Jul 2026 09:51:26 +0100 Subject: [PATCH 01/14] Updated slot number determination logic in the 'StagePositionValues' Pydantic model --- src/murfey/util/models.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/murfey/util/models.py b/src/murfey/util/models.py index cdf6898c3..6c267cac6 100644 --- a/src/murfey/util/models.py +++ b/src/murfey/util/models.py @@ -111,9 +111,30 @@ class StagePositionValues(BaseModel): @computed_field def slot_number(self) -> int | None: - if self.x is None: - return None - return 1 if self.x < 0 else 2 + """ + In the Aquilos, -75 degrees is the stage rotation value when the sample grid is + facing the electron and ion beam guns, and the plane formed by the two guns is + normal to the surface of the sample grid. + + The xyz values recorded represent the stage position that brings the imaged + region into focus. At a rotation of -75 degrees, a negative x-value corresponds + to Slot 1, while a positive one corresponds to Slot 2. + + The xy values that keep a particular point on the grid at the euncentric focus + under the electron and ion beams will change based on the stage rotation. This + means that a transformation matrix will need to be applied to estimate the + x-value of the region when the stage rotation is at -75 degrees. The correct + slot number can then be determined from there. + """ + if self.x is not None and self.y is not None and self.rotation is not None: + # Rotate the xy-coordinates to the -75 degrees frame + theta = math.radians(self.rotation + 75) + sin = math.sin(theta) + cos = math.cos(theta) + x_rot = (self.x * cos) - (self.y * sin) + return 1 if x_rot < 0 else 2 + # return 1 if self.x < 0 else 2 + return None class StagePositionInfo(BaseModel): From f52b8e61d5b825bf0b937c51ab3aa5301af11dec Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 16 Jul 2026 09:53:11 +0100 Subject: [PATCH 02/14] Updated tests to reflect new stage position logic --- tests/client/contexts/test_fib.py | 6 +++++- tests/workflows/fib/test_register_atlas.py | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/client/contexts/test_fib.py b/tests/client/contexts/test_fib.py index c9fc019c1..af47f2b48 100644 --- a/tests/client/contexts/test_fib.py +++ b/tests/client/contexts/test_fib.py @@ -756,7 +756,11 @@ def test_fib_full_autotem_context_drift_correction_images( if has_stage_position: stage_dict: dict[str, dict] = {"preparation_site": {}} if has_stage_values: - stage_dict["preparation_site"] = {"x": 0.003} + stage_dict["preparation_site"] = { + "x": 0.003, + "y": 0.003, + "rotation": -75, + } metadata_dict["stage_info"] = stage_dict if has_site_info: context._site_info[lamella_num] = LamellaSiteInfo(**metadata_dict) diff --git a/tests/workflows/fib/test_register_atlas.py b/tests/workflows/fib/test_register_atlas.py index 455a62ad4..158b7dc48 100644 --- a/tests/workflows/fib/test_register_atlas.py +++ b/tests/workflows/fib/test_register_atlas.py @@ -154,6 +154,7 @@ def create_electron_snapshot_metadata( -1.309, # Rotation 0.8, # Alpha tilt 0, # Beta tilt + 2, # Expected slot number 3072, # Image size X 2048, # Y 1e-6, # Pixel size X @@ -171,9 +172,10 @@ def create_electron_snapshot_metadata( -0.003, # Stage X 0.0003, # Y 0.01, # Z - 1.309, # Rotation + 1.833, # Rotation 0, # Alpha tilt 0, # Beta tilt + 1, # Expected slot number 3072, # Image size X 2048, # Y 1e-6, # Pixel size X @@ -200,6 +202,7 @@ def test_parse_metadata( float, int, int, + int, float, float, ], @@ -221,6 +224,7 @@ def test_parse_metadata( rotation, tilt_alpha, tilt_beta, + expected_slot_number, pixels_x, pixels_y, pixel_size_x, @@ -234,7 +238,6 @@ def test_parse_metadata( / image_name / f"{image_name}.tiff" ) - slot_number = 1 if pos_x < 0 else 2 # Mock the results of opening an image file xml_string = create_electron_snapshot_metadata( @@ -283,8 +286,8 @@ def test_parse_metadata( assert parsed.pixels_y == pixels_y assert parsed.pixel_size_x == pixel_size_x assert parsed.pixel_size_y == pixel_size_y - assert parsed.slot_number == slot_number - assert parsed.site_name == f"{project_name}--slot_{slot_number}" + assert parsed.slot_number == expected_slot_number + assert parsed.site_name == f"{project_name}--slot_{expected_slot_number}" assert parsed.pixel_size == 0.5 * (pixel_size_x + pixel_size_y) From d4067372426faf1adcd182a7121c3be0c5df0135 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 04:11:55 +0100 Subject: [PATCH 03/14] Moved slot number determination logic into a separate utility function that accepts a variable 'rotation_offset' parameter --- src/murfey/util/fib.py | 18 +++++++++++++ src/murfey/util/models.py | 54 +++++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/murfey/util/fib.py b/src/murfey/util/fib.py index fc0b0767f..6661bce18 100644 --- a/src/murfey/util/fib.py +++ b/src/murfey/util/fib.py @@ -2,8 +2,11 @@ General functinos specific to the FIB workflow """ +import math from pathlib import Path +from murfey.util.models import StagePositionValues + def number_from_name(name: str) -> int: """ @@ -27,3 +30,18 @@ def number_from_name(name: str) -> int: return int(stem[stem.rfind("(") + 1 : -1]) # Names without '()' or '#' should return 1 return 1 + + +def get_slot_number(stage_values: StagePositionValues, rotation_offset: float = -75): + if ( + stage_values.x is not None + and stage_values.y is not None + and stage_values.rotation is not None + ): + # Rotate the xy-coordinates to the -75 degrees frame + theta = math.radians(stage_values.rotation - rotation_offset) + sin = math.sin(theta) + cos = math.cos(theta) + x_rot = (stage_values.x * cos) - (stage_values.y * sin) + return 1 if x_rot < 0 else 2 + return None diff --git a/src/murfey/util/models.py b/src/murfey/util/models.py index 6c267cac6..714d9cfc7 100644 --- a/src/murfey/util/models.py +++ b/src/murfey/util/models.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional -from pydantic import BaseModel, computed_field, field_validator +from pydantic import BaseModel, field_validator """ ======================================================================================= @@ -109,32 +109,32 @@ class StagePositionValues(BaseModel): rotation: float | None = None tilt_alpha: float | None = None - @computed_field - def slot_number(self) -> int | None: - """ - In the Aquilos, -75 degrees is the stage rotation value when the sample grid is - facing the electron and ion beam guns, and the plane formed by the two guns is - normal to the surface of the sample grid. - - The xyz values recorded represent the stage position that brings the imaged - region into focus. At a rotation of -75 degrees, a negative x-value corresponds - to Slot 1, while a positive one corresponds to Slot 2. - - The xy values that keep a particular point on the grid at the euncentric focus - under the electron and ion beams will change based on the stage rotation. This - means that a transformation matrix will need to be applied to estimate the - x-value of the region when the stage rotation is at -75 degrees. The correct - slot number can then be determined from there. - """ - if self.x is not None and self.y is not None and self.rotation is not None: - # Rotate the xy-coordinates to the -75 degrees frame - theta = math.radians(self.rotation + 75) - sin = math.sin(theta) - cos = math.cos(theta) - x_rot = (self.x * cos) - (self.y * sin) - return 1 if x_rot < 0 else 2 - # return 1 if self.x < 0 else 2 - return None + # @computed_field + # def slot_number(self) -> int | None: + # """ + # In the Aquilos, -75 degrees is the stage rotation value when the sample grid is + # facing the electron and ion beam guns, and the plane formed by the two guns is + # normal to the surface of the sample grid. + + # The xyz values recorded represent the stage position that brings the imaged + # region into focus. At a rotation of -75 degrees, a negative x-value corresponds + # to Slot 1, while a positive one corresponds to Slot 2. + + # The xy values that keep a particular point on the grid at the euncentric focus + # under the electron and ion beams will change based on the stage rotation. This + # means that a transformation matrix will need to be applied to estimate the + # x-value of the region when the stage rotation is at -75 degrees. The correct + # slot number can then be determined from there. + # """ + # if self.x is not None and self.y is not None and self.rotation is not None: + # # Rotate the xy-coordinates to the -75 degrees frame + # theta = math.radians(self.rotation + 75) + # sin = math.sin(theta) + # cos = math.cos(theta) + # x_rot = (self.x * cos) - (self.y * sin) + # return 1 if x_rot < 0 else 2 + # # return 1 if self.x < 0 else 2 + # return None class StagePositionInfo(BaseModel): From 31e4681cbea4a8f874e0f7d2fbd3dc83b0869cd8 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 04:13:23 +0100 Subject: [PATCH 04/14] Updated FIB context and 'register_milling_progress' workflow so that the rotation offset parameter is extracted from the MachineConfig as a dict from the 'calibrations' field --- src/murfey/client/contexts/fib.py | 21 +++++++++++++----- .../fib/register_milling_progress.py | 22 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/murfey/client/contexts/fib.py b/src/murfey/client/contexts/fib.py index fa8ae9fb4..6cc2426d5 100644 --- a/src/murfey/client/contexts/fib.py +++ b/src/murfey/client/contexts/fib.py @@ -5,12 +5,12 @@ import xml.etree.ElementTree as ET from dataclasses import dataclass, field from pathlib import Path -from typing import Callable, Type, TypeVar +from typing import Callable, Type, TypeVar, cast from murfey.client.context import Context from murfey.client.instance_environment import MurfeyInstanceEnvironment from murfey.util.client import capture_post -from murfey.util.fib import number_from_name +from murfey.util.fib import get_slot_number, number_from_name from murfey.util.models import ( LamellaSiteInfo, MillingStepInfo, @@ -532,12 +532,21 @@ def _determine_output_dir( # Determine the slot number slot_number: int | None = None for stage_name in reversed(STAGE_POSITION_NAMES.keys()): - if (stage_info := getattr(site_info.stage_info, stage_name, None)) is None: - continue - if stage_info.slot_number is None: + stage_values: StagePositionValues | None = getattr( + site_info.stage_info, stage_name, None + ) + if stage_values is None: continue else: - slot_number = stage_info.slot_number + rotation_offset = cast( + float, + self._machine_config.get("calibrations", {}).get( + "rotation_offset", 0 + ), + ) + slot_number = get_slot_number( + stage_values, rotation_offset=rotation_offset + ) break # Early exit if no slot number if slot_number is None: diff --git a/src/murfey/workflows/fib/register_milling_progress.py b/src/murfey/workflows/fib/register_milling_progress.py index e3d794f69..2e0fa6bdc 100644 --- a/src/murfey/workflows/fib/register_milling_progress.py +++ b/src/murfey/workflows/fib/register_milling_progress.py @@ -3,12 +3,14 @@ import json import logging from importlib.metadata import entry_points -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from sqlmodel import Session as SQLModelSession, select import murfey.util.db as MurfeyDB from murfey.server import _transport_object +from murfey.util.config import get_machine_config +from murfey.util.fib import get_slot_number from murfey.util.models import ( GridSquareParameters, LamellaSiteInfo, @@ -344,10 +346,6 @@ def run(message: dict[str, Any], murfey_db: SQLModelSession): "Could not construct lookup tags; no stage position information found" ) return {"success": False, "requeue": False} - if latest_stage_position.slot_number is None: - logger.error("Could not construct lookup tags; 'slot_number' is missing") - return {"success": False, "requeue": False} - slot_number = latest_stage_position.slot_number # Milling step information if site_info.steps is None: @@ -364,6 +362,20 @@ def run(message: dict[str, Any], murfey_db: SQLModelSession): ).one() visit_name = murfey_session.visit instrument_name = murfey_session.instrument_name + + # Load the machine config + machine_config = get_machine_config(instrument_name)[instrument_name] + rotation_offset = cast( + float, machine_config.calibrations.get("rotation_offset", 0) + ) + + # Calculate the slot number + slot_number = get_slot_number(latest_stage_position, rotation_offset) + if slot_number is None: + logger.error( + "Could not construct lookup tags; 'slot_number' is missing" + ) + return {"success": False, "requeue": False} except Exception: logger.error( "Exception encountered while querying Murfey database", exc_info=True From e0bd468a2c07dcfb827ad92c79bfea3adf82b76d Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 04:21:48 +0100 Subject: [PATCH 05/14] Updated FIBContext tests --- tests/client/contexts/test_fib.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/client/contexts/test_fib.py b/tests/client/contexts/test_fib.py index af47f2b48..433e8e6d0 100644 --- a/tests/client/contexts/test_fib.py +++ b/tests/client/contexts/test_fib.py @@ -58,6 +58,11 @@ def visit_dir(tmp_path: Path): return tmp_path / visit_name +@pytest.fixture +def mock_machine_config(): + return {"calibrations": {"rotation_offset": -75}} + + def _create_stage_position_node(stage_values: dict[str, str]): stage_position_node = ET.Element("StagePosition") for key, value in stage_values.items(): @@ -480,6 +485,7 @@ def test_fib_full_autotem_context_projectdata( test_params: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool], tmp_path: Path, visit_dir: Path, + mock_machine_config: dict, ): # Unpack test params ( @@ -555,7 +561,7 @@ def test_fib_full_autotem_context_projectdata( context = FIBContext( acquisition_software="autotem", basepath=basepath, - machine_config={}, + machine_config=mock_machine_config, token="", ) if has_drift_correction_images: @@ -693,6 +699,7 @@ def test_fib_full_autotem_context_drift_correction_images( test_params: tuple[bool, bool, bool, bool, bool, bool, bool], tmp_path: Path, visit_dir: Path, + mock_machine_config: dict, fib_autotem_dc_images: list[Path], ): # Unpack test params @@ -740,7 +747,7 @@ def test_fib_full_autotem_context_drift_correction_images( context = FIBContext( acquisition_software="autotem", basepath=basepath, - machine_config={}, + machine_config=mock_machine_config, token="", ) @@ -804,12 +811,6 @@ def test_fib_full_autotem_context_drift_correction_images( for i in range(num_lamellae): lamella_num = i + 1 - # The '_site_info' attribute should now be populated - assert ( - context._site_info[lamella_num].stage_info.preparation_site.slot_number - == 2 - ) - # The output file should point to 'grid_2' for a positive x stage position output_file = ( tmp_path @@ -833,6 +834,7 @@ def test_fib_full_autotem_context_drift_correction_images( def test_fib_manual_autotem_context_projectdata( mocker: MockerFixture, visit_dir: Path, + mock_machine_config: dict, ): # Mock the ProjectData.dat file mock_projectdata = create_fib_autotem_project_data( @@ -856,7 +858,7 @@ def test_fib_manual_autotem_context_projectdata( context = FIBContext( acquisition_software="autotem", basepath=basepath, - machine_config={}, + machine_config=mock_machine_config, token="", ) @@ -870,6 +872,7 @@ def test_fib_maps_context( mocker: MockerFixture, tmp_path: Path, visit_dir: Path, + mock_machine_config: dict, fib_maps_images: list[Path], ): # Mock the environment @@ -895,7 +898,7 @@ def test_fib_maps_context( context = FIBContext( acquisition_software="maps", basepath=basepath, - machine_config={}, + machine_config=mock_machine_config, token="", ) From f06a942c4b761089ee12eaa0ac90bf6b812a276e Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 07:39:10 +0100 Subject: [PATCH 06/14] Pass values directly into 'get_slot_number' function instead of Pydantic model --- src/murfey/util/fib.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/murfey/util/fib.py b/src/murfey/util/fib.py index 6661bce18..9598741e6 100644 --- a/src/murfey/util/fib.py +++ b/src/murfey/util/fib.py @@ -5,8 +5,6 @@ import math from pathlib import Path -from murfey.util.models import StagePositionValues - def number_from_name(name: str) -> int: """ @@ -32,16 +30,17 @@ def number_from_name(name: str) -> int: return 1 -def get_slot_number(stage_values: StagePositionValues, rotation_offset: float = -75): - if ( - stage_values.x is not None - and stage_values.y is not None - and stage_values.rotation is not None - ): +def get_slot_number( + x: float | None = None, + y: float | None = None, + rotation: float | None = None, + rotation_offset: float = -75, +): + if x is not None and y is not None and rotation is not None: # Rotate the xy-coordinates to the -75 degrees frame - theta = math.radians(stage_values.rotation - rotation_offset) + theta = math.radians(rotation - rotation_offset) sin = math.sin(theta) cos = math.cos(theta) - x_rot = (stage_values.x * cos) - (stage_values.y * sin) + x_rot = (x * cos) - (y * sin) return 1 if x_rot < 0 else 2 return None From 5635666b9c3e1505fcf04738e6a484b32d2228bc Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 07:39:47 +0100 Subject: [PATCH 07/14] Forgot to remove commented out block --- src/murfey/util/models.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/murfey/util/models.py b/src/murfey/util/models.py index 714d9cfc7..339b089ad 100644 --- a/src/murfey/util/models.py +++ b/src/murfey/util/models.py @@ -109,33 +109,6 @@ class StagePositionValues(BaseModel): rotation: float | None = None tilt_alpha: float | None = None - # @computed_field - # def slot_number(self) -> int | None: - # """ - # In the Aquilos, -75 degrees is the stage rotation value when the sample grid is - # facing the electron and ion beam guns, and the plane formed by the two guns is - # normal to the surface of the sample grid. - - # The xyz values recorded represent the stage position that brings the imaged - # region into focus. At a rotation of -75 degrees, a negative x-value corresponds - # to Slot 1, while a positive one corresponds to Slot 2. - - # The xy values that keep a particular point on the grid at the euncentric focus - # under the electron and ion beams will change based on the stage rotation. This - # means that a transformation matrix will need to be applied to estimate the - # x-value of the region when the stage rotation is at -75 degrees. The correct - # slot number can then be determined from there. - # """ - # if self.x is not None and self.y is not None and self.rotation is not None: - # # Rotate the xy-coordinates to the -75 degrees frame - # theta = math.radians(self.rotation + 75) - # sin = math.sin(theta) - # cos = math.cos(theta) - # x_rot = (self.x * cos) - (self.y * sin) - # return 1 if x_rot < 0 else 2 - # # return 1 if self.x < 0 else 2 - # return None - class StagePositionInfo(BaseModel): """ From 9310e0fb1fa42c9f0170b1e2cb7f7c368acc7353 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 07:52:39 +0100 Subject: [PATCH 08/14] Pass stage position values to 'get_slot_number' directly in the FIBContext --- src/murfey/client/contexts/fib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/murfey/client/contexts/fib.py b/src/murfey/client/contexts/fib.py index 6cc2426d5..157c929d0 100644 --- a/src/murfey/client/contexts/fib.py +++ b/src/murfey/client/contexts/fib.py @@ -545,7 +545,10 @@ def _determine_output_dir( ), ) slot_number = get_slot_number( - stage_values, rotation_offset=rotation_offset + x=stage_values.x, + y=stage_values.y, + rotation=stage_values.rotation, + rotation_offset=rotation_offset, ) break # Early exit if no slot number From 7ef85044130d8a8fd23482dffaf812ff527d6f42 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 07:58:41 +0100 Subject: [PATCH 09/14] Updated 'register_atlas' workflow so that rotation offset information is fed in from the MachineConfig --- src/murfey/workflows/fib/register_atlas.py | 84 ++++++++++++---------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/src/murfey/workflows/fib/register_atlas.py b/src/murfey/workflows/fib/register_atlas.py index 03df5dc19..fdf59f648 100644 --- a/src/murfey/workflows/fib/register_atlas.py +++ b/src/murfey/workflows/fib/register_atlas.py @@ -1,10 +1,11 @@ import logging +import math import traceback import xml.etree.ElementTree as ET from functools import cached_property from importlib.metadata import entry_points from pathlib import Path -from typing import Any +from typing import Any, cast import numpy as np import PIL.Image @@ -12,7 +13,8 @@ from sqlmodel import Session, select import murfey.util.db as MurfeyDB -from murfey.util.fib import number_from_name +from murfey.util.config import get_machine_config +from murfey.util.fib import get_slot_number, number_from_name logger = logging.getLogger("murfey.workflows.fib.register_atlas") @@ -39,6 +41,7 @@ class FIBAtlasMetadata(BaseModel): pos_y: float pos_z: float rotation: float # Radians + slot_number: int tilt_alpha: float # Radians tilt_beta: float # Radians # Image dimensions @@ -66,17 +69,6 @@ def pixel_size(self) -> float: """ return 0.5 * (self.pixel_size_x + self.pixel_size_y) - # mypy doesn't support decorators on @property - @computed_field # type: ignore - @cached_property - def slot_number(self) -> int: - """ - Decide on a slot number for the site being inspected. From observation, - the x-position is entirely negative for one slot and entirely positive - for the other. - """ - return 1 if self.pos_x < 0 else 2 - # mypy doesn't support decorators on @property @computed_field # type: ignore @cached_property @@ -100,7 +92,7 @@ def site_name(self) -> str: return f"{self.project_name}--slot_{self.slot_number}" -def _parse_metadata(file: Path, visit_name: str): +def _parse_metadata(file: Path, visit_name: str, rotation_offset: float): """ Parses through the atlas image's tags to extract the relevant metadata """ @@ -127,31 +119,38 @@ def _parse_metadata(file: Path, visit_name: str): raise ValueError(f"Could not find required metadata in file {file}") # Extract key values from metadata + extracted: dict[str, Any] = { + key: node.text if (node := xml_metadata.find(node_path)) is not None else None + for key, node_path in ( + ("voltage", ".//Optics/AccelerationVoltage"), + ("shift_x", ".//Optics/BeamShift/X"), + ("shift_y", ".//Optics/BeamShift/Y"), + ("len_x", ".//Optics/ScanFieldOfView/X"), + ("len_y", ".//Optics/ScanFieldOfView/Y"), + ("pos_x", ".//StageSettings/StagePosition/X"), + ("pos_y", ".//StageSettings/StagePosition/Y"), + ("pos_z", ".//StageSettings/StagePosition/Z"), + ("rotation", ".//StageSettings/StagePosition/Rotation"), + ("tilt_alpha", ".//StageSettings/StagePosition/Tilt/Alpha"), + ("tilt_beta", ".//StageSettings/StagePosition/Tilt/Beta"), + ("pixels_x", ".//BinaryResult/ImageSize/X"), + ("pixels_y", ".//BinaryResult/ImageSize/Y"), + ("pixel_size_x", ".//BinaryResult/PixelSize/X"), + ("pixel_size_y", ".//BinaryResult/PixelSize/Y"), + ) + } + # Calculate the slot number + extracted["slot_number"] = get_slot_number( + x=float(extracted["pos_x"]), + y=float(extracted["pos_y"]), + rotation=math.degrees(float(extracted["rotation"])), + rotation_offset=rotation_offset, + ) + # Return the parsed Pydantic model return FIBAtlasMetadata( visit_name=visit_name, file=file, - **{ - key: node.text - if (node := xml_metadata.find(node_path)) is not None - else None - for key, node_path in ( - ("voltage", ".//Optics/AccelerationVoltage"), - ("shift_x", ".//Optics/BeamShift/X"), - ("shift_y", ".//Optics/BeamShift/Y"), - ("len_x", ".//Optics/ScanFieldOfView/X"), - ("len_y", ".//Optics/ScanFieldOfView/Y"), - ("pos_x", ".//StageSettings/StagePosition/X"), - ("pos_y", ".//StageSettings/StagePosition/Y"), - ("pos_z", ".//StageSettings/StagePosition/Z"), - ("rotation", ".//StageSettings/StagePosition/Rotation"), - ("tilt_alpha", ".//StageSettings/StagePosition/Tilt/Alpha"), - ("tilt_beta", ".//StageSettings/StagePosition/Tilt/Beta"), - ("pixels_x", ".//BinaryResult/ImageSize/X"), - ("pixels_y", ".//BinaryResult/ImageSize/Y"), - ("pixel_size_x", ".//BinaryResult/PixelSize/X"), - ("pixel_size_y", ".//BinaryResult/PixelSize/Y"), - ) - }, + **extracted, ) @@ -364,6 +363,7 @@ def run( ) ).one() visit_name = murfey_session.visit + instrument_name = murfey_session.instrument_name except Exception: logger.error( "Exception encountered while querying Murfey database", exc_info=True @@ -371,8 +371,18 @@ def run( return {"success": False, "requeue": False} try: + # Load the machine config + machine_config = get_machine_config(instrument_name)[instrument_name] + rotation_offset: float = cast( + float, machine_config.calibrations.get("rotation_offset", 0) + ) + # Extract metadata from Electron Snapshot image - metadata = _parse_metadata(fib_info.atlas_file, visit_name) + metadata = _parse_metadata( + fib_info.atlas_file, + visit_name=visit_name, + rotation_offset=rotation_offset, + ) except Exception: logger.error( f"Error extracting metadata from file {fib_info.atlas_file}", From 1e0fc4164c0598baafabe824f17230e748cebbb1 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 08:03:08 +0100 Subject: [PATCH 10/14] Updated tests --- tests/workflows/fib/test_register_atlas.py | 13 ++++++++++++ .../fib/test_register_milling_progress.py | 21 ++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/workflows/fib/test_register_atlas.py b/tests/workflows/fib/test_register_atlas.py index 158b7dc48..41fd8051f 100644 --- a/tests/workflows/fib/test_register_atlas.py +++ b/tests/workflows/fib/test_register_atlas.py @@ -322,6 +322,19 @@ def test_run_with_db( murfey_db_session.add(session_entry) murfey_db_session.commit() + # Mock the MachineConfig + mock_machine_config = MagicMock( + calibrations={ + "rotation_offset": -75, + } + ) + mocker.patch( + "murfey.workflows.fib.register_atlas.get_machine_config", + return_value={ + instrument_name: mock_machine_config, + }, + ) + # Mock the ISPyB connection where the TransportManager class is located mock_security_config = MagicMock() mock_security_config.ispyb_credentials = mock_ispyb_credentials diff --git a/tests/workflows/fib/test_register_milling_progress.py b/tests/workflows/fib/test_register_milling_progress.py index 606f5ba79..6cbe80c96 100644 --- a/tests/workflows/fib/test_register_milling_progress.py +++ b/tests/workflows/fib/test_register_milling_progress.py @@ -299,7 +299,6 @@ "z": 0.0323644854106331, "rotation": 285.003247202109, "tilt_alpha": 25.9996646026832, - "slot_number": 1, }, "chunk_site": { "x": -0.0030037500000000003, @@ -307,7 +306,6 @@ "z": 0.032350405092592606, "rotation": 285.003247202109, "tilt_alpha": -0.000134158926728586, - "slot_number": 1, }, "thinning_site": { "x": -0.0030037500000000003, @@ -315,7 +313,6 @@ "z": 0.032350405092592606, "rotation": 285.003247202109, "tilt_alpha": -0.000134158926728586, - "slot_number": 1, }, "chunk_coincidence_params": { "x": -0.0030048260286678298, @@ -323,7 +320,6 @@ "z": 0.0323400707790533, "rotation": 285.003247202109, "tilt_alpha": -0.000134158926728586, - "slot_number": 1, }, "thinning_params": { "x": -0.0030037500000000003, @@ -331,7 +327,6 @@ "z": 0.032350405092592606, "rotation": 285.003247202109, "tilt_alpha": -0.000134158926728586, - "slot_number": 1, }, } site_info = { @@ -369,9 +364,21 @@ def test_run_with_db( murfey_db_session.add(session_entry) murfey_db_session.commit() + # Mock the MachineConfig + mock_machine_config = MagicMock( + calibrations={ + "rotation_offset": -75, + } + ) + mocker.patch( + "murfey.workflows.fib.register_milling_progress.get_machine_config", + return_value={ + instrument_name: mock_machine_config, + }, + ) + # Mock the ISPyB connection where the TransportManager class is located - mock_security_config = MagicMock() - mock_security_config.ispyb_credentials = mock_ispyb_credentials + mock_security_config = MagicMock(ispyb_credentials=mock_ispyb_credentials) mocker.patch( "murfey.server.ispyb.get_security_config", return_value=mock_security_config, From aeaffd626ead94a2e4eb62b359fed2d697c9bd0b Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 08:19:59 +0100 Subject: [PATCH 11/14] Missed fixing an instance of 'get_slot_number' --- src/murfey/workflows/fib/register_milling_progress.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/murfey/workflows/fib/register_milling_progress.py b/src/murfey/workflows/fib/register_milling_progress.py index 2e0fa6bdc..84fb4d6ae 100644 --- a/src/murfey/workflows/fib/register_milling_progress.py +++ b/src/murfey/workflows/fib/register_milling_progress.py @@ -370,7 +370,12 @@ def run(message: dict[str, Any], murfey_db: SQLModelSession): ) # Calculate the slot number - slot_number = get_slot_number(latest_stage_position, rotation_offset) + slot_number = get_slot_number( + x=latest_stage_position.x, + y=latest_stage_position.y, + rotation=latest_stage_position.rotation, + rotation_offset=rotation_offset, + ) if slot_number is None: logger.error( "Could not construct lookup tags; 'slot_number' is missing" From 8791a54eef0fa89ebb36fd4959318a5d84127703 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 08:50:33 +0100 Subject: [PATCH 12/14] Fixed broken test --- tests/workflows/fib/test_register_atlas.py | 52 ++++++++++++++-------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/tests/workflows/fib/test_register_atlas.py b/tests/workflows/fib/test_register_atlas.py index 41fd8051f..f94821e06 100644 --- a/tests/workflows/fib/test_register_atlas.py +++ b/tests/workflows/fib/test_register_atlas.py @@ -12,6 +12,7 @@ from sqlmodel import Session as SQLModelSession, select as sm_select import murfey.util.db as MurfeyDB +from murfey.util.fib import get_slot_number from murfey.workflows.fib.register_atlas import FIBAtlasMetadata, _parse_metadata, run from tests.conftest import ExampleVisit @@ -152,6 +153,7 @@ def create_electron_snapshot_metadata( 0.0003, # Y 0.01, # Z -1.309, # Rotation + -75, # Rotation offset 0.8, # Alpha tilt 0, # Beta tilt 2, # Expected slot number @@ -173,9 +175,10 @@ def create_electron_snapshot_metadata( 0.0003, # Y 0.01, # Z 1.833, # Rotation + -75, # Rotation offset 0, # Alpha tilt 0, # Beta tilt - 1, # Expected slot number + 2, # Expected slot number 3072, # Image size X 2048, # Y 1e-6, # Pixel size X @@ -200,6 +203,7 @@ def test_parse_metadata( float, float, float, + float, int, int, int, @@ -222,6 +226,7 @@ def test_parse_metadata( pos_y, pos_z, rotation, + rotation_offset, tilt_alpha, tilt_beta, expected_slot_number, @@ -267,7 +272,7 @@ def test_parse_metadata( ) # Run the function and check that output is correct - parsed = _parse_metadata(file, visit_name) + parsed = _parse_metadata(file, visit_name, rotation_offset) assert parsed.visit_name == visit_name assert parsed.file == file @@ -302,6 +307,7 @@ def test_run_with_db( ispyb_db_session: SQLAlchemySession, mock_ispyb_credentials, ): + rotation_offset = -75 test_files = ( visit_dir / "maps/LayersData/Layer/Electron Snapshot/Electron Snapshot.tiff", visit_dir @@ -325,7 +331,7 @@ def test_run_with_db( # Mock the MachineConfig mock_machine_config = MagicMock( calibrations={ - "rotation_offset": -75, + "rotation_offset": rotation_offset, } ) mocker.patch( @@ -368,25 +374,35 @@ def test_run_with_db( # Mock the metadata returned from the image file import murfey.workflows.fib.register_atlas + for test_file in test_files: + extracted = { + "voltage": 2000, + "shift_x": 0, + "shift_y": 0, + "len_x": 0.003072, + "len_y": 0.002048, + "pos_x": 0.003, + "pos_y": 0.0003, + "pos_z": 0.01, + "rotation": -1.309, + "tilt_alpha": 0.8, + "tilt_beta": 0, + "pixels_x": 3072, + "pixels_y": 2048, + "pixel_size_x": 1e-6, + "pixel_size_y": 1e-6, + } + extracted["slot_number"] = get_slot_number( + x=extracted["pos_x"], + y=extracted["pos_y"], + rotation=extracted["rotation"], + rotation_offset=rotation_offset, + ) mock_metadata = [ FIBAtlasMetadata( visit_name=visit_name, file=test_file, - voltage=2000, - shift_x=0, - shift_y=0, - len_x=0.003072, - len_y=0.002048, - pos_x=0.003, - pos_y=0.0003, - pos_z=0.01, - rotation=-1.309, - tilt_alpha=0.8, - tilt_beta=0, - pixels_x=3072, - pixels_y=2048, - pixel_size_x=1e-6, - pixel_size_y=1e-6, + **extracted, ) for test_file in test_files ] From ab0f5a1564c31e81565a202f04c664918b006777 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 15:25:35 +0100 Subject: [PATCH 13/14] Added comments about function purpose and angle units --- src/murfey/util/fib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/murfey/util/fib.py b/src/murfey/util/fib.py index 9598741e6..48f4be53c 100644 --- a/src/murfey/util/fib.py +++ b/src/murfey/util/fib.py @@ -33,11 +33,19 @@ def number_from_name(name: str) -> int: def get_slot_number( x: float | None = None, y: float | None = None, + # Angles are in degrees rotation: float | None = None, rotation_offset: float = -75, ): + """ + In the Aquilos, the stage position values corresponding to slots 1 and 2 are + taken at a fixed stage rotation; at different stage rotation values, the x- + and y- ranges corresponding to slots 1 and 2 will change. This function will + rotate the provided stage values into the correct reference frame and return + the slot number. + """ if x is not None and y is not None and rotation is not None: - # Rotate the xy-coordinates to the -75 degrees frame + # Rotate the xy-coordinates to reference frame theta = math.radians(rotation - rotation_offset) sin = math.sin(theta) cos = math.cos(theta) From 6829465241edd0c6eb8892e15fb2c3bb54b3bb0b Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Fri, 24 Jul 2026 15:29:56 +0100 Subject: [PATCH 14/14] Added comments about angle units --- src/murfey/workflows/fib/register_atlas.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/murfey/workflows/fib/register_atlas.py b/src/murfey/workflows/fib/register_atlas.py index fdf59f648..37fec21d6 100644 --- a/src/murfey/workflows/fib/register_atlas.py +++ b/src/murfey/workflows/fib/register_atlas.py @@ -130,6 +130,7 @@ def _parse_metadata(file: Path, visit_name: str, rotation_offset: float): ("pos_x", ".//StageSettings/StagePosition/X"), ("pos_y", ".//StageSettings/StagePosition/Y"), ("pos_z", ".//StageSettings/StagePosition/Z"), + # Angles are in radians ("rotation", ".//StageSettings/StagePosition/Rotation"), ("tilt_alpha", ".//StageSettings/StagePosition/Tilt/Alpha"), ("tilt_beta", ".//StageSettings/StagePosition/Tilt/Beta"), @@ -143,7 +144,7 @@ def _parse_metadata(file: Path, visit_name: str, rotation_offset: float): extracted["slot_number"] = get_slot_number( x=float(extracted["pos_x"]), y=float(extracted["pos_y"]), - rotation=math.degrees(float(extracted["rotation"])), + rotation=math.degrees(float(extracted["rotation"])), # Convert to degrees rotation_offset=rotation_offset, ) # Return the parsed Pydantic model