Skip to content

Commit 6d93c78

Browse files
Search for a txrm reference is no internal one is present (#826)
If a txrm does not have an internal reference, instead of dismissing it search for a reference file in xrm format. Takes the most recent xrm which is not a mosaic image. Still skips processing for txrms with less than 20 frames as these will be reference stacks. Also changes the tilt series tags to remove angle naming, and skips transfer of "New folder" Pairs with DiamondLightSource/cryoem-services#271
1 parent ba586ab commit 6d93c78

5 files changed

Lines changed: 192 additions & 7 deletions

File tree

src/murfey/client/contexts/sxt.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import datetime
23
from pathlib import Path
34
from typing import Any
45

@@ -13,11 +14,50 @@
1314
)
1415
from murfey.client.instance_environment import MurfeyInstanceEnvironment
1516
from murfey.util.client import capture_post
17+
from murfey.util.models import File
1618
from murfey.util.tomo import midpoint
1719

1820
logger = logging.getLogger("murfey.client.contexts.sxt")
1921

2022

23+
def _find_reference(txrm_file: Path) -> Path | None:
24+
"""Find a suitable reference to apply to the given txrm file"""
25+
# Look for xrm files in the txrm folder, reverse sorted by time
26+
candidates = []
27+
for gf in txrm_file.parent.glob("*.xrm"):
28+
candidates.append(
29+
File(
30+
name=gf.name,
31+
description="",
32+
size=gf.stat().st_size / 1e6,
33+
timestamp=datetime.fromtimestamp(gf.stat().st_mtime),
34+
full_path=str(gf),
35+
)
36+
)
37+
candidates.sort(key=lambda x: x.timestamp, reverse=True)
38+
for ref_option in candidates:
39+
mosaic_size = 1
40+
with OleFileIO(ref_option.full_path) as xrm_ole:
41+
# Find images which are not mosaics (txrm spec typos this as mosiac)
42+
if xrm_ole.exists("ImageInfo/MosiacRows") and xrm_ole.exists(
43+
"ImageInfo/MosiacColumns"
44+
):
45+
mosaic_size = int(
46+
np.frombuffer(
47+
xrm_ole.openstream("ImageInfo/MosiacRows").getvalue(), np.int32
48+
)[0]
49+
* np.frombuffer(
50+
xrm_ole.openstream("ImageInfo/MosiacColumns").getvalue(),
51+
np.int32,
52+
)[0]
53+
)
54+
if mosaic_size == 0:
55+
logger.info(f"Found reference {ref_option}")
56+
return Path(ref_option.full_path)
57+
logger.warning(f"No reference found for {txrm_file}")
58+
return None
59+
60+
2161
class SXTContext(Context):
2262
def __init__(
2363
self,
@@ -128,6 +168,7 @@ def post_transfer(
128168
return False
129169

130170
# Read the tilt angles and pixel size from the txrm
171+
angles: list = []
131172
metadata: dict[str, Any] = {
132173
"source": str(self._basepath),
133174
"tilt_series_tag": transferred_file.stem,
@@ -206,16 +247,30 @@ def post_transfer(
206247
)
207248
metadata["energy"] = int(round(axis_values[energy_index]))
208249

209-
if not metadata.get("has_reference", False):
250+
if (
251+
not metadata.get("has_reference", False)
252+
and metadata.get("tilt_series_length", len(angles)) < 20
253+
):
254+
# References are collected with only 10 frames
210255
logger.debug(f"Reference image {transferred_file} not processed")
211256
return True
257+
elif not metadata.get("has_reference", False):
258+
reference_file = _find_reference(transferred_file)
259+
else:
260+
reference_file = None
212261

262+
if "@" in transferred_file.stem:
263+
tilt_series_tag = "_".join(
264+
transferred_file.stem.split("@")[0].split("_")[:-1]
265+
)
266+
else:
267+
tilt_series_tag = transferred_file.stem
213268
visit_index = transferred_file.parent.parts.index(environment.visit)
214269
destination_search_dir = "/".join(
215270
transferred_file.parts[: visit_index + 2]
216271
).replace("//", "/")
217272
self.register_sxt_data_collection(
218-
tilt_series=transferred_file.stem,
273+
tilt_series=tilt_series_tag,
219274
data_collection_parameters=metadata,
220275
file_extension=transferred_file.suffix,
221276
image_directory=str(
@@ -238,6 +293,15 @@ def post_transfer(
238293
transferred_file,
239294
Path(self._machine_config.get("rsync_basepath", "")),
240295
)
296+
if reference_file:
297+
reference_file_transferred_to = _file_transferred_to(
298+
environment,
299+
source,
300+
reference_file,
301+
Path(self._machine_config.get("rsync_basepath", "")),
302+
)
303+
else:
304+
reference_file_transferred_to = None
241305
capture_post(
242306
base_url=str(environment.url.geturl()),
243307
router_name="workflow.sxt_router",
@@ -247,7 +311,7 @@ def post_transfer(
247311
visit_name=environment.visit,
248312
session_id=environment.murfey_session,
249313
data={
250-
"tag": transferred_file.stem,
314+
"tag": tilt_series_tag,
251315
"source": destination_search_dir,
252316
"pixel_size": round(
253317
metadata.get("pixel_size", 100), 2
@@ -257,6 +321,9 @@ def post_transfer(
257321
"tilt_series_length", len(angles)
258322
),
259323
"txrm": str(file_transferred_to),
324+
"xrm_reference": str(reference_file_transferred_to)
325+
if reference_file_transferred_to
326+
else None,
260327
},
261328
)
262329
return True

src/murfey/client/watchdir_multigrid.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def _handle_fractions(self, directory: Path):
9393
def _process(self):
9494
while not self._stopping:
9595
for d in self._basepath.glob("*"):
96-
if d.name in self._machine_config["create_directories"]:
96+
if d.name.startswith("New folder"):
97+
self._seen_dirs.append(d)
98+
elif d.name in self._machine_config["create_directories"]:
9799
if d.is_dir() and d not in self._seen_dirs:
98100
self.notify(
99101
d,

src/murfey/workflows/sxt/process_sxt_tilt_series.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class SXTTiltSeriesInfo(BaseModel):
2828
tilt_series_length: int
2929
pixel_size: float
3030
tilt_offset: int
31+
xrm_reference: str | None
3132

3233

3334
def process_sxt_tilt_series_workflow(
@@ -98,6 +99,7 @@ def process_sxt_tilt_series_workflow(
9899
"recipes": ["sxt-aretomo"],
99100
"parameters": {
100101
"txrm_file": tilt_series_info.txrm,
102+
"xrm_reference": tilt_series_info.xrm_reference or "",
101103
"dcid": collected_ids[1].id,
102104
"appid": collected_ids[3].id,
103105
"stack_file": str(stack_file),

tests/client/contexts/test_sxt.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_sxt_context_txrm(mock_ole_file, mock_post, tmp_path):
5454
np.array([2048], dtype=np.int32).tobytes(), # Image Height
5555
np.array([1.5], dtype=np.float32).tobytes(), # Exposure time
5656
np.array([1000], dtype=np.float32).tobytes(), # Mag
57-
np.array([5], dtype=np.int32).tobytes(), # Image count
57+
np.array([200], dtype=np.int32).tobytes(), # Image count
5858
np.array([0, 519, 2, 3], dtype=np.float32).tobytes(), # Motor Pos (energy)
5959
]
6060

@@ -122,7 +122,7 @@ def test_sxt_context_txrm(mock_ole_file, mock_post, tmp_path):
122122
"voltage": 0,
123123
"axis_start": -55,
124124
"axis_end": 65,
125-
"tilt_series_length": 5,
125+
"tilt_series_length": 200,
126126
},
127127
headers={"Authorization": "Bearer "},
128128
)
@@ -143,8 +143,120 @@ def test_sxt_context_txrm(mock_ole_file, mock_post, tmp_path):
143143
"source": f"{tmp_path}/cm12345-6/grid1",
144144
"pixel_size": 100.1,
145145
"tilt_offset": 5,
146-
"tilt_series_length": 5,
146+
"tilt_series_length": 200,
147147
"txrm": str(tmp_path / "destination/cm12345-6/grid1/example.txrm"),
148+
"xrm_reference": None,
149+
},
150+
headers={"Authorization": "Bearer "},
151+
)
152+
153+
154+
@patch("requests.post")
155+
@patch("murfey.client.contexts.sxt.OleFileIO")
156+
def test_sxt_context_txrm_external_ref(mock_ole_file, mock_post, tmp_path):
157+
mock_post().status_code = 200
158+
exists_return = [False] # False for reference, then True
159+
exists_return.extend([True for i in range(20)])
160+
mock_ole_file().__enter__().exists.side_effect = exists_return
161+
# Motor position names
162+
mock_ole_file().__enter__().openstream().read.return_value = (
163+
"\x00Val1\x00\x00Energy\x00".encode()
164+
)
165+
# Metadata encoded arrays
166+
mock_ole_file().__enter__().openstream().getvalue.side_effect = [
167+
np.array([-55, -25, 5, 35, 65], dtype=np.float32).tobytes(), # Angles
168+
np.array([0.01001], dtype=np.float32).tobytes(), # Pixel size
169+
np.array([1024], dtype=np.int32).tobytes(), # Image Width
170+
np.array([2048], dtype=np.int32).tobytes(), # Image Height
171+
np.array([1.5], dtype=np.float32).tobytes(), # Exposure time
172+
np.array([1000], dtype=np.float32).tobytes(), # Mag
173+
np.array([200], dtype=np.int32).tobytes(), # Image count
174+
np.array([0, 519, 2, 3], dtype=np.float32).tobytes(), # Motor Pos (energy)
175+
np.array([0], dtype=np.int32).tobytes(), # Mosaic size
176+
np.array([0], dtype=np.int32).tobytes(), # Mosaic size
177+
]
178+
179+
# xrm file as reference
180+
(tmp_path / "cm12345-6/grid1").mkdir(parents=True)
181+
(tmp_path / "cm12345-6/grid1/ref.xrm").touch()
182+
183+
env = MurfeyInstanceEnvironment(
184+
url=urlparse("http://localhost:8000"),
185+
client_id=0,
186+
sources=[tmp_path / "cm12345-6/grid1"],
187+
default_destinations={
188+
f"{tmp_path}/cm12345-6/grid1": f"{tmp_path}/destination/cm12345-6/grid1"
189+
},
190+
instrument_name="",
191+
visit="cm12345-6",
192+
murfey_session=1,
193+
)
194+
context = SXTContext("zeiss", tmp_path / "cm12345-6/grid1", {}, "")
195+
context.post_transfer(
196+
tmp_path / "cm12345-6/grid1/example_-60to60@0.5.txrm",
197+
required_position_files=[],
198+
required_strings=["fractions"],
199+
environment=env,
200+
)
201+
202+
mock_ole_file.assert_any_call(
203+
str(tmp_path / "cm12345-6/grid1/example_-60to60@0.5.txrm")
204+
)
205+
mock_ole_file.assert_any_call(str(tmp_path / "cm12345-6/grid1/ref.xrm"))
206+
207+
assert mock_post.call_count == 5
208+
mock_post.assert_any_call(
209+
"http://localhost:8000/workflow/visits/cm12345-6/sessions/1/register_data_collection_group",
210+
json={
211+
"experiment_type_id": 47,
212+
"tag": f"{tmp_path}/cm12345-6/grid1",
213+
},
214+
headers={"Authorization": "Bearer "},
215+
)
216+
mock_post.assert_any_call(
217+
"http://localhost:8000/workflow/visits/cm12345-6/sessions/1/start_data_collection",
218+
json={
219+
"experiment_type": "sxt",
220+
"file_extension": ".txrm",
221+
"acquisition_software": "zeiss",
222+
"image_directory": f"{tmp_path}/destination/cm12345-6/grid1",
223+
"data_collection_tag": "example",
224+
"source": f"{tmp_path}/cm12345-6/grid1",
225+
"tag": "example",
226+
"pixel_size_on_image": str(100.1 * 1e-10),
227+
"image_size_x": 1024,
228+
"image_size_y": 2048,
229+
"magnification": 1000,
230+
"energy": 519,
231+
"voltage": 0,
232+
"axis_start": -55,
233+
"axis_end": 65,
234+
"tilt_series_length": 200,
235+
},
236+
headers={"Authorization": "Bearer "},
237+
)
238+
mock_post.assert_any_call(
239+
"http://localhost:8000/workflow/visits/cm12345-6/sessions/1/register_processing_job",
240+
json={
241+
"tag": "example",
242+
"source": f"{tmp_path}/cm12345-6/grid1",
243+
"recipe": "sxt-aretomo",
244+
"experiment_type": "sxt",
245+
},
246+
headers={"Authorization": "Bearer "},
247+
)
248+
mock_post.assert_any_call(
249+
"http://localhost:8000/workflow/sxt/visits/cm12345-6/sessions/1/sxt_tilt_series",
250+
json={
251+
"tag": "example",
252+
"source": f"{tmp_path}/cm12345-6/grid1",
253+
"pixel_size": 100.1,
254+
"tilt_offset": 5,
255+
"tilt_series_length": 200,
256+
"txrm": str(
257+
tmp_path / "destination/cm12345-6/grid1/example_-60to60@0.5.txrm"
258+
),
259+
"xrm_reference": str(tmp_path / "destination/cm12345-6/grid1/ref.xrm"),
148260
},
149261
headers={"Authorization": "Bearer "},
150262
)

tests/workflows/sxt/test_process_sxt_tilt_series.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def test_process_new_sxt_tilt_series(
6565
tag="tomogram_tag",
6666
source="/path/to/tomogram_source",
6767
txrm=f"{tmp_path}/cm12345-6/raw/tomogram_tag.txrm",
68+
xrm_reference=f"{tmp_path}/cm12345-6/raw/ref.xrm",
6869
tilt_series_length=5,
6970
pixel_size=100,
7071
tilt_offset=1,
@@ -84,6 +85,7 @@ def test_process_new_sxt_tilt_series(
8485
{
8586
"parameters": {
8687
"txrm_file": f"{tmp_path}/cm12345-6/raw/tomogram_tag.txrm",
88+
"xrm_reference": f"{tmp_path}/cm12345-6/raw/ref.xrm",
8789
"dcid": dc_id,
8890
"appid": app_id,
8991
"stack_file": f"{tmp_path}/cm12345-6/processed/raw/Tomograms/tomogram_tag_stack.mrc",

0 commit comments

Comments
 (0)