Skip to content

Commit 5adef4c

Browse files
authored
Add starting point for SmartEM integration (#748)
If installed and configured then starting a multigrid controller also adds an Acquisition in SmartEM and triggers the acquisition creation event. The atlas context is expanded to behave differently under different acquisition software. In the case that SerialEM is being used it will "register" atlases that it sees, meaning they will be registered with SmartEM if installed and configured. The SerialEM context is not accessed by discovery at the moment, as the others are, but is explicitly set in the API calls that trigger the multigrid controller setup. Note this means that this context will be used for all atlas analysers spawned by that controller.
1 parent 833e9b6 commit 5adef4c

File tree

13 files changed

+153
-1
lines changed

13 files changed

+153
-1
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ server = [
6868
"stomp-py>8.1.1", # 8.1.1 (released 2024-04-06) doesn't work with our project
6969
"zocalo>=1",
7070
]
71+
smartem = [
72+
"smartem-decisions[backend]",
73+
]
7174
[project.urls]
7275
Bug-Tracker = "https://github.com/DiamondLightSource/python-murfey/issues"
7376
Documentation = "https://github.com/DiamondLightSource/python-murfey"

src/murfey/client/analyser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
environment: MurfeyInstanceEnvironment | None = None,
4040
force_mdoc_metadata: bool = False,
4141
limited: bool = False,
42+
serialem: bool = False,
4243
):
4344
super().__init__()
4445
self._basepath = basepath_local.absolute()
@@ -52,6 +53,7 @@ def __init__(
5253
self._environment = environment
5354
self._force_mdoc_metadata = force_mdoc_metadata
5455
self._token = token
56+
self._serialem = serialem
5557
self.parameters_model: (
5658
Type[ProcessingParametersSPA] | Type[ProcessingParametersTomo] | None
5759
) = None
@@ -138,7 +140,9 @@ def _find_context(self, file_path: Path) -> bool:
138140

139141
# Tomography and SPA workflow checks
140142
if "atlas" in file_path.parts:
141-
self._context = AtlasContext("epu", self._basepath, self._token)
143+
self._context = AtlasContext(
144+
"serialem" if self._serialem else "epu", self._basepath, self._token
145+
)
142146
return True
143147

144148
if "Metadata" in file_path.parts or file_path.name == "EpuSession.dm":

src/murfey/client/contexts/atlas.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,40 @@ def post_transfer(
2828
environment=environment,
2929
**kwargs,
3030
)
31+
if self._acquisition_software == "serialem":
32+
self.post_transfer_serialem(
33+
transferred_file, environment=environment, **kwargs
34+
)
35+
else:
36+
self.post_transfer_epu(transferred_file, environment=environment, **kwargs)
3137

38+
def post_transfer_serialem(
39+
self,
40+
transferred_file: Path,
41+
environment: Optional[MurfeyInstanceEnvironment] = None,
42+
**kwargs,
43+
):
44+
if environment and transferred_file.suffix == ".mrc":
45+
source = _get_source(transferred_file, environment)
46+
if source:
47+
capture_post(
48+
base_url=str(environment.url.geturl()),
49+
router_name="session_control.spa_router",
50+
function_name="register_atlas",
51+
token=self._token,
52+
session_id=environment.murfey_session,
53+
data={
54+
"name": transferred_file.stem,
55+
"acquisition_uuid": environment.acquisition_uuid,
56+
},
57+
)
58+
59+
def post_transfer_epu(
60+
self,
61+
transferred_file: Path,
62+
environment: Optional[MurfeyInstanceEnvironment] = None,
63+
**kwargs,
64+
):
3265
if (
3366
environment
3467
and "Atlas_" in transferred_file.stem

src/murfey/client/instance_environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class MurfeyInstanceEnvironment(BaseModel):
5353
murfey_session: Optional[int] = None
5454
samples: Dict[Path, SampleInfo] = {}
5555
rsync_url: str = ""
56+
acquisition_uuid: Optional[str] = None
5657

5758
model_config = ConfigDict(arbitrary_types_allowed=True)
5859

src/murfey/client/multigrid_control.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class MultigridController:
4848
analysers: Dict[Path, Analyser] = field(default_factory=lambda: {})
4949
data_collection_parameters: dict = field(default_factory=lambda: {})
5050
token: str = ""
51+
serialem: bool = False
52+
acquisition_uuid: Optional[str] = None
5153
_machine_config: dict = field(default_factory=lambda: {})
5254
visit_end_time: Optional[datetime] = None
5355

@@ -72,6 +74,7 @@ def __post_init__(self):
7274
symmetry=self.data_collection_parameters.get("symmetry"),
7375
eer_fractionation=self.data_collection_parameters.get("eer_fractionation"),
7476
instrument_name=self.instrument_name,
77+
acquisition_uuid=self.acquisition_uuid,
7578
)
7679
self._machine_config = get_machine_config_client(
7780
str(self._environment.url.geturl()),
@@ -449,6 +452,7 @@ def rsync_result(update: RSyncerUpdate):
449452
environment=self._environment if not self.dummy_dc else None,
450453
force_mdoc_metadata=self.force_mdoc_metadata,
451454
limited=limited,
455+
serialem=self.serialem,
452456
)
453457
self.analysers[source].subscribe(self._start_dc)
454458
self.analysers[source].start()

src/murfey/instrument_server/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def setup_multigrid_watcher(
179179
data_collection_parameters=data_collection_parameters.get(label, {}),
180180
rsync_restarts=watcher_spec.rsync_restarts,
181181
visit_end_time=watcher_spec.visit_end_time,
182+
acquisition_uuid=watcher_spec.acquisition_uuid,
183+
serialem=watcher_spec.serialem,
182184
)
183185
# Make child directories, if specified
184186
watcher_spec.source.mkdir(exist_ok=True)

src/murfey/server/api/instrument.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
from sqlmodel import select
1414
from werkzeug.utils import secure_filename
1515

16+
try:
17+
from smartem_backend.api_client import EntityConverter
18+
from smartem_common.schemas import AcquisitionData
19+
20+
SMARTEM_ACTIVE = True
21+
except ImportError:
22+
SMARTEM_ACTIVE = False
23+
1624
import murfey.server.prometheus as prom
1725
from murfey.server.api.auth import (
1826
MurfeyInstrumentNameFrontend as MurfeyInstrumentName,
@@ -75,6 +83,7 @@ async def activate_instrument_server_for_session(
7583
success = response.status == 200
7684
instrument_server_token = await response.json()
7785
instrument_server_tokens[session_id] = instrument_server_token
86+
7887
if success:
7988
log.info("Handshake successful")
8089
else:
@@ -147,6 +156,25 @@ async def setup_multigrid_watcher(
147156
session = db.exec(select(Session).where(Session.id == session_id)).one()
148157
visit = session.visit
149158
async with aiohttp.ClientSession() as clientsession:
159+
acquisition_uuid = None
160+
if SMARTEM_ACTIVE and machine_config.smartem_api_url:
161+
log.info("registering an acquisition with smartem")
162+
try:
163+
acquisition_data = EntityConverter.acquisition_to_request(
164+
AcquisitionData(name=visit)
165+
)
166+
async with clientsession.post(
167+
f"{machine_config.smartem_api_url}/acquisitions",
168+
json=acquisition_data.model_dump(),
169+
) as response:
170+
acquisition_response_data = await response.json()
171+
acquisition_uuid = acquisition_response_data["uuid"]
172+
except Exception:
173+
log.warning(
174+
"failed to register acquisition with smartem", exc_info=True
175+
)
176+
else:
177+
log.info("smartem not configured")
150178
async with clientsession.post(
151179
f"{machine_config.instrument_server_url}{url_path_for('api.router', 'setup_multigrid_watcher', session_id=session_id)}",
152180
json={
@@ -161,6 +189,8 @@ async def setup_multigrid_watcher(
161189
"visit_end_time": (
162190
str(session.visit_end_time) if session.visit_end_time else None
163191
),
192+
"acquisition_uuid": acquisition_uuid,
193+
"serialem": watcher_spec.serialem,
164194
},
165195
headers={
166196
"Authorization": f"Bearer {instrument_server_tokens[session_id]['access_token']}"

src/murfey/server/api/session_control.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
from sqlalchemy import func
1111
from sqlmodel import select
1212

13+
try:
14+
from smartem_backend.api_client import SmartEMAPIClient
15+
from smartem_common.schemas import AtlasData
16+
17+
SMARTEM_ACTIVE = True
18+
except ImportError:
19+
SMARTEM_ACTIVE = False
20+
1321
import murfey.server.prometheus as prom
1422
from murfey.server import _transport_object
1523
from murfey.server.api.auth import (
@@ -349,6 +357,49 @@ def get_foil_hole(
349357
return _get_foil_hole(session_id, fh_name, db)
350358

351359

360+
class AtlasRegistration(BaseModel):
361+
name: str
362+
acqusition_uuid: str
363+
364+
365+
@spa_router.post("/sessions/{session_id}/register_atlas")
366+
def register_atlas(
367+
session_id: MurfeySessionID,
368+
atlas_registration_data: AtlasRegistration,
369+
db=murfey_db,
370+
):
371+
if SMARTEM_ACTIVE:
372+
session = db.exec(select(Session).where(Session.id == session_id)).one()
373+
machine_config = get_machine_config(session.instrument_name)[
374+
session.instrument_name
375+
]
376+
if machine_config.smartem_api_url:
377+
smartem_client = SmartEMAPIClient(
378+
base_url=machine_config.smartem_api_url, logger=logger
379+
)
380+
possible_grids = smartem_client.get_acquisition_grids(
381+
atlas_registration_data.acqusition_uuid
382+
)
383+
grid_uuid = None
384+
for grid in possible_grids:
385+
if grid.name == atlas_registration_data.name.replace("_atlas", ""):
386+
grid_uuid = grid.uuid
387+
break
388+
if grid_uuid is not None:
389+
atlas_data = AtlasData(
390+
id=atlas_registration_data.name,
391+
acquisition_data=datetime.now(),
392+
storage_folder="",
393+
name=atlas_registration_data.name,
394+
tiles=[],
395+
gridsquare_positions=None,
396+
grid_uuid=grid_uuid,
397+
)
398+
smartem_client.create_grid_atlas(atlas_data)
399+
else:
400+
logger.info("smartem deactivated so did not register atlas")
401+
402+
352403
@spa_router.post("/sessions/{session_id}/make_atlas_jpg")
353404
def make_atlas_jpg(
354405
session_id: MurfeySessionID, atlas_mrc: StringOfPathModel, db=murfey_db

src/murfey/server/api/session_info.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ def machine_info_by_instrument(
8181
return get_machine_config(instrument_name)[instrument_name]
8282

8383

84+
@router.get("/instruments/{instrument_name}/smartem")
85+
def check_smartem_availability(instrument_name: str):
86+
machine_config = get_machine_config(instrument_name)[instrument_name]
87+
return {"available": bool(machine_config.smartem_api_url)}
88+
89+
8490
@router.get("/instruments/{instrument_name}/visits_raw", response_model=List[Visit])
8591
def get_current_visits(instrument_name: MurfeyInstrumentName, db=ispyb_db):
8692
logger.debug(

src/murfey/util/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class MachineConfig(BaseModel): # type: ignore
106106
murfey_url: str = "http://localhost:8000"
107107
frontend_url: str = "http://localhost:3000"
108108
instrument_server_url: str = "http://localhost:8001"
109+
smartem_api_url: str = ""
109110

110111
# Messaging queues
111112
failure_queue: str = ""

0 commit comments

Comments
 (0)