diff --git a/pyproject.toml b/pyproject.toml index fe7a69b8b..506cf8358 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,6 +127,7 @@ TomographyMetadataContext = "murfey.client.contexts.tomo_metadata:TomographyMeta "spa.flush_spa_preprocess" = "murfey.workflows.spa.flush_spa_preprocess:flush_spa_preprocess" "spa.motion_corrected" = "murfey.workflows.spa.motion_correction:motion_corrected" "sxt.process_tilt_series" = "murfey.workflows.sxt.process_sxt_tilt_series:run" +"sxt.register_roi" = "murfey.workflows.sxt.sxt_metadata:run" [tool.setuptools] package-dir = {"" = "src"} diff --git a/src/murfey/client/contexts/sxt.py b/src/murfey/client/contexts/sxt.py index da5732b44..31f1cc4b4 100644 --- a/src/murfey/client/contexts/sxt.py +++ b/src/murfey/client/contexts/sxt.py @@ -246,7 +246,7 @@ def post_transfer( / environment.visit ) destination_extra = "" - converted_file_path = ( + converted_tiff_path = ( Path(self._machine_config.get("rsync_basepath", "")) / destination_base / self._machine_config.get("processed_directory_name", "") @@ -254,6 +254,9 @@ def post_transfer( / destination_extra / f"{transferred_file.relative_to(source).stem}_Annotated.tiff" ) + thumbnail_path = converted_tiff_path.parent / ( + converted_tiff_path.stem + "_thumbnail.jpg" + ) capture_post( base_url=str(environment.url.geturl()), router_name="workflow_sxt.router", @@ -262,7 +265,7 @@ def post_transfer( instrument_name=environment.instrument_name, data={ "xrm_path": str(image_path), - "tiff_path": str(converted_file_path), + "tiff_path": str(converted_tiff_path), }, ) @@ -274,7 +277,7 @@ def post_transfer( dcg_data = { "experiment_type_id": 44, # Atlas "tag": dcg_tag, - "atlas": str(converted_file_path), + "atlas": str(thumbnail_path), "atlas_pixel_size": round(metadata.get("pixel_size", 0), 2), "atlas_x_stage_position": metadata.get("x_position", None), "atlas_y_stage_position": metadata.get("y_position", None), @@ -303,11 +306,10 @@ def post_transfer( function_name="register_sxt_roi", token=self._token, instrument_name=environment.instrument_name, - visit_name=environment.visit, session_id=environment.murfey_session, + roi_name=transferred_file.stem, data={ "tag": dcg_tag, - "name": transferred_file.stem, "x_stage_position": metadata.get("x_position", None), "y_stage_position": metadata.get("y_position", None), "pixel_size": round(metadata.get("pixel_size", 0), 2), @@ -317,7 +319,7 @@ def post_transfer( "width": int( metadata.get("width", 0) * metadata["mosaic_columns"] ), - "image": str(converted_file_path), + "image": str(thumbnail_path), }, ) diff --git a/src/murfey/server/api/workflow.py b/src/murfey/server/api/workflow.py index b228dbda0..a91dcd3ee 100644 --- a/src/murfey/server/api/workflow.py +++ b/src/murfey/server/api/workflow.py @@ -57,6 +57,7 @@ DataCollectionGroup, FoilHole, GridSquare, + ImagingSite, Movie, PreprocessStash, ProcessingJob, @@ -121,15 +122,15 @@ def register_dc_group( db.exec(select(Session).where(Session.id == session_id)).one().instrument_name ) logger.info(f"Registering data collection group on microscope {instrument_name}") + machine_config = get_machine_config(instrument_name=instrument_name)[ + instrument_name + ] smartem_grid_uuid = None if ( dcg_params.create_smartem_grid and SMARTEM_ACTIVE and dcg_params.acquisition_uuid ): - machine_config = get_machine_config(instrument_name=instrument_name)[ - instrument_name - ] if machine_config.smartem_api_url: try: smartem_client = SmartEMAPIClient( @@ -178,6 +179,40 @@ def register_dc_group( if smartem_grid_uuid: dcg_instance.smartem_grid_uuid = smartem_grid_uuid + # Update any atlases which are registered as imaging sites + if atlas_instance := db.exec( + select(ImagingSite).where(ImagingSite.dcg_id == dcg_instance.id) + ).one_or_none(): + atlas_instance.pos_x = ( + dcg_params.atlas_x_stage_position or atlas_instance.pos_x + ) + atlas_instance.pos_y = ( + dcg_params.atlas_y_stage_position or atlas_instance.pos_y + ) + atlas_instance.image_pixels_x = ( + dcg_params.atlas_width or atlas_instance.image_pixels_x + ) + atlas_instance.image_pixels_y = ( + dcg_params.atlas_height or atlas_instance.image_pixels_y + ) + atlas_instance.image_pixel_size = ( + dcg_params.atlas_pixel_size or atlas_instance.image_pixel_size + ) + db.add(atlas_instance) + elif dcg_params.atlas_x_stage_position: + atlas_instance = ImagingSite( + dcg_id=dcg_instance.id, + session_id=session_id, + site_name=dcg_instance.tag, + data_type="atlas", + pos_x=dcg_params.atlas_x_stage_position, + pos_y=dcg_params.atlas_y_stage_position, + image_pixels_x=dcg_params.atlas_width, + image_pixels_y=dcg_params.atlas_height, + image_pixel_size=dcg_params.atlas_pixel_size, + ) + db.add(atlas_instance) + if _transport_object: if dcg_instance.atlas_id is not None: _transport_object.send( @@ -213,9 +248,21 @@ def register_dc_group( ).all() search_map_params = SearchMapParameters(tag=dcg_params.tag) for sm in search_maps: - register_search_map_in_database( - session_id, sm.name, search_map_params, db, close_db=False - ) + # Different behaviour for tomo and SXT + if "tomo" in machine_config.acquisition_software: + register_search_map_in_database( + session_id, sm.name, search_map_params, db, close_db=False + ) + elif _transport_object: + _transport_object.send( + _transport_object.feedback_queue, + { + "register": "sxt.register_roi", + "session_id": session_id, + "roi_name": sm.name, + "roi_info": search_map_params.model_dump(mode="json"), + }, + ) db.close() elif dcg_murfey := db.exec( select(DataCollectionGroup) @@ -255,6 +302,10 @@ def register_dc_group( "atlas": dcg_params.atlas, "sample": dcg_params.sample, "atlas_pixel_size": dcg_params.atlas_pixel_size, + "atlas_x_stage_position": dcg_params.atlas_x_stage_position, + "atlas_y_stage_position": dcg_params.atlas_y_stage_position, + "atlas_width": dcg_params.atlas_width, + "atlas_height": dcg_params.atlas_height, } if _transport_object: diff --git a/src/murfey/server/api/workflow_sxt.py b/src/murfey/server/api/workflow_sxt.py index 61059f9b9..6f5e0a9e3 100644 --- a/src/murfey/server/api/workflow_sxt.py +++ b/src/murfey/server/api/workflow_sxt.py @@ -10,6 +10,8 @@ validate_instrument_token, ) from murfey.server.murfey_db import murfey_db +from murfey.util import sanitise +from murfey.util.models import SearchMapParameters from murfey.workflows.sxt.process_sxt_tilt_series import SXTTiltSeriesInfo logger = getLogger("murfey.server.api.workflow_sxt") @@ -62,23 +64,24 @@ def convert_xrm_to_tiff(xrm_file: XrmFile, db=murfey_db): ) -class SxtRoiInfo(BaseModel): - tag: str - name: str - x_stage_position: float - y_stage_position: float - pixel_size: float - height: int - width: int - image: Path - - -@router.post("/visits/{visit_name}/sessions/{session_id}/register_sxt_roi") +@router.post("/sessions/{session_id}/sxt_roi/{roi_name}") def register_sxt_roi( - visit_name: str, session_id: MurfeySessionID, - tilt_series_info: SXTTiltSeriesInfo, + roi_name: str, + roi_info: SearchMapParameters, db=murfey_db, ): - # TODO - return + if _transport_object: + logger.info(f"Registering SXT region {sanitise(roi_name)}") + _transport_object.send( + _transport_object.feedback_queue, + { + "register": "sxt.register_roi", + "session_id": session_id, + "roi_name": roi_name, + "roi_info": roi_info.model_dump(mode="json"), + }, + new_connection=True, + ) + else: + logger.warning("No transport object for register_sxt_roi") diff --git a/src/murfey/server/ispyb.py b/src/murfey/server/ispyb.py index 75525dd0c..eead79075 100644 --- a/src/murfey/server/ispyb.py +++ b/src/murfey/server/ispyb.py @@ -543,6 +543,96 @@ def do_update_search_map( ) return {"success": False, "return_value": None} + def do_insert_sxt_roi( + self, + atlas_id: int, + roi_parameters: SearchMapParameters, + ): + if ( + roi_parameters.pixel_size + and roi_parameters.height + and roi_parameters.height_on_atlas + ): + roi_parameters.pixel_size *= ( + roi_parameters.height / roi_parameters.height_on_atlas + ) + roi_parameters.x_location = ( + int(roi_parameters.x_location) if roi_parameters.x_location else None + ) + roi_parameters.y_location = ( + int(roi_parameters.y_location) if roi_parameters.y_location else None + ) + record = GridSquare( + atlasId=atlas_id, + gridSquareImage=roi_parameters.image, + pixelLocationX=roi_parameters.x_location, + pixelLocationY=roi_parameters.y_location, + height=roi_parameters.height_on_atlas, + width=roi_parameters.width_on_atlas, + angle=0, + stageLocationX=roi_parameters.x_stage_position, + stageLocationY=roi_parameters.y_stage_position, + pixelSize=roi_parameters.pixel_size, + ) + try: + with ISPyBSession() as db: + db.add(record) + db.commit() + log.info(f"Created SXT ROI (GridSquare) {record.gridSquareId}") + return {"success": True, "return_value": record.gridSquareId} + except ispyb.ISPyBException as e: + log.error( + "Inserting SXT ROI (GridSquare) entry caused exception '%s'.", + e, + exc_info=True, + ) + return {"success": False, "return_value": None} + + def do_update_sxt_roi(self, roi_id, roi_parameters: SearchMapParameters): + try: + with ISPyBSession() as db: + grid_square = ( + db.query(GridSquare).filter(GridSquare.gridSquareId == roi_id).one() + ) + if ( + roi_parameters.pixel_size + and roi_parameters.height + and roi_parameters.height_on_atlas + ): + roi_parameters.pixel_size *= ( + roi_parameters.height / roi_parameters.height_on_atlas + ) + grid_square.gridSquareImage = ( + roi_parameters.image or grid_square.gridSquareImage + ) + if roi_parameters.x_location: + grid_square.pixelLocationX = int(roi_parameters.x_location) + if roi_parameters.y_location: + grid_square.pixelLocationY = int(roi_parameters.y_location) + if roi_parameters.height_on_atlas: + grid_square.height = int(roi_parameters.height_on_atlas) + if roi_parameters.width_on_atlas: + grid_square.width = int(roi_parameters.width_on_atlas) + grid_square.stageLocationX = ( + roi_parameters.x_stage_position or grid_square.stageLocationX + ) + grid_square.stageLocationY = ( + roi_parameters.y_stage_position or grid_square.stageLocationY + ) + grid_square.pixelSize = ( + roi_parameters.pixel_size or grid_square.pixelSize + ) + db.add(grid_square) + db.commit() + return {"success": True, "return_value": grid_square.gridSquareId} + except ispyb.ISPyBException as e: + log.error( + "Updating SXT ROI (GridSquare) entry caused exception '%s'.", + e, + exc_info=True, + ) + return {"success": False, "return_value": None} + def send(self, queue: str, message: dict, new_connection: bool = False): if self.transport: if not self.transport.is_connected(): diff --git a/src/murfey/util/route_manifest.yaml b/src/murfey/util/route_manifest.yaml index 01527f335..e80e1237b 100644 --- a/src/murfey/util/route_manifest.yaml +++ b/src/murfey/util/route_manifest.yaml @@ -1460,10 +1460,10 @@ murfey.server.api.workflow_sxt.router: path_params: [] methods: - POST - - path: /workflow/sxt/visits/{visit_name}/sessions/{session_id}/register_sxt_roi + - path: /workflow/sxt/sessions/{session_id}/sxt_roi/{roi_name} function: register_sxt_roi path_params: - - name: visit_name + - name: roi_name type: str - name: session_id type: int diff --git a/src/murfey/workflows/register_data_collection_group.py b/src/murfey/workflows/register_data_collection_group.py index 0908e769b..148d9dbaa 100644 --- a/src/murfey/workflows/register_data_collection_group.py +++ b/src/murfey/workflows/register_data_collection_group.py @@ -9,7 +9,7 @@ from murfey.server import _transport_object from murfey.server.ispyb import ISPyBSession, get_session_id -from murfey.util.db import DataCollectionGroup +from murfey.util.db import DataCollectionGroup, ImagingSite logger = logging.getLogger("murfey.workflows.register_data_collection_group") @@ -90,6 +90,19 @@ def run(message: dict, murfey_db: SQLModelSession) -> dict[str, bool]: smartem_grid_uuid=message.get("smartem_grid_uuid"), ) murfey_db.add(murfey_dcg) + if dcgid is not None and message.get("atlas_x_stage_position"): + atlas_site = ImagingSite( + dcg_id=dcgid, + session_id=message["session_id"], + site_name=message.get("tag"), + data_type="atlas", + pos_x=message.get("atlas_x_stage_position"), + pos_y=message.get("atlas_y_stage_position"), + image_pixels_x=message.get("atlas_width"), + image_pixels_y=message.get("atlas_height"), + image_pixel_size=message.get("atlas_pixel_size"), + ) + murfey_db.add(atlas_site) murfey_db.commit() murfey_db.close() diff --git a/src/murfey/workflows/sxt/sxt_metadata.py b/src/murfey/workflows/sxt/sxt_metadata.py new file mode 100644 index 000000000..642257fc5 --- /dev/null +++ b/src/murfey/workflows/sxt/sxt_metadata.py @@ -0,0 +1,127 @@ +import logging + +from sqlmodel import select +from sqlmodel.orm.session import Session as SQLModelSession + +from murfey.server import _transport_object +from murfey.util import sanitise +from murfey.util.db import DataCollectionGroup, ImagingSite, SearchMap +from murfey.util.models import SearchMapParameters + +logger = logging.getLogger("murfey.workflows.sxt.sxt_metadata") + + +def register_sxt_roi( + session_id: int, + roi_name: str, + roi_parameters: SearchMapParameters, + murfey_db: SQLModelSession, +) -> dict[str, bool]: + dcg = murfey_db.exec( + select(DataCollectionGroup) + .where(DataCollectionGroup.session_id == session_id) + .where(DataCollectionGroup.tag == roi_parameters.tag) + ).one() + roi = murfey_db.exec( + select(SearchMap) + .where(SearchMap.name == roi_name) + .where(SearchMap.tag == roi_parameters.tag) + .where(SearchMap.session_id == session_id) + ).one_or_none() + if roi: + # See if there is already a search map with this name and update if so + roi.x_stage_position = roi_parameters.x_stage_position or roi.x_stage_position + roi.y_stage_position = roi_parameters.y_stage_position or roi.y_stage_position + roi.height = roi_parameters.height or roi.height + roi.width = roi_parameters.width or roi.width + roi.pixel_size = roi_parameters.pixel_size or roi.pixel_size + roi.image = roi_parameters.image or roi.image + if _transport_object: + _transport_object.do_update_sxt_roi(roi.id, roi_parameters) + else: + logger.info(f"Registering new sxt roi {sanitise(roi_name)}") + if _transport_object: + roi_ispyb_response = _transport_object.do_insert_sxt_roi( + dcg.atlas_id, roi_parameters + ) + else: + # mock up response so that below still works + roi_ispyb_response = {"success": False, "return_value": None} + # Register new search map + roi = SearchMap( + id=( + roi_ispyb_response["return_value"] + if roi_ispyb_response["success"] + else None + ), + name=roi_name, + session_id=session_id, + tag=roi_parameters.tag, + x_stage_position=roi_parameters.x_stage_position, + y_stage_position=roi_parameters.y_stage_position, + pixel_size=roi_parameters.pixel_size, + width=roi_parameters.width, + height=roi_parameters.height, + image=roi_parameters.image or "", + ) + + atlas = murfey_db.exec( + select(ImagingSite).where(ImagingSite.dcg_id == dcg.id) + ).one_or_none() + if atlas and all( + [ + roi.x_stage_position, + roi.y_stage_position, + roi.pixel_size, + atlas.pos_x, + atlas.pos_y, + atlas.image_pixel_size, + atlas.image_pixels_x, + atlas.image_pixels_y, + ] + ): + # Convert from stage position to pixel locations + roi.x_location = (roi.x_stage_position - atlas.pos_x) / atlas.image_pixel_size + roi.y_location = (roi.y_stage_position - atlas.pos_y) / atlas.image_pixel_size + + # Scaling from different pixel size of atlas and roi, and atlas thumbnail size + roi_parameters.x_location = roi.x_location * (512 / atlas.image_pixels_x) + 256 + roi_parameters.y_location = 256 - roi.y_location * (512 / atlas.image_pixels_y) + roi_parameters.width_on_atlas = int( + round( + roi.width + * (roi.pixel_size / atlas.image_pixel_size) + * (512 / atlas.image_pixels_x) + ) + ) + roi_parameters.height_on_atlas = int( + round( + roi.height + * (roi.pixel_size / atlas.image_pixel_size) + * (512 / atlas.image_pixels_y) + ) + ) + if _transport_object: + _transport_object.do_update_sxt_roi(roi.id, roi_parameters) + else: + logger.warning("No transport object for register_sxt_roi") + else: + logger.info( + f"Unable to register roi {sanitise(roi.name)} position yet: " + f"roi pixel size {sanitise(str(roi.pixel_size))}, " + f"atlas pixel size {sanitise(str(dcg.atlas_pixel_size))}, " + f"roi pixel size {sanitise(str(atlas.image_pixel_size))}" + ) + murfey_db.add(roi) + murfey_db.commit() + murfey_db.close() + return {"success": True} + + +def run(message: dict, murfey_db: SQLModelSession) -> dict[str, bool]: + return register_sxt_roi( + message["session_id"], + message["roi_name"], + SearchMapParameters(**message["roi_info"]), + murfey_db, + ) diff --git a/tests/client/contexts/test_sxt.py b/tests/client/contexts/test_sxt.py index bdbe6876d..e0beba0b8 100644 --- a/tests/client/contexts/test_sxt.py +++ b/tests/client/contexts/test_sxt.py @@ -82,7 +82,7 @@ def test_sxt_context_xrm_atlas(mock_ole_file, mock_post, tmp_path): json={ "experiment_type_id": 44, "tag": f"{tmp_path}/cm12345-6/grid1", - "atlas": "/path/to/dest/cm12345-6/processed/grid1/example_atlas_Annotated.tiff", + "atlas": "/path/to/dest/cm12345-6/processed/grid1/example_atlas_Annotated_thumbnail.jpg", "atlas_pixel_size": 0.3, "atlas_x_stage_position": 1, "atlas_y_stage_position": -1, @@ -158,16 +158,15 @@ def test_sxt_context_xrm_roi(mock_ole_file, mock_post, tmp_path): headers={"Authorization": "Bearer "}, ) mock_post.assert_any_call( - "http://localhost:8000/workflow/sxt/visits/cm12345-6/sessions/1/register_sxt_roi", + "http://localhost:8000/workflow/sxt/sessions/1/sxt_roi/example_roi", json={ "tag": f"{tmp_path}/cm12345-6/grid1", - "name": "example_roi", "x_stage_position": 1, "y_stage_position": -1, "pixel_size": 0.03, "height": 6000, "width": 4500, - "image": "/path/to/dest/cm12345-6/processed/grid1/example_roi_Annotated.tiff", + "image": "/path/to/dest/cm12345-6/processed/grid1/example_roi_Annotated_thumbnail.jpg", }, headers={"Authorization": "Bearer "}, ) diff --git a/tests/server/api/test_workflow.py b/tests/server/api/test_workflow.py index a1ec8336c..78219d5d4 100644 --- a/tests/server/api/test_workflow.py +++ b/tests/server/api/test_workflow.py @@ -6,7 +6,9 @@ DCGroupParameters, register_dc_group, ) -from murfey.util.db import DataCollectionGroup, SearchMap +from murfey.util.config import MachineConfig +from murfey.util.db import DataCollectionGroup, ImagingSite, SearchMap +from murfey.util.models import SearchMapParameters from tests.conftest import ExampleVisit @@ -42,6 +44,10 @@ def test_register_dc_group_new_dcg(mock_transport, murfey_db_session: Session): "atlas": "/path/to/Sample10/Atlas/Atlas_1.jpg", "sample": 10, "atlas_pixel_size": 1e-5, + "atlas_x_stage_position": None, + "atlas_y_stage_position": None, + "atlas_width": None, + "atlas_height": None, "microscope": "", "proposal_code": ExampleVisit.proposal_code, "proposal_number": str(ExampleVisit.proposal_number), @@ -263,6 +269,10 @@ def test_register_dc_group_new_dcg_old_atlas( "atlas": "/path/to/Sample10/Atlas/Atlas_1.jpg", "sample": 10, "atlas_pixel_size": 1e-5, + "atlas_x_stage_position": None, + "atlas_y_stage_position": None, + "atlas_width": None, + "atlas_height": None, "microscope": "", "proposal_code": ExampleVisit.proposal_code, "proposal_number": str(ExampleVisit.proposal_number), @@ -328,14 +338,21 @@ def test_register_dc_group_new_atlas(mock_transport, murfey_db_session: Session) @mock.patch("murfey.server.api.workflow._transport_object") @mock.patch("murfey.server.api.workflow.register_search_map_in_database") +@mock.patch("murfey.server.api.workflow.get_machine_config") def test_register_dc_group_new_atlas_with_searchmaps( - mock_register_search_map, mock_transport, murfey_db_session: Session + mock_machine_config, + mock_register_search_map, + mock_transport, + murfey_db_session: Session, ): """ Test the request to update an existing data collection group by adding an atlas, using the same tag, and also update search maps """ mock_transport.feedback_queue = "mock_feedback_queue" + mock_machine_config.return_value = { + "": MachineConfig(acquisition_software=["tomo"]) + } # Make sure dcg is present with an atlas id dcg = DataCollectionGroup( @@ -430,3 +447,224 @@ def test_register_dc_group_new_atlas_with_searchmaps( murfey_db_session, close_db=False, ) + + +@mock.patch("murfey.server.api.workflow._transport_object") +@mock.patch("murfey.server.api.workflow.get_machine_config") +def test_register_dc_group_new_atlas_with_sxt_roi( + mock_machine_config, + mock_transport, + murfey_db_session: Session, +): + """ + Test the request to update an existing data collection group + by adding an atlas, using the same tag, and also update sxt rois + """ + mock_transport.feedback_queue = "mock_feedback_queue" + mock_machine_config.return_value = {"": MachineConfig(acquisition_software=["sxt"])} + + # Make sure dcg is present with an atlas id + dcg = DataCollectionGroup( + id=1, + session_id=ExampleVisit.murfey_session_id, + tag="processing_tag", + atlas_id=90, + atlas_pixel_size=1e-5, + sample=10, + atlas="/path/to/Sample10/Atlas/Atlas_1.jpg", + ) + murfey_db_session.add(dcg) + murfey_db_session.commit() + + # Add some search maps with the dcg tag and one with a different tag + sm1 = SearchMap( + id=1, + session_id=ExampleVisit.murfey_session_id, + tag="processing_tag", + name="searchmap1", + ) + sm2 = SearchMap( + id=2, + session_id=ExampleVisit.murfey_session_id, + tag="processing_tag", + name="searchmap2", + ) + sm3 = SearchMap( + id=3, + session_id=ExampleVisit.murfey_session_id, + tag="different_tag", + name="searchmap3", + ) + murfey_db_session.add(sm1) + murfey_db_session.add(sm2) + murfey_db_session.add(sm3) + murfey_db_session.commit() + + # Request new dcg registration with new atlas tag and sample + dcg_params = DCGroupParameters( + experiment_type_id=37, + tag="processing_tag", + atlas="/path/to/Sample12/Atlas/Atlas_2.jpg", + sample=12, + atlas_pixel_size=1e-4, + atlas_x_stage_position=10, + atlas_y_stage_position=20, + atlas_width=200, + atlas_height=300, + ) + register_dc_group( + visit_name="cm12345-6", + session_id=ExampleVisit.murfey_session_id, + dcg_params=dcg_params, + db=murfey_db_session, + ) + + # Check request to ispyb for updating the experiment type + mock_transport.send.assert_any_call( + "mock_feedback_queue", + { + "register": "atlas_update", + "atlas_id": 90, + "atlas": "/path/to/Sample12/Atlas/Atlas_2.jpg", + "sample": 12, + "atlas_pixel_size": 1e-4, + "dcgid": 1, + "session_id": ExampleVisit.murfey_session_id, + "tag": "processing_tag", + }, + ) + + # Check the data collection group atlas was updated + new_dcg = murfey_db_session.exec( + select(DataCollectionGroup).where(DataCollectionGroup.id == dcg.id) + ).one() + assert new_dcg.atlas == "/path/to/Sample12/Atlas/Atlas_2.jpg" + assert new_dcg.sample == 12 + assert new_dcg.atlas_pixel_size == 1e-4 + assert new_dcg.tag == "processing_tag" + assert new_dcg.atlas_id == 90 + + # Check the imaging site insert + image_site = murfey_db_session.exec( + select(ImagingSite).where(ImagingSite.dcg_id == dcg.id) + ).one() + assert image_site.site_name == "processing_tag" + assert image_site.pos_x == 10 + assert image_site.image_pixels_x == 200 + assert image_site.image_pixel_size == 1e-4 + + # Check search map row updates + assert mock_transport.send.call_count == 3 + mock_transport.send.assert_any_call( + "mock_feedback_queue", + { + "register": "sxt.register_roi", + "session_id": ExampleVisit.murfey_session_id, + "roi_name": "searchmap1", + "roi_info": SearchMapParameters(tag="processing_tag").model_dump( + mode="json" + ), + }, + ) + mock_transport.send.assert_any_call( + "mock_feedback_queue", + { + "register": "sxt.register_roi", + "session_id": ExampleVisit.murfey_session_id, + "roi_name": "searchmap2", + "roi_info": SearchMapParameters(tag="processing_tag").model_dump( + mode="json" + ), + }, + ) + + +@mock.patch("murfey.server.api.workflow._transport_object") +@mock.patch("murfey.server.api.workflow.get_machine_config") +def test_register_dc_group_roi_update( + mock_machine_config, + mock_transport, + murfey_db_session: Session, +): + """ + Test the request to update an existing data collection group + by adding an atlas, using the same tag, and also update sxt rois + """ + mock_transport.feedback_queue = "mock_feedback_queue" + mock_machine_config.return_value = { + "": MachineConfig(acquisition_software=["tomo"]) + } + + # Make sure dcg is present with an atlas id + dcg = DataCollectionGroup( + id=1, + session_id=ExampleVisit.murfey_session_id, + tag="processing_tag", + atlas_id=90, + atlas_pixel_size=1e-5, + sample=10, + atlas="/path/to/Sample10/Atlas/Atlas_1.jpg", + ) + murfey_db_session.add(dcg) + murfey_db_session.commit() + + # Add some search maps with the dcg tag and one with a different tag + im1 = ImagingSite( + dcg_id=dcg.id, + site_name="processing_tag", + ) + murfey_db_session.add(im1) + murfey_db_session.commit() + + # Request new dcg registration with new atlas tag and sample + dcg_params = DCGroupParameters( + experiment_type_id=37, + tag="processing_tag", + atlas="/path/to/Sample12/Atlas/Atlas_2.jpg", + sample=12, + atlas_pixel_size=1e-4, + atlas_x_stage_position=10, + atlas_y_stage_position=20, + atlas_width=200, + atlas_height=300, + ) + register_dc_group( + visit_name="cm12345-6", + session_id=ExampleVisit.murfey_session_id, + dcg_params=dcg_params, + db=murfey_db_session, + ) + + # Check request to ispyb for updating the experiment type + mock_transport.send.assert_called_once_with( + "mock_feedback_queue", + { + "register": "atlas_update", + "atlas_id": 90, + "atlas": "/path/to/Sample12/Atlas/Atlas_2.jpg", + "sample": 12, + "atlas_pixel_size": 1e-4, + "dcgid": 1, + "session_id": ExampleVisit.murfey_session_id, + "tag": "processing_tag", + }, + ) + + # Check the data collection group atlas was updated + new_dcg = murfey_db_session.exec( + select(DataCollectionGroup).where(DataCollectionGroup.id == dcg.id) + ).one() + assert new_dcg.atlas == "/path/to/Sample12/Atlas/Atlas_2.jpg" + assert new_dcg.sample == 12 + assert new_dcg.atlas_pixel_size == 1e-4 + assert new_dcg.tag == "processing_tag" + assert new_dcg.atlas_id == 90 + + # Check the imaging site insert + image_site = murfey_db_session.exec( + select(ImagingSite).where(ImagingSite.dcg_id == dcg.id) + ).one() + assert image_site.site_name == "processing_tag" + assert image_site.pos_x == 10 + assert image_site.image_pixels_x == 200 + assert image_site.image_pixel_size == 1e-4 diff --git a/tests/workflows/sxt/test_sxt_metadata.py b/tests/workflows/sxt/test_sxt_metadata.py new file mode 100644 index 000000000..7519abcc8 --- /dev/null +++ b/tests/workflows/sxt/test_sxt_metadata.py @@ -0,0 +1,130 @@ +import copy +from unittest import mock + +from sqlmodel import Session, select + +from murfey.util.db import ( + DataCollectionGroup, + ImagingSite, + SearchMap, +) +from murfey.util.models import SearchMapParameters +from murfey.workflows.sxt import sxt_metadata +from tests.conftest import ExampleVisit, get_or_create_db_entry + + +def set_up_db(murfey_db_session: Session): + # Insert common elements needed in all tests + dcg_entry: DataCollectionGroup = get_or_create_db_entry( + murfey_db_session, + DataCollectionGroup, + lookup_kwargs={ + "id": 0, + "session_id": ExampleVisit.murfey_session_id, + "tag": "/path/to/tomogram_source", + "atlas_id": 10, + }, + ) + get_or_create_db_entry( + murfey_db_session, + ImagingSite, + lookup_kwargs={ + "dcg_id": dcg_entry.id, + "site_name": "site", + "pos_x": 2, + "pos_y": 3, + "image_pixel_size": 0.5, + "image_pixels_x": 400, + "image_pixels_y": 500, + }, + ) + return dcg_entry.id + + +@mock.patch("murfey.workflows.sxt.sxt_metadata._transport_object") +def test_register_new_sxt_roi(mock_transport, murfey_db_session: Session, tmp_path): + set_up_db(murfey_db_session) + mock_transport.do_insert_sxt_roi.return_value = {"success": True, "return_value": 2} + + roi_params = SearchMapParameters( + tag="/path/to/tomogram_source", + x_stage_position=10, + y_stage_position=20, + pixel_size=None, + width=None, + height=None, + image="/path/to/image.jpg", + ) + return_dict = sxt_metadata.register_sxt_roi( + ExampleVisit.murfey_session_id, "roi_1", roi_params, murfey_db_session + ) + assert return_dict.get("success") + + # Check the ispyb message + mock_transport.do_insert_sxt_roi.assert_called_once_with(10, roi_params) + + # Check the database insert + roi_entry = murfey_db_session.exec(select(SearchMap)).one() + assert roi_entry.id == 2 + assert roi_entry.session_id == ExampleVisit.murfey_session_id + assert roi_entry.name == "roi_1" + assert roi_entry.tag == "/path/to/tomogram_source" + assert roi_entry.x_stage_position == 10 + assert roi_entry.y_stage_position == 20 + assert not roi_entry.pixel_size + assert not roi_entry.width + assert not roi_entry.height + assert roi_entry.image == "/path/to/image.jpg" + + +@mock.patch("murfey.workflows.sxt.sxt_metadata._transport_object") +def test_update_sxt_roi(mock_transport, murfey_db_session: Session, tmp_path): + set_up_db(murfey_db_session) + + get_or_create_db_entry( + murfey_db_session, + SearchMap, + lookup_kwargs={ + "session_id": ExampleVisit.murfey_session_id, + "name": "roi_1", + "tag": "/path/to/tomogram_source", + }, + ) + + roi_params = SearchMapParameters( + tag="/path/to/tomogram_source", + x_stage_position=10, + y_stage_position=20, + pixel_size=0.025, + width=200, + height=400, + image="/path/to/image.jpg", + ) + return_dict = sxt_metadata.register_sxt_roi( + ExampleVisit.murfey_session_id, + "roi_1", + copy.deepcopy(roi_params), + murfey_db_session, + ) + assert return_dict.get("success") + + # Check the second update + roi_params.x_location = 16 * 512 / 400 + 256 + roi_params.y_location = 256 - 34 * 512 / 500 + roi_params.width_on_atlas = int(round(200 * 0.05 * 512 / 400)) + roi_params.height_on_atlas = int(round(400 * 0.05 * 512 / 500)) + mock_transport.do_update_sxt_roi.assert_any_call(1, roi_params) + + # Check the database insert + roi_entry = murfey_db_session.exec(select(SearchMap)).one() + assert roi_entry.session_id == ExampleVisit.murfey_session_id + assert roi_entry.name == "roi_1" + assert roi_entry.tag == "/path/to/tomogram_source" + assert roi_entry.x_stage_position == 10 + assert roi_entry.y_stage_position == 20 + assert roi_entry.pixel_size == 0.025 + assert roi_entry.width == 200 + assert roi_entry.height == 400 + assert roi_entry.image == "/path/to/image.jpg" + assert roi_entry.x_location == 16 + assert roi_entry.y_location == 34 diff --git a/tests/workflows/test_register_data_collection_group.py b/tests/workflows/test_register_data_collection_group.py index 9d2783f70..6a565bc04 100644 --- a/tests/workflows/test_register_data_collection_group.py +++ b/tests/workflows/test_register_data_collection_group.py @@ -3,6 +3,7 @@ import pytest from pytest_mock import MockerFixture +from murfey.util.db import DataCollectionGroup, ImagingSite from murfey.workflows.register_data_collection_group import run from tests.conftest import ExampleVisit @@ -10,31 +11,33 @@ @pytest.mark.parametrize( "test_params", ( - # ISPyB session ID | # DCG search result | # DCG insert result | # Atlas insert result - (0, 0, 0, 0), - (0, 0, 0, None), - (0, 0, None, 0), - (0, 0, None, None), - (0, None, 0, 0), - (0, None, 0, None), - (0, None, None, 0), - (0, None, None, None), - (None, 0, 0, 0), - (None, 0, 0, None), - (None, 0, None, 0), - (None, 0, None, None), - (None, None, 0, 0), - (None, None, 0, None), - (None, None, None, 0), - (None, None, None, None), + # ISPyB session ID | # DCG search result | # DCG insert result | # Atlas insert result | # Atlas with location + (0, 0, 0, 0, False), + (0, 0, 0, None, False), + (0, 0, None, 0, False), + (0, 0, None, None, False), + (0, None, 0, 0, True), + (0, None, 0, None, False), + (0, None, None, 0, False), + (0, None, None, None, False), + (None, 0, 0, 0, True), + (None, 0, 0, None, False), + (None, 0, None, 0, False), + (None, 0, None, None, False), + (None, None, 0, 0, False), + (None, None, 0, None, False), + (None, None, None, 0, False), + (None, None, None, None, False), ), ) def test_run( mocker: MockerFixture, - test_params: tuple[int | None, int | None, int | None, int | None], + test_params: tuple[int | None, int | None, int | None, int | None, bool], ): # Unpack test params - (ispyb_session_id, dcg_result, insert_dcg, insert_atlas) = test_params + (ispyb_session_id, dcg_result, insert_dcg, insert_atlas, atlas_location) = ( + test_params + ) # Mock the transport object functions mock_transport_object = mocker.patch( @@ -72,6 +75,11 @@ def test_run( "atlas_pixel_size": 1e-9, "sample": 0, } + if atlas_location: + message["atlas_x_stage_position"] = 10 + message["atlas_y_stage_position"] = 20 + message["atlas_width"] = 200 + message["atlas_height"] = 400 result = run(message=message, murfey_db=mock_murfey_db) if dcg_result is not None: assert result == {"success": True} @@ -80,8 +88,42 @@ def test_run( mock_transport_object.do_insert_data_collection_group.assert_called_once() if insert_dcg is not None: mock_transport_object.do_insert_atlas.assert_called_once() + mock_murfey_db.add.assert_any_call( + DataCollectionGroup( + id=insert_dcg, + session_id=ExampleVisit.murfey_session_id, + tag="some_text", + smartem_grid_uuid=None, + atlas_id=insert_atlas, + atlas_pixel_size=1e-9, + atlas="some_file", + sample=0, + ) + ) assert result == {"success": True} else: assert result == {"success": False, "requeue": True} else: + mock_murfey_db.add.assert_any_call( + DataCollectionGroup( + session_id=ExampleVisit.murfey_session_id, + tag="some_text", + smartem_grid_uuid=None, + ) + ) assert result == {"success": True} + + if atlas_location: + mock_murfey_db.add.assert_any_call( + ImagingSite( + dcg_id=insert_dcg, + session_id=ExampleVisit.murfey_session_id, + site_name="some_text", + data_type="atlas", + pos_x=10, + pos_y=20, + image_pixels_x=200, + image_pixels_y=400, + image_pixel_size=1e-9, + ) + )