Skip to content

Commit 41e0dfa

Browse files
Make full-resolution jpg files of atlases (#640)
For FIB use, full-resolution JPG versions of the atlas are desired. This PR detects valid atlas files and triggers a workflow to save JPG versions of the atlases into the processed folder. --------- Co-authored-by: Eu Pin Tien <[email protected]>
1 parent 120c112 commit 41e0dfa

File tree

9 files changed

+303
-39
lines changed

9 files changed

+303
-39
lines changed

src/murfey/client/analyser.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Type
1616

1717
from murfey.client.context import Context
18+
from murfey.client.contexts.atlas import AtlasContext
1819
from murfey.client.contexts.clem import CLEMContext
1920
from murfey.client.contexts.spa import SPAModularContext
2021
from murfey.client.contexts.spa_metadata import SPAMetadataContext
@@ -135,7 +136,7 @@ def _find_context(self, file_path: Path) -> bool:
135136

136137
# Tomography and SPA workflow checks
137138
if "atlas" in file_path.parts:
138-
self._context = SPAMetadataContext("epu", self._basepath)
139+
self._context = AtlasContext("epu", self._basepath)
139140
return True
140141

141142
if "Metadata" in file_path.parts or file_path.name == "EpuSession.dm":
@@ -266,7 +267,7 @@ def _analyse(self):
266267
)
267268
except Exception as e:
268269
logger.error(f"Exception encountered: {e}")
269-
if "atlas" not in transferred_file.parts:
270+
if not isinstance(self._context, AtlasContext):
270271
if not dc_metadata:
271272
try:
272273
dc_metadata = self._context.gather_metadata(
@@ -308,6 +309,10 @@ def _analyse(self):
308309
)
309310
self.post_transfer(transferred_file)
310311

312+
elif isinstance(self._context, AtlasContext):
313+
logger.debug(f"File {transferred_file.name!r} is part of the atlas")
314+
self.post_transfer(transferred_file)
315+
311316
# Handle files with tomography and SPA context differently
312317
elif not self._extension or self._unseen_xml:
313318
valid_extension = self._find_extension(transferred_file)
@@ -325,36 +330,35 @@ def _analyse(self):
325330
)
326331
except Exception as e:
327332
logger.error(f"Exception encountered: {e}")
328-
if "atlas" not in transferred_file.parts:
329-
if not dc_metadata:
330-
try:
331-
dc_metadata = self._context.gather_metadata(
332-
mdoc_for_reading
333-
or self._xml_file(transferred_file),
334-
environment=self._environment,
335-
)
336-
except KeyError as e:
337-
logger.error(
338-
f"Metadata gathering failed with a key error for key: {e.args[0]}"
339-
)
340-
raise e
341-
if not dc_metadata or not self._force_mdoc_metadata:
342-
mdoc_for_reading = None
343-
self._unseen_xml.append(transferred_file)
344-
if dc_metadata:
345-
self._unseen_xml = []
346-
if dc_metadata.get("file_extension"):
347-
self._extension = dc_metadata["file_extension"]
348-
else:
349-
dc_metadata["file_extension"] = self._extension
350-
dc_metadata["acquisition_software"] = (
351-
self._context._acquisition_software
333+
if not dc_metadata:
334+
try:
335+
dc_metadata = self._context.gather_metadata(
336+
mdoc_for_reading
337+
or self._xml_file(transferred_file),
338+
environment=self._environment,
352339
)
353-
self.notify(
354-
{
355-
"form": dc_metadata,
356-
}
340+
except KeyError as e:
341+
logger.error(
342+
f"Metadata gathering failed with a key error for key: {e.args[0]}"
357343
)
344+
raise e
345+
if not dc_metadata or not self._force_mdoc_metadata:
346+
mdoc_for_reading = None
347+
self._unseen_xml.append(transferred_file)
348+
if dc_metadata:
349+
self._unseen_xml = []
350+
if dc_metadata.get("file_extension"):
351+
self._extension = dc_metadata["file_extension"]
352+
else:
353+
dc_metadata["file_extension"] = self._extension
354+
dc_metadata["acquisition_software"] = (
355+
self._context._acquisition_software
356+
)
357+
self.notify(
358+
{
359+
"form": dc_metadata,
360+
}
361+
)
358362
elif isinstance(
359363
self._context,
360364
(
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import logging
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
import requests
6+
7+
from murfey.client.context import Context
8+
from murfey.client.contexts.spa import _get_source
9+
from murfey.client.contexts.spa_metadata import _atlas_destination
10+
from murfey.client.instance_environment import MurfeyInstanceEnvironment
11+
from murfey.util.api import url_path_for
12+
from murfey.util.client import authorised_requests, capture_post
13+
14+
logger = logging.getLogger("murfey.client.contexts.atlas")
15+
16+
requests.get, requests.post, requests.put, requests.delete = authorised_requests()
17+
18+
19+
class AtlasContext(Context):
20+
def __init__(self, acquisition_software: str, basepath: Path):
21+
super().__init__("Atlas", acquisition_software)
22+
self._basepath = basepath
23+
24+
def post_transfer(
25+
self,
26+
transferred_file: Path,
27+
environment: Optional[MurfeyInstanceEnvironment] = None,
28+
**kwargs,
29+
):
30+
super().post_transfer(
31+
transferred_file=transferred_file,
32+
environment=environment,
33+
**kwargs,
34+
)
35+
36+
if (
37+
environment
38+
and "Atlas_" in transferred_file.stem
39+
and transferred_file.suffix == ".mrc"
40+
):
41+
source = _get_source(transferred_file, environment)
42+
if source:
43+
transferred_atlas_name = _atlas_destination(
44+
environment, source, transferred_file
45+
) / transferred_file.relative_to(source.parent)
46+
capture_post(
47+
f"{str(environment.url.geturl())}{url_path_for('session_control.spa_router', 'make_atlas_jpg', session_id=environment.murfey_session)}",
48+
json={"path": str(transferred_atlas_name)},
49+
)
50+
logger.info(
51+
f"Submitted request to create JPG image of atlas {str(transferred_atlas_name)!r}"
52+
)

src/murfey/client/multigrid_control.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def _rsyncer_stopped(self, source: Path, explicit_stop: bool = False):
292292
requests.delete(remove_url)
293293
else:
294294
stop_url = f"{self.murfey_url}{url_path_for('session_control.router', 'register_stopped_rsyncer', session_id=self.session_id)}"
295-
capture_post(stop_url, json={"source": str(source)})
295+
capture_post(stop_url, json={"path": str(source)})
296296

297297
def _finalise_rsyncer(self, source: Path):
298298
"""
@@ -312,7 +312,7 @@ def _finalise_rsyncer(self, source: Path):
312312
def _restart_rsyncer(self, source: Path):
313313
self.rsync_processes[source].restart()
314314
restarted_url = f"{self.murfey_url}{url_path_for('session_control.router', 'register_restarted_rsyncer', session_id=self.session_id)}"
315-
capture_post(restarted_url, json={"source": str(source)})
315+
capture_post(restarted_url, json={"path": str(source)})
316316

317317
def _request_watcher_stop(self, source: Path):
318318
self._environment.watchers[source]._stopping = True

src/murfey/server/api/session_control.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
SearchMapParameters,
5555
Visit,
5656
)
57+
from murfey.workflows.spa.atlas import atlas_jpg_from_mrc
5758
from murfey.workflows.spa.flush_spa_preprocess import (
5859
register_foil_hole as _register_foil_hole,
5960
)
@@ -262,18 +263,18 @@ def get_rsyncers_for_session(session_id: MurfeySessionID, db=murfey_db):
262263
return rsync_instances.all()
263264

264265

265-
class RsyncerSource(BaseModel):
266-
source: str
266+
class StringOfPathModel(BaseModel):
267+
path: str
267268

268269

269270
@router.post("/sessions/{session_id}/rsyncer_stopped")
270271
def register_stopped_rsyncer(
271-
session_id: int, rsyncer_source: RsyncerSource, db=murfey_db
272+
session_id: int, rsyncer_source: StringOfPathModel, db=murfey_db
272273
):
273274
rsyncer = db.exec(
274275
select(RsyncInstance)
275276
.where(RsyncInstance.session_id == session_id)
276-
.where(RsyncInstance.source == rsyncer_source.source)
277+
.where(RsyncInstance.source == rsyncer_source.path)
277278
).one()
278279
rsyncer.transferring = False
279280
db.add(rsyncer)
@@ -282,12 +283,12 @@ def register_stopped_rsyncer(
282283

283284
@router.post("/sessions/{session_id}/rsyncer_started")
284285
def register_restarted_rsyncer(
285-
session_id: int, rsyncer_source: RsyncerSource, db=murfey_db
286+
session_id: int, rsyncer_source: StringOfPathModel, db=murfey_db
286287
):
287288
rsyncer = db.exec(
288289
select(RsyncInstance)
289290
.where(RsyncInstance.session_id == session_id)
290-
.where(RsyncInstance.source == rsyncer_source.source)
291+
.where(RsyncInstance.source == rsyncer_source.path)
291292
).one()
292293
rsyncer.transferring = True
293294
db.add(rsyncer)
@@ -347,6 +348,19 @@ def get_foil_hole(
347348
return _get_foil_hole(session_id, fh_name, db)
348349

349350

351+
@spa_router.post("/sessions/{session_id}/make_atlas_jpg")
352+
def make_atlas_jpg(
353+
session_id: MurfeySessionID, atlas_mrc: StringOfPathModel, db=murfey_db
354+
):
355+
logger.debug(
356+
f"Received request to create JPG image of atlas {sanitise(atlas_mrc.path)!r}"
357+
)
358+
session = db.exec(select(Session).where(Session.id == session_id)).one()
359+
return atlas_jpg_from_mrc(
360+
session.instrument_name, session.visit, Path(atlas_mrc.path)
361+
)
362+
363+
350364
@spa_router.post("/sessions/{session_id}/grid_square/{gsid}")
351365
def register_grid_square(
352366
session_id: MurfeySessionID,

src/murfey/util/route_manifest.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,11 @@ murfey.server.api.session_control.spa_router:
814814
type: int
815815
methods:
816816
- GET
817+
- path: /session_control/spa/sessions/{session_id}/make_atlas_jpg
818+
function: make_atlas_jpg
819+
path_params: []
820+
methods:
821+
- POST
817822
- path: /session_control/spa/sessions/{session_id}/grid_square/{gsid}
818823
function: register_grid_square
819824
path_params:

src/murfey/workflows/spa/atlas.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
from pathlib import Path
3+
4+
import mrcfile
5+
import PIL.Image
6+
from werkzeug.utils import secure_filename
7+
8+
from murfey.util import sanitise
9+
from murfey.util.config import get_machine_config
10+
11+
logger = logging.getLogger("murfey.workflows.spa.atlas")
12+
13+
14+
def atlas_jpg_from_mrc(instrument_name: str, visit_name: str, atlas_mrc: Path):
15+
logger.debug(
16+
f"Starting workflow to create JPG image of atlas {sanitise(str(atlas_mrc))!r}"
17+
)
18+
with mrcfile.open(atlas_mrc) as mrc:
19+
data = mrc.data
20+
21+
machine_config = get_machine_config(instrument_name=instrument_name)[
22+
instrument_name
23+
]
24+
25+
parts = [secure_filename(p) for p in atlas_mrc.parts]
26+
visit_idx = parts.index(visit_name)
27+
core = Path("/".join(parts[: visit_idx + 1]))
28+
sample_id = "Sample"
29+
for p in parts:
30+
if "Sample" in p:
31+
sample_id = p
32+
break
33+
atlas_jpg_file = (
34+
core
35+
/ machine_config.processed_directory_name
36+
/ "atlas"
37+
/ secure_filename(f"{sample_id}_{atlas_mrc.stem}_fullres.jpg")
38+
)
39+
atlas_jpg_file.parent.mkdir(parents=True, exist_ok=True)
40+
41+
im = PIL.Image.fromarray(data)
42+
im.convert(mode="L").save(atlas_jpg_file)
43+
logger.debug(f"JPG image of atlas saved as {str(atlas_jpg_file)!r}")

tests/client/test_analyser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from murfey.client.analyser import Analyser
6+
from murfey.client.contexts.atlas import AtlasContext
67
from murfey.client.contexts.clem import CLEMContext
78
from murfey.client.contexts.spa import SPAModularContext
89
from murfey.client.contexts.spa_metadata import SPAMetadataContext
@@ -28,7 +29,7 @@
2829
["visit/FoilHole_01234_fractions.tiff", SPAModularContext],
2930
["visit/FoilHole_01234_EER.eer", SPAModularContext],
3031
# SPA metadata
31-
["atlas/atlas.mrc", SPAMetadataContext],
32+
["atlas/atlas.mrc", AtlasContext],
3233
["visit/EpuSession.dm", SPAMetadataContext],
3334
["visit/Metadata/GridSquare.dm", SPAMetadataContext],
3435
# CLEM LIF file
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from pathlib import Path
2+
from unittest import mock
3+
from unittest.mock import MagicMock
4+
5+
from fastapi import FastAPI
6+
from fastapi.testclient import TestClient
7+
from pytest_mock import MockerFixture
8+
9+
from murfey.server.api.auth import (
10+
validate_instrument_server_session_access,
11+
validate_instrument_token,
12+
)
13+
from murfey.server.api.session_control import spa_router
14+
from murfey.server.murfey_db import murfey_db_session
15+
from murfey.util.api import url_path_for
16+
17+
18+
def test_make_atlas_jpg(mocker: MockerFixture, tmp_path: Path):
19+
# Set up the objects to mock
20+
instrument_name = "test"
21+
visit_name = "test_visit"
22+
session_id = 1
23+
24+
# Override the database session generator
25+
mock_session = MagicMock()
26+
mock_session.instrument_name = instrument_name
27+
mock_session.visit = visit_name
28+
mock_query_result = MagicMock()
29+
mock_query_result.one.return_value = mock_session
30+
mock_db_session = MagicMock()
31+
mock_db_session.exec.return_value = mock_query_result
32+
33+
def mock_get_db_session():
34+
yield mock_db_session
35+
36+
# Mock the instrument server tokens dictionary
37+
mock_tokens = mocker.patch(
38+
"murfey.server.api.instrument.instrument_server_tokens",
39+
{session_id: {"access_token": mock.sentinel}},
40+
)
41+
42+
# Mock the called workflow function
43+
mock_atlas_jpg = mocker.patch(
44+
"murfey.server.api.session_control.atlas_jpg_from_mrc",
45+
return_value=None,
46+
)
47+
48+
# Set up the test file
49+
image_dir = tmp_path / instrument_name / "data" / visit_name / "Atlas"
50+
image_dir.mkdir(parents=True, exist_ok=True)
51+
test_file = image_dir / "Atlas1.mrc"
52+
53+
# Set up the backend server
54+
backend_app = FastAPI()
55+
56+
# Override validation and database dependencies
57+
backend_app.dependency_overrides[validate_instrument_token] = lambda: None
58+
backend_app.dependency_overrides[validate_instrument_server_session_access] = (
59+
lambda: session_id
60+
)
61+
backend_app.dependency_overrides[murfey_db_session] = mock_get_db_session
62+
backend_app.include_router(spa_router)
63+
backend_server = TestClient(backend_app)
64+
65+
atlas_jpg_url = url_path_for(
66+
"api.session_control.spa_router", "make_atlas_jpg", session_id=session_id
67+
)
68+
response = backend_server.post(
69+
atlas_jpg_url,
70+
json={"path": str(test_file)},
71+
headers={"Authorization": f"Bearer {mock_tokens[session_id]['access_token']}"},
72+
)
73+
74+
# Check that the expected calls were made
75+
mock_atlas_jpg.assert_called_once_with(instrument_name, visit_name, test_file)
76+
assert response.status_code == 200

0 commit comments

Comments
 (0)