Skip to content

Commit 7ed67bb

Browse files
authored
Make ISPyB records for atlas, grid squares and foil holes (#426)
This is complicated and almost certainly buggy. The problems arise because we need to allow for grid square and foil hole updates when metadata becomes available.
1 parent 26d5523 commit 7ed67bb

File tree

7 files changed

+414
-31
lines changed

7 files changed

+414
-31
lines changed

src/murfey/client/contexts/spa.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class FoilHole(NamedTuple):
4141
thumbnail_size_y: Optional[int] = None
4242
pixel_size: Optional[float] = None
4343
image: str = ""
44+
diameter: Optional[float] = None
4445

4546

4647
class GridSquare(NamedTuple):
@@ -59,9 +60,18 @@ class GridSquare(NamedTuple):
5960
tag: str = ""
6061

6162

62-
def _get_grid_square_atlas_positions(
63-
xml_path: Path, grid_square: str = ""
64-
) -> Dict[str, Tuple[Optional[int], Optional[int], Optional[float], Optional[float]]]:
63+
def _get_grid_square_atlas_positions(xml_path: Path, grid_square: str = "") -> Dict[
64+
str,
65+
Tuple[
66+
Optional[int],
67+
Optional[int],
68+
Optional[float],
69+
Optional[float],
70+
Optional[int],
71+
Optional[int],
72+
Optional[float],
73+
],
74+
]:
6575
with open(
6676
xml_path,
6777
"r",
@@ -71,7 +81,16 @@ def _get_grid_square_atlas_positions(
7181
"TileXml"
7282
]
7383
gs_pix_positions: Dict[
74-
str, Tuple[Optional[int], Optional[int], Optional[float], Optional[float]]
84+
str,
85+
Tuple[
86+
Optional[int],
87+
Optional[int],
88+
Optional[float],
89+
Optional[float],
90+
Optional[int],
91+
Optional[int],
92+
Optional[float],
93+
],
7594
] = {}
7695
for ti in tile_info:
7796
try:
@@ -96,6 +115,13 @@ def _get_grid_square_atlas_positions(
96115
* 1e9,
97116
float(gs["value"]["b:PositionOnTheAtlas"]["c:Physical"]["d:y"])
98117
* 1e9,
118+
int(
119+
float(gs["value"]["b:PositionOnTheAtlas"]["c:Size"]["d:width"])
120+
),
121+
int(
122+
float(gs["value"]["b:PositionOnTheAtlas"]["c:Size"]["d:height"])
123+
),
124+
float(gs["value"]["b:PositionOnTheAtlas"]["c:Rotation"]),
99125
)
100126
if grid_square:
101127
break
@@ -221,6 +247,7 @@ def _foil_hole_data(
221247
for fh_block in serialization_array[required_key]:
222248
pix = fh_block["b:value"]["PixelCenter"]
223249
stage = fh_block["b:value"]["StagePosition"]
250+
diameter = fh_block["b:value"]["PixelWidthHeight"]["c:width"]
224251
if int(fh_block["b:key"]) == foil_hole:
225252
return FoilHole(
226253
id=foil_hole,
@@ -236,6 +263,7 @@ def _foil_hole_data(
236263
thumbnail_size_y=None,
237264
pixel_size=float(pixel_size) if image_path else None,
238265
image=str(image_path),
266+
diameter=diameter,
239267
)
240268
logger.warning(
241269
f"Foil hole positions could not be determined from metadata file {xml_path} for foil hole {foil_hole}"
@@ -557,7 +585,10 @@ def _position_analysis(
557585
Optional[int],
558586
Optional[float],
559587
Optional[float],
560-
] = (None, None, None, None)
588+
Optional[int],
589+
Optional[int],
590+
Optional[float],
591+
] = (None, None, None, None, None, None, None)
561592
data_collection_group = (
562593
requests.get(
563594
f"{str(environment.url.geturl())}/sessions/{environment.murfey_session}/data_collection_groups"
@@ -611,6 +642,9 @@ def _position_analysis(
611642
"y_location": gs_pix_position[1],
612643
"x_stage_position": gs_pix_position[2],
613644
"y_stage_position": gs_pix_position[3],
645+
"width": gs_pix_position[4],
646+
"height": gs_pix_position[5],
647+
"angle": gs_pix_position[6],
614648
},
615649
)
616650
foil_hole = _foil_hole_from_file(transferred_file)
@@ -651,6 +685,7 @@ def _position_analysis(
651685
"thumbnail_size_x": fh.thumbnail_size_x,
652686
"thumbnail_size_y": fh.thumbnail_size_y,
653687
"pixel_size": fh.pixel_size,
688+
"diameter": fh.diameter,
654689
"tag": str(source),
655690
"image": str(image_path),
656691
},

src/murfey/client/contexts/spa_metadata.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import requests
66
import xmltodict
7+
from PIL import Image
78

89
from murfey.client.context import Context
910
from murfey.client.contexts.spa import _get_grid_square_atlas_positions, _get_source
@@ -64,6 +65,38 @@ def post_transfer(
6465
visitless_path = Path(
6566
str(transferred_file).replace(f"/{environment.visit}", "")
6667
)
68+
visit_index_of_transferred_file = transferred_file.parts.index(
69+
environment.visit
70+
)
71+
atlas_xml_path = list(
72+
(
73+
Path(
74+
"/".join(
75+
transferred_file.parts[
76+
: visit_index_of_transferred_file + 1
77+
]
78+
)
79+
)
80+
/ partial_path
81+
).parent.glob("Atlas_*.xml")
82+
)[0]
83+
with open(atlas_xml_path, "rb") as atlas_xml:
84+
atlas_xml_data = xmltodict.parse(atlas_xml)
85+
atlas_original_pixel_size = atlas_xml_data["MicroscopeImage"][
86+
"SpatialScale"
87+
]["pixelSize"]["x"]["numericValue"]
88+
readout_width = float(
89+
atlas_xml_data["MicroscopeImage"]["SpatialScale"]["pixelSize"]["x"][
90+
"numericValue"
91+
]
92+
)
93+
94+
# need to calculate the pixel size of the downscaled image
95+
atlas_im = Image.open(atlas_xml_path.with_suffix(".jpg"))
96+
atlas_pixel_size = atlas_original_pixel_size * (
97+
readout_width / atlas_im.width
98+
)
99+
67100
source = _get_source(
68101
visitless_path.parent / "Images-Disc1" / visitless_path.name,
69102
environment,
@@ -90,6 +123,7 @@ def post_transfer(
90123
/ environment.samples[source].atlas
91124
),
92125
"sample": environment.samples[source].sample,
126+
"atlas_pixel_size": atlas_pixel_size,
93127
}
94128
capture_post(url, json=dcg_data)
95129
registered_grid_squares = (
@@ -121,5 +155,8 @@ def post_transfer(
121155
"y_location": pos_data[1],
122156
"x_stage_position": pos_data[2],
123157
"y_stage_position": pos_data[3],
158+
"width": pos_data[4],
159+
"height": pos_data[5],
160+
"angle": pos_data[6],
124161
},
125162
)

src/murfey/server/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from fastapi.templating import Jinja2Templates
2424
from importlib_metadata import EntryPoint # For type hinting only
2525
from ispyb.sqlalchemy._auto_db_schema import (
26+
Atlas,
2627
AutoProcProgram,
2728
Base,
2829
DataCollection,
@@ -43,6 +44,7 @@
4344
from werkzeug.utils import secure_filename
4445

4546
import murfey
47+
import murfey.server.ispyb
4648
import murfey.server.prometheus as prom
4749
import murfey.server.websocket
4850
import murfey.util.db as db
@@ -2629,8 +2631,17 @@ def feedback_callback(header: dict, message: dict) -> None:
26292631
experimentTypeId=message["experiment_type_id"],
26302632
)
26312633
dcgid = _register(record, header)
2634+
atlas_record = Atlas(
2635+
dataCollectionGroupId=dcgid,
2636+
atlasImage=message.get("atlas", ""),
2637+
pixelSize=message.get("atlas_pixel_size", 0),
2638+
cassetteSlot=message.get("sample"),
2639+
)
2640+
if _transport_object:
2641+
atlas_id = _transport_object.do_insert_atlas(atlas_record)
26322642
murfey_dcg = db.DataCollectionGroup(
26332643
id=dcgid,
2644+
atlas_id=atlas_id,
26342645
session_id=message["session_id"],
26352646
tag=message.get("tag"),
26362647
)
@@ -2655,6 +2666,14 @@ def feedback_callback(header: dict, message: dict) -> None:
26552666
}
26562667
_transport_object.transport.ack(header)
26572668
return None
2669+
elif message["register"] == "atlas_update":
2670+
if _transport_object:
2671+
_transport_object.do_update_atlas(
2672+
message["atlas_id"],
2673+
message["atlas"],
2674+
message["atlas_pixel_size"],
2675+
message["sample"],
2676+
)
26582677
elif message["register"] == "data_collection":
26592678
murfey_session_id = message["session_id"]
26602679
ispyb_session_id = murfey.server.ispyb.get_session_id(

src/murfey/server/api/__init__.py

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,21 @@ def register_grid_square(
502502
grid_square.y_location = grid_square_params.y_location
503503
grid_square.x_stage_position = grid_square_params.x_stage_position
504504
grid_square.y_stage_position = grid_square_params.y_stage_position
505+
if _transport_object:
506+
_transport_object.do_update_grid_square(grid_square.id, grid_square_params)
505507
except Exception:
508+
if _transport_object:
509+
dcg = db.exec(
510+
select(DataCollectionGroup)
511+
.where(DataCollectionGroup.session_id == session_id)
512+
.where(DataCollectionGroup.tag == grid_square_params.tag)
513+
).one()
514+
gs_ispyb_response = _transport_object.do_insert_grid_square(
515+
dcg.atlas_id, gsid, grid_square_params
516+
)
517+
else:
518+
# mock up response so that below still works
519+
gs_ispyb_response = {"success": False, "return_value": None}
506520
secured_grid_square_image_path = secure_filename(grid_square_params.image)
507521
if (
508522
secured_grid_square_image_path
@@ -512,6 +526,11 @@ def register_grid_square(
512526
else:
513527
jpeg_size = (0, 0)
514528
grid_square = GridSquare(
529+
id=(
530+
gs_ispyb_response["return_value"]
531+
if gs_ispyb_response["success"]
532+
else None
533+
),
515534
name=gsid,
516535
session_id=session_id,
517536
tag=grid_square_params.tag,
@@ -552,16 +571,13 @@ def register_foil_hole(
552571
db=murfey_db,
553572
):
554573
try:
555-
gsid = (
556-
db.exec(
557-
select(GridSquare)
558-
.where(GridSquare.tag == foil_hole_params.tag)
559-
.where(GridSquare.session_id == session_id)
560-
.where(GridSquare.name == gs_name)
561-
)
562-
.one()
563-
.id
564-
)
574+
gs = db.exec(
575+
select(GridSquare)
576+
.where(GridSquare.tag == foil_hole_params.tag)
577+
.where(GridSquare.session_id == session_id)
578+
.where(GridSquare.name == gs_name)
579+
).one()
580+
gsid = gs.id
565581
except NoResultFound:
566582
log.debug(
567583
f"Foil hole {sanitise(str(foil_hole_params.name))} could not be registered as grid square {sanitise(str(gs_name))} was not found"
@@ -572,21 +588,53 @@ def register_foil_hole(
572588
jpeg_size = Image.open(secured_foil_hole_image_path).size
573589
else:
574590
jpeg_size = (0, 0)
575-
foil_hole = FoilHole(
576-
name=foil_hole_params.name,
577-
session_id=session_id,
578-
grid_square_id=gsid,
579-
x_location=foil_hole_params.x_location,
580-
y_location=foil_hole_params.y_location,
581-
x_stage_position=foil_hole_params.x_stage_position,
582-
y_stage_position=foil_hole_params.y_stage_position,
583-
readout_area_x=foil_hole_params.readout_area_x,
584-
readout_area_y=foil_hole_params.readout_area_y,
585-
thumbnail_size_x=foil_hole_params.thumbnail_size_x or jpeg_size[0],
586-
thumbnail_size_y=foil_hole_params.thumbnail_size_y or jpeg_size[1],
587-
pixel_size=foil_hole_params.pixel_size,
588-
image=secured_foil_hole_image_path,
589-
)
591+
try:
592+
foil_hole = db.exec(
593+
select(FoilHole)
594+
.where(FoilHole.name == foil_hole_params.name)
595+
.where(FoilHole.grid_square_id == gsid)
596+
.where(FoilHole.session_id == session_id)
597+
).one()
598+
foil_hole.x_location = foil_hole_params.x_location
599+
foil_hole.y_location = foil_hole_params.y_location
600+
foil_hole.x_stage_position = foil_hole_params.x_stage_position
601+
foil_hole.y_stage_position = foil_hole_params.y_stage_position
602+
foil_hole.readout_area_x = foil_hole_params.readout_area_x
603+
foil_hole.readout_area_y = foil_hole_params.readout_area_y
604+
foil_hole.thumbnail_size_x = foil_hole_params.thumbnail_size_x or jpeg_size[0]
605+
foil_hole.thumbnail_size_y = foil_hole_params.thumbnail_size_y or jpeg_size[1]
606+
foil_hole.pixel_size = foil_hole_params.pixel_size
607+
if _transport_object:
608+
_transport_object.do_update_foil_hole(
609+
foil_hole.id, gs.thumbnail_size_x / gs.readout_area_x, foil_hole_params
610+
)
611+
except Exception:
612+
if _transport_object:
613+
fh_ispyb_response = _transport_object.do_insert_foil_hole(
614+
gsid.id, gs.thumbnail_size_x / gs.readout_area_x, foil_hole_params
615+
)
616+
else:
617+
fh_ispyb_response = {"success": False, "return_value": None}
618+
foil_hole = FoilHole(
619+
id=(
620+
fh_ispyb_response["return_value"]
621+
if fh_ispyb_response["success"]
622+
else None
623+
),
624+
name=foil_hole_params.name,
625+
session_id=session_id,
626+
grid_square_id=gsid,
627+
x_location=foil_hole_params.x_location,
628+
y_location=foil_hole_params.y_location,
629+
x_stage_position=foil_hole_params.x_stage_position,
630+
y_stage_position=foil_hole_params.y_stage_position,
631+
readout_area_x=foil_hole_params.readout_area_x,
632+
readout_area_y=foil_hole_params.readout_area_y,
633+
thumbnail_size_x=foil_hole_params.thumbnail_size_x or jpeg_size[0],
634+
thumbnail_size_y=foil_hole_params.thumbnail_size_y or jpeg_size[1],
635+
pixel_size=foil_hole_params.pixel_size,
636+
image=secured_foil_hole_image_path,
637+
)
590638
db.add(foil_hole)
591639
db.commit()
592640
db.close()
@@ -1128,6 +1176,7 @@ async def request_spa_preprocessing(
11281176
"image_number": proc_file.image_number,
11291177
"microscope": get_microscope(),
11301178
"mc_uuid": murfey_ids[0],
1179+
"foil_hole_id": foil_hole_id,
11311180
"ft_bin": proc_params["motion_corr_binning"],
11321181
"fm_dose": proc_params["dose_per_frame"],
11331182
"gain_ref": proc_params["gain_ref"],
@@ -1377,15 +1426,30 @@ def register_dc_group(
13771426
).all():
13781427
dcg_murfey[0].atlas = dcg_params.atlas
13791428
dcg_murfey[0].sample = dcg_params.sample
1429+
dcg_murfey[0].atlas_pixel_size = dcg_params.atlas_pixel_size
13801430
db.add(dcg_murfey[0])
13811431
db.commit()
1432+
if _transport_object:
1433+
_transport_object.send(
1434+
_transport_object.feedback_queue,
1435+
{
1436+
"register": "atlas_update",
1437+
"atlas_id": dcg_murfey.atlas_id,
1438+
"atlas": dcg_params.atlas,
1439+
"sample": dcg_params.sample,
1440+
"atlas_pixel_size": dcg_params.atlas_pixel_size,
1441+
},
1442+
)
13821443
else:
13831444
dcg_parameters = {
13841445
"start_time": str(datetime.datetime.now()),
13851446
"experiment_type": dcg_params.experiment_type,
13861447
"experiment_type_id": dcg_params.experiment_type_id,
13871448
"tag": dcg_params.tag,
13881449
"session_id": session_id,
1450+
"atlas": dcg_params.atlas,
1451+
"sample": dcg_params.sample,
1452+
"atlas_pixel_size": dcg_params.atlas_pixel_size,
13891453
}
13901454

13911455
if _transport_object:

0 commit comments

Comments
 (0)