Skip to content

Commit 59d2aa4

Browse files
Read and insert tomography atlas and search map locations (#616)
Registers the locations of SearchMaps and BatchPositions for tomography data collection. This requires reading the SearchMap.dm and .xml files, and the BatchPositionsList.xml, plus finding the atlas in the Session.dm The logic for converting from stage coordinates to pixel positions is non-trivial and does not work perfectly. For the Krios microscopes with FALCON and K3_FLIPX cameras it appears to give reasonably accurate values (with some ambiguity over whether "zero" is top-left or bottom-left which may need fixing), but for the Talos with K3_FLIPY it does not currently work. The Murfey database has a new table for SearchMaps and the TiltSeries table now has positions. For ispyb the SearchMaps are inserted as GridSquares. TiltSeries location inserts will be done as part of the em-tomo-align recipe.
1 parent d8a7c9b commit 59d2aa4

File tree

12 files changed

+1286
-3
lines changed

12 files changed

+1286
-3
lines changed

src/murfey/client/analyser.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from murfey.client.contexts.spa import SPAModularContext
2020
from murfey.client.contexts.spa_metadata import SPAMetadataContext
2121
from murfey.client.contexts.tomo import TomographyContext
22+
from murfey.client.contexts.tomo_metadata import TomographyMetadataContext
2223
from murfey.client.instance_environment import MurfeyInstanceEnvironment
2324
from murfey.client.rsync import RSyncerUpdate, TransferResult
2425
from murfey.util.client import Observer, get_machine_config_client
@@ -226,6 +227,13 @@ def _analyse(self):
226227
and not self._context
227228
):
228229
self._context = SPAMetadataContext("epu", self._basepath)
230+
elif (
231+
"Batch" in transferred_file.parts
232+
or "SearchMaps" in transferred_file.parts
233+
or transferred_file.name == "Session.dm"
234+
and not self._context
235+
):
236+
self._context = TomographyMetadataContext("tomo", self._basepath)
229237
self.post_transfer(transferred_file)
230238
else:
231239
dc_metadata = {}
@@ -369,9 +377,10 @@ def _analyse(self):
369377
elif isinstance(
370378
self._context,
371379
(
372-
TomographyContext,
373380
SPAModularContext,
374381
SPAMetadataContext,
382+
TomographyContext,
383+
TomographyMetadataContext,
375384
),
376385
):
377386
context = str(self._context).split(" ")[0].split(".")[-1]
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import logging
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
import requests
6+
import xmltodict
7+
8+
from murfey.client.context import Context
9+
from murfey.client.contexts.spa import _file_transferred_to, _get_source
10+
from murfey.client.contexts.spa_metadata import _atlas_destination
11+
from murfey.client.instance_environment import MurfeyInstanceEnvironment, SampleInfo
12+
from murfey.util.api import url_path_for
13+
from murfey.util.client import authorised_requests, capture_post
14+
15+
logger = logging.getLogger("murfey.client.contexts.tomo_metadata")
16+
17+
requests.get, requests.post, requests.put, requests.delete = authorised_requests()
18+
19+
20+
def ensure_dcg_exists(transferred_file: Path, environment: MurfeyInstanceEnvironment):
21+
# Make sure we have a data collection group
22+
source = _get_source(transferred_file, environment=environment)
23+
if not source:
24+
return None
25+
dcg_tag = str(source).replace(f"/{environment.visit}", "")
26+
url = f"{str(environment.url.geturl())}{url_path_for('workflow.router', 'register_dc_group', visit_name=environment.visit, session_id=environment.murfey_session)}"
27+
dcg_data = {
28+
"experiment_type": "single particle",
29+
"experiment_type_id": 37,
30+
"tag": dcg_tag,
31+
}
32+
capture_post(url, json=dcg_data)
33+
return dcg_tag
34+
35+
36+
class TomographyMetadataContext(Context):
37+
def __init__(self, acquisition_software: str, basepath: Path):
38+
super().__init__("Tomography_metadata", acquisition_software)
39+
self._basepath = basepath
40+
41+
def post_transfer(
42+
self,
43+
transferred_file: Path,
44+
environment: Optional[MurfeyInstanceEnvironment] = None,
45+
**kwargs,
46+
):
47+
super().post_transfer(
48+
transferred_file=transferred_file,
49+
environment=environment,
50+
**kwargs,
51+
)
52+
53+
if transferred_file.name == "Session.dm" and environment:
54+
logger.info("Tomography session metadata found")
55+
with open(transferred_file, "r") as session_xml:
56+
session_data = xmltodict.parse(session_xml.read())
57+
58+
windows_path = session_data["TomographySession"]["AtlasId"]
59+
logger.info(f"Windows path to atlas metadata found: {windows_path}")
60+
visit_index = windows_path.split("\\").index(environment.visit)
61+
partial_path = "/".join(windows_path.split("\\")[visit_index + 1 :])
62+
logger.info("Partial Linux path successfully constructed from Windows path")
63+
64+
source = _get_source(transferred_file, environment)
65+
if not source:
66+
logger.warning(
67+
f"Source could not be identified for {str(transferred_file)}"
68+
)
69+
return
70+
71+
source_visit_dir = source.parent
72+
73+
logger.info(
74+
f"Looking for atlas XML file in metadata directory {str((source_visit_dir / partial_path).parent)}"
75+
)
76+
atlas_xml_path = list(
77+
(source_visit_dir / partial_path).parent.glob("Atlas_*.xml")
78+
)[0]
79+
logger.info(f"Atlas XML path {str(atlas_xml_path)} found")
80+
with open(atlas_xml_path, "rb") as atlas_xml:
81+
atlas_xml_data = xmltodict.parse(atlas_xml)
82+
atlas_pixel_size = float(
83+
atlas_xml_data["MicroscopeImage"]["SpatialScale"]["pixelSize"]["x"][
84+
"numericValue"
85+
]
86+
)
87+
88+
for p in partial_path.split("/"):
89+
if p.startswith("Sample"):
90+
sample = int(p.replace("Sample", ""))
91+
break
92+
else:
93+
logger.warning(f"Sample could not be identified for {transferred_file}")
94+
return
95+
environment.samples[source] = SampleInfo(
96+
atlas=Path(partial_path), sample=sample
97+
)
98+
url = f"{str(environment.url.geturl())}{url_path_for('workflow.router', 'register_dc_group', visit_name=environment.visit, session_id=environment.murfey_session)}"
99+
dcg_tag = "/".join(
100+
p for p in transferred_file.parent.parts if p != environment.visit
101+
).replace("//", "/")
102+
dcg_data = {
103+
"experiment_type": "tomo",
104+
"experiment_type_id": 36,
105+
"tag": dcg_tag,
106+
"atlas": str(
107+
_atlas_destination(environment, source, transferred_file)
108+
/ environment.samples[source].atlas.parent
109+
/ atlas_xml_path.with_suffix(".jpg").name
110+
),
111+
"sample": environment.samples[source].sample,
112+
"atlas_pixel_size": atlas_pixel_size,
113+
}
114+
capture_post(url, json=dcg_data)
115+
116+
elif transferred_file.name == "SearchMap.xml" and environment:
117+
logger.info("Tomography session search map xml found")
118+
dcg_tag = ensure_dcg_exists(transferred_file, environment)
119+
with open(transferred_file, "r") as sm_xml:
120+
sm_data = xmltodict.parse(sm_xml.read())
121+
122+
# This bit gets SearchMap location on Atlas
123+
sm_pixel_size = float(
124+
sm_data["MicroscopeImage"]["SpatialScale"]["pixelSize"]["x"][
125+
"numericValue"
126+
]
127+
)
128+
stage_position = sm_data["MicroscopeImage"]["microscopeData"]["stage"][
129+
"Position"
130+
]
131+
sm_binning = float(
132+
sm_data["MicroscopeImage"]["microscopeData"]["acquisition"]["camera"][
133+
"Binning"
134+
]["a:x"]
135+
)
136+
137+
# Get the stage transformation
138+
sm_transformations = sm_data["MicroscopeImage"]["CustomData"][
139+
"a:KeyValueOfstringanyType"
140+
]
141+
stage_matrix: dict[str, float] = {}
142+
image_matrix: dict[str, float] = {}
143+
for key_val in sm_transformations:
144+
if key_val["a:Key"] == "ReferenceCorrectionForStage":
145+
stage_matrix = {
146+
"m11": float(key_val["a:Value"]["b:_m11"]),
147+
"m12": float(key_val["a:Value"]["b:_m12"]),
148+
"m21": float(key_val["a:Value"]["b:_m21"]),
149+
"m22": float(key_val["a:Value"]["b:_m22"]),
150+
}
151+
elif key_val["a:Key"] == "ReferenceCorrectionForImageShift":
152+
image_matrix = {
153+
"m11": float(key_val["a:Value"]["b:_m11"]),
154+
"m12": float(key_val["a:Value"]["b:_m12"]),
155+
"m21": float(key_val["a:Value"]["b:_m21"]),
156+
"m22": float(key_val["a:Value"]["b:_m22"]),
157+
}
158+
if not stage_matrix or not image_matrix:
159+
logger.error(
160+
f"No stage or image shift matrix found for {transferred_file}"
161+
)
162+
163+
ref_matrix = {
164+
"m11": float(
165+
sm_data["MicroscopeImage"]["ReferenceTransformation"]["matrix"][
166+
"a:_m11"
167+
]
168+
),
169+
"m12": float(
170+
sm_data["MicroscopeImage"]["ReferenceTransformation"]["matrix"][
171+
"a:_m12"
172+
]
173+
),
174+
"m21": float(
175+
sm_data["MicroscopeImage"]["ReferenceTransformation"]["matrix"][
176+
"a:_m21"
177+
]
178+
),
179+
"m22": float(
180+
sm_data["MicroscopeImage"]["ReferenceTransformation"]["matrix"][
181+
"a:_m22"
182+
]
183+
),
184+
}
185+
186+
source = _get_source(transferred_file, environment=environment)
187+
image_path = (
188+
_file_transferred_to(
189+
environment, source, transferred_file.parent / "SearchMap.jpg"
190+
)
191+
if source
192+
else ""
193+
)
194+
195+
sm_url = f"{str(environment.url.geturl())}{url_path_for('session_control.tomo_router', 'register_search_map', session_id=environment.murfey_session, sm_name=transferred_file.parent.name)}"
196+
capture_post(
197+
sm_url,
198+
json={
199+
"tag": dcg_tag,
200+
"x_stage_position": float(stage_position["X"]),
201+
"y_stage_position": float(stage_position["Y"]),
202+
"pixel_size": sm_pixel_size,
203+
"image": str(image_path),
204+
"binning": sm_binning,
205+
"reference_matrix": ref_matrix,
206+
"stage_correction": stage_matrix,
207+
"image_shift_correction": image_matrix,
208+
},
209+
)
210+
211+
elif transferred_file.name == "SearchMap.dm" and environment:
212+
logger.info("Tomography session search map dm found")
213+
dcg_tag = ensure_dcg_exists(transferred_file, environment)
214+
with open(transferred_file, "r") as sm_xml:
215+
sm_data = xmltodict.parse(sm_xml.read())
216+
217+
# This bit gets SearchMap size
218+
try:
219+
sm_width = int(sm_data["TileSetXml"]["ImageSize"]["a:width"])
220+
sm_height = int(sm_data["TileSetXml"]["ImageSize"]["a:height"])
221+
except KeyError:
222+
logger.warning(f"Unable to find size for SearchMap {transferred_file}")
223+
readout_width = int(
224+
sm_data["TileSetXml"]["AcquisitionSettings"]["a:camera"][
225+
"a:ReadoutArea"
226+
]["b:width"]
227+
)
228+
readout_height = int(
229+
sm_data["TileSetXml"]["AcquisitionSettings"]["a:camera"][
230+
"a:ReadoutArea"
231+
]["b:height"]
232+
)
233+
sm_width = int(
234+
8005 * readout_width / max(readout_height, readout_width)
235+
)
236+
sm_height = int(
237+
8005 * readout_height / max(readout_height, readout_width)
238+
)
239+
logger.warning(
240+
f"Inserting incorrect width {sm_width}, height {sm_height} for SearchMap display"
241+
)
242+
243+
sm_url = f"{str(environment.url.geturl())}{url_path_for('session_control.tomo_router', 'register_search_map', session_id=environment.murfey_session, sm_name=transferred_file.parent.name)}"
244+
capture_post(
245+
sm_url,
246+
json={
247+
"tag": dcg_tag,
248+
"height": sm_height,
249+
"width": sm_width,
250+
},
251+
)
252+
253+
elif transferred_file.name == "BatchPositionsList.xml" and environment:
254+
logger.info("Tomography session batch positions list found")
255+
dcg_tag = ensure_dcg_exists(transferred_file, environment)
256+
with open(transferred_file) as xml:
257+
for_parsing = xml.read()
258+
batch_xml = xmltodict.parse(for_parsing)
259+
260+
batch_positions_list = batch_xml["BatchPositionsList"]["BatchPositions"][
261+
"BatchPositionParameters"
262+
]
263+
if isinstance(batch_positions_list, dict):
264+
# Case of a single batch
265+
batch_positions_list = [batch_positions_list]
266+
267+
for batch_position in batch_positions_list:
268+
batch_name = batch_position["Name"]
269+
search_map_name = batch_position["PositionOnTileSet"]["TileSetName"]
270+
batch_stage_location_x = float(
271+
batch_position["PositionOnTileSet"]["StagePositionX"]
272+
)
273+
batch_stage_location_y = float(
274+
batch_position["PositionOnTileSet"]["StagePositionY"]
275+
)
276+
277+
# Always need search map before batch position
278+
sm_url = f"{str(environment.url.geturl())}{url_path_for('session_control.tomo_router', 'register_search_map', session_id=environment.murfey_session, sm_name=search_map_name)}"
279+
capture_post(
280+
sm_url,
281+
json={
282+
"tag": dcg_tag,
283+
},
284+
)
285+
286+
# Then register batch position
287+
bp_url = f"{str(environment.url.geturl())}{url_path_for('session_control.tomo_router', 'register_batch_position', session_id=environment.murfey_session, batch_name=batch_name)}"
288+
capture_post(
289+
bp_url,
290+
json={
291+
"tag": dcg_tag,
292+
"x_stage_position": batch_stage_location_x,
293+
"y_stage_position": batch_stage_location_y,
294+
"x_beamshift": 0,
295+
"y_beamshift": 0,
296+
"search_map_name": search_map_name,
297+
},
298+
)
299+
300+
# Beamshifts
301+
if batch_position.get("AdditionalExposureTemplateAreas"):
302+
beamshifts = batch_position["AdditionalExposureTemplateAreas"][
303+
"ExposureTemplateAreaParameters"
304+
]
305+
if type(beamshifts) is dict:
306+
beamshifts = [beamshifts]
307+
for beamshift in beamshifts:
308+
beamshift_name = beamshift["Name"]
309+
beamshift_position_x = float(beamshift["PositionX"])
310+
beamshift_position_y = float(beamshift["PositionY"])
311+
312+
# Registration of beamshifted position
313+
bp_url = f"{str(environment.url.geturl())}{url_path_for('session_control.tomo_router', 'register_batch_position', session_id=environment.murfey_session, batch_name=beamshift_name)}"
314+
capture_post(
315+
bp_url,
316+
json={
317+
"tag": dcg_tag,
318+
"x_stage_position": batch_stage_location_x,
319+
"y_stage_position": batch_stage_location_y,
320+
"x_beamshift": beamshift_position_x,
321+
"y_beamshift": beamshift_position_y,
322+
"search_map_name": search_map_name,
323+
},
324+
)

0 commit comments

Comments
 (0)