Skip to content

Commit 16fd649

Browse files
authored
SmartEM integration for the EPU context (#772)
Add a smartem grid when a data collection group is made Add smartem gridsquares alongside Murfey squares, and the same for foilholes Once all squares have been added register the grid in smartem to trigger quality predictions Similar for foilholes on each square Add some post processing stage hooks to pass the correct measurement information to smartem Not fully tested for robustness against different metadata file sequences but designed to not interfere with current capabilities
1 parent 24e6c5f commit 16fd649

File tree

15 files changed

+469
-3
lines changed

15 files changed

+469
-3
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ TomographyMetadataContext = "murfey.client.contexts.tomo_metadata:TomographyMeta
119119
"picked_particles" = "murfey.workflows.spa.picking:particles_picked"
120120
"picked_tomogram" = "murfey.workflows.tomo.picking:picked_tomogram"
121121
"processing_job" = "murfey.workflows.register_processing_job:run"
122+
"spa.ctf_estimated" = "murfey.workflows.spa.ctf_estimation:ctf_estimated"
122123
"spa.flush_spa_preprocess" = "murfey.workflows.spa.flush_spa_preprocess:flush_spa_preprocess"
124+
"spa.motion_corrected" = "murfey.workflows.spa.motion_correction:motion_corrected"
123125

124126
[tool.setuptools]
125127
package-dir = {"" = "src"}

src/murfey/client/analyser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def _find_context(self, file_path: Path) -> bool:
303303

304304
# Files starting with "Position" belong to the standard tomography workflow
305305
# NOTE: not completely reliable, mdocs can be in tomography metadata as well
306-
if (
306+
if not self._serialem and (
307307
split_file_stem[0] == "Position"
308308
or "[" in file_path.name
309309
or split_file_stem[-1] in ["Fractions", "fractions", "EER"]
@@ -378,7 +378,7 @@ def _analyse(self):
378378
self.post_transfer(transferred_file)
379379
else:
380380
dc_metadata = {}
381-
if (
381+
if not self._serialem and (
382382
self._force_mdoc_metadata
383383
and transferred_file.suffix == ".mdoc"
384384
or mdoc_for_reading

src/murfey/client/contexts/atlas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def post_transfer_serialem(
6060
data={
6161
"name": transferred_file.stem,
6262
"acquisition_uuid": environment.acquisition_uuid,
63+
"storage_folder": str(source),
6364
},
6465
)
6566

@@ -134,6 +135,8 @@ def post_transfer_epu(
134135
"atlas": str(transferred_atlas_jpg).replace("//", "/"),
135136
"sample": sample,
136137
"atlas_pixel_size": atlas_pixel_size,
138+
"create_smartem_grid": bool(environment.acquisition_uuid),
139+
"acquisition_uuid": environment.acquisition_uuid,
137140
}
138141
capture_post(
139142
base_url=str(environment.url.geturl()),

src/murfey/client/contexts/spa_metadata.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,19 @@ def post_transfer(
152152
"angle": pos_data[6],
153153
},
154154
)
155+
if pos_data:
156+
capture_post(
157+
base_url=str(environment.url.geturl()),
158+
router_name="session_control.spa_router",
159+
function_name="register_atlas",
160+
token=self._token,
161+
session_id=environment.murfey_session,
162+
data={
163+
"name": f"{environment.visit}-sample-{environment.samples[images_disc].sample}",
164+
"acquisition_uuid": environment.acquisition_uuid,
165+
"register_grid": True,
166+
},
167+
)
155168

156169
elif (
157170
transferred_file.suffix == ".dm"
@@ -253,3 +266,13 @@ def post_transfer(
253266
"image": fh_data.image,
254267
},
255268
)
269+
if fh_positions:
270+
capture_post(
271+
base_url=str(environment.url.geturl()),
272+
router_name="session_control.spa_router",
273+
function_name="register_square",
274+
token=self._token,
275+
session_id=environment.murfey_session,
276+
gsid=gs_name,
277+
data={"tag": visitless_source},
278+
)

src/murfey/server/api/session_control.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ def get_foil_hole(
360360
class AtlasRegistration(BaseModel):
361361
name: str
362362
acquisition_uuid: str
363+
storage_folder: str = ""
364+
register_grid: bool = False
363365

364366

365367
@spa_router.post("/sessions/{session_id}/register_atlas")
@@ -389,17 +391,51 @@ def register_atlas(
389391
atlas_data = AtlasData(
390392
id=atlas_registration_data.name,
391393
acquisition_date=datetime.now(),
392-
storage_folder="",
394+
storage_folder=atlas_registration_data.storage_folder,
393395
name=atlas_registration_data.name,
394396
tiles=[],
395397
gridsquare_positions=None,
396398
grid_uuid=grid_uuid,
397399
)
398400
smartem_client.create_grid_atlas(atlas_data)
401+
if atlas_registration_data.register_grid:
402+
smartem_client.grid_registered(grid_uuid)
399403
else:
400404
logger.info("smartem deactivated so did not register atlas")
401405

402406

407+
class SquareRegistration(BaseModel):
408+
tag: str
409+
410+
411+
@spa_router.post("/sessions/{session_id}/register_square/{gsid}")
412+
def register_square(
413+
session_id: MurfeySessionID,
414+
gsid: int,
415+
square_registration_data: SquareRegistration,
416+
db=murfey_db,
417+
):
418+
if SMARTEM_ACTIVE:
419+
gs = db.exec(
420+
select(GridSquare)
421+
.where(GridSquare.name == gsid)
422+
.where(GridSquare.tag == square_registration_data.tag)
423+
.where(GridSquare.session_id == session_id)
424+
).one_or_none()
425+
if gs and gs.smartem_uuid:
426+
session = db.exec(select(Session).where(Session.id == session_id)).one()
427+
machine_config = get_machine_config(session.instrument_name)[
428+
session.instrument_name
429+
]
430+
if machine_config.smartem_api_url:
431+
smartem_client = SmartEMAPIClient(
432+
base_url=machine_config.smartem_api_url, logger=logger
433+
)
434+
smartem_client.gridsquare_registered(gs.smartem_uuid)
435+
else:
436+
logger.info("smartem deactivated so did not register square")
437+
438+
403439
@spa_router.post("/sessions/{session_id}/make_atlas_jpg")
404440
def make_atlas_jpg(
405441
session_id: MurfeySessionID, atlas_mrc: StringOfPathModel, db=murfey_db

src/murfey/server/api/workflow.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@
2424
except ImportError:
2525
Image = None
2626

27+
try:
28+
from smartem_backend.api_client import SmartEMAPIClient
29+
from smartem_common.schemas import (
30+
AcquisitionData as SmartEMAcquisitionData,
31+
GridData as SmartEMGridData,
32+
MicrographData as SmartEMMicrographData,
33+
MicrographManifest as SmartEMMicrographManifest,
34+
)
35+
36+
SMARTEM_ACTIVE = True
37+
except ImportError:
38+
SMARTEM_ACTIVE = False
39+
2740
import murfey.server.prometheus as prom
2841
from murfey.server import _transport_object
2942
from murfey.server.api.auth import (
@@ -78,6 +91,7 @@
7891

7992
logger = getLogger("murfey.server.api.workflow")
8093

94+
8195
router = APIRouter(
8296
prefix="/workflow",
8397
dependencies=[Depends(validate_instrument_token)],
@@ -92,6 +106,8 @@ class DCGroupParameters(BaseModel):
92106
atlas: str = ""
93107
sample: Optional[int] = None
94108
atlas_pixel_size: float = 0
109+
create_smartem_grid: bool = False
110+
acquisition_uuid: Optional[str] = None
95111

96112

97113
@router.post(
@@ -110,6 +126,35 @@ def register_dc_group(
110126
db.exec(select(Session).where(Session.id == session_id)).one().instrument_name
111127
)
112128
logger.info(f"Registering data collection group on microscope {instrument_name}")
129+
smartem_grid_uuid = None
130+
if (
131+
dcg_params.create_smartem_grid
132+
and SMARTEM_ACTIVE
133+
and dcg_params.acquisition_uuid
134+
):
135+
machine_config = get_machine_config(instrument_name=instrument_name)[
136+
instrument_name
137+
]
138+
if machine_config.smartem_api_url:
139+
try:
140+
smartem_client = SmartEMAPIClient(
141+
base_url=machine_config.smartem_api_url, logger=logger
142+
)
143+
grid_data = SmartEMGridData(
144+
data_dir=Path(dcg_params.tag),
145+
atlas_dir=Path(dcg_params.atlas) if dcg_params.atlas else None,
146+
acquisition_data=SmartEMAcquisitionData(
147+
uuid=dcg_params.acquisition_uuid,
148+
name=f"{visit_name}-sample-{dcg_params.sample}"
149+
if dcg_params.sample
150+
else f"{visit_name}-sample-unknown",
151+
),
152+
)
153+
smartem_grid_uuid = smartem_client.create_acquisition_grid(
154+
grid_data
155+
).uuid
156+
except Exception:
157+
logger.warning("Failed to register SmartEM grid", exc_info=True)
113158
if (
114159
dcg_murfey := db.exec(
115160
select(DataCollectionGroup)
@@ -135,6 +180,8 @@ def register_dc_group(
135180
dcg_instance.atlas_pixel_size = (
136181
dcg_params.atlas_pixel_size or dcg_instance.atlas_pixel_size
137182
)
183+
if smartem_grid_uuid:
184+
dcg_instance.smartem_grid_uuid = smartem_grid_uuid
138185

139186
if _transport_object:
140187
if dcg_instance.atlas_id is not None:
@@ -217,6 +264,11 @@ def register_dc_group(
217264
"proposal_code": ispyb_proposal_code,
218265
"proposal_number": ispyb_proposal_number,
219266
"visit_number": ispyb_visit_number,
267+
**(
268+
{"smartem_grid_uuid": smartem_grid_uuid}
269+
if smartem_grid_uuid
270+
else {}
271+
),
220272
},
221273
)
222274
return dcg_params
@@ -491,6 +543,58 @@ async def request_spa_preprocessing(
491543
)
492544
db.add(movie)
493545
db.commit()
546+
547+
if (
548+
SMARTEM_ACTIVE
549+
and machine_config.smartem_api_url
550+
and foil_hole_id is not None
551+
):
552+
try:
553+
fh_with_gs = db.exec(
554+
select(FoilHole, GridSquare)
555+
.where(FoilHole.id == foil_hole_id)
556+
.where(GridSquare.id == FoilHole.grid_square_id)
557+
).one_or_none()
558+
if fh_with_gs is not None:
559+
fh, gs = fh_with_gs
560+
if fh.smartem_uuid:
561+
smartem_client = SmartEMAPIClient(
562+
base_url=machine_config.smartem_api_url, logger=logger
563+
)
564+
movie_path = Path(proc_file.path)
565+
micrograph_manifest = SmartEMMicrographManifest(
566+
unique_id=movie_path.stem,
567+
acquisition_datetime=datetime.now(),
568+
defocus=None,
569+
detector_name="",
570+
energy_filter=True,
571+
phase_plate=False,
572+
image_size_x=None,
573+
image_size_y=None,
574+
binning_x=1,
575+
binning_y=1,
576+
)
577+
micrograph_data = SmartEMMicrographData(
578+
id=movie_path.stem,
579+
gridsquare_id=str(gs.name),
580+
foilhole_uuid=fh.smartem_uuid,
581+
foilhole_id=str(fh.name),
582+
location_id=str(murfey_ids[0]),
583+
high_res_path=movie_path,
584+
manifest_file=movie_path,
585+
manifest=micrograph_manifest,
586+
)
587+
response = smartem_client.create_foilhole_micrograph(
588+
micrograph_data
589+
)
590+
movie.smartem_uuid = response.uuid
591+
db.add(movie)
592+
db.commit()
593+
except Exception:
594+
logger.warning(
595+
"Failed to register micrograph with smartem", exc_info=True
596+
)
597+
494598
db.close()
495599

496600
if not mrc_out.parent.exists():

src/murfey/util/db.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class DataCollectionGroup(SQLModel, table=True): # type: ignore
211211
atlas_pixel_size: Optional[float] = None
212212
atlas: str = ""
213213
sample: Optional[int] = None
214+
smartem_grid_uuid: Optional[str] = None
214215
session: Optional["Session"] = Relationship(back_populates="data_collection_groups")
215216
data_collections: List["DataCollection"] = Relationship(
216217
back_populates="data_collection_group",
@@ -500,6 +501,7 @@ class GridSquare(SQLModel, table=True): # type: ignore
500501
width: Optional[int] = None
501502
angle: Optional[float] = None
502503
quality_indicator: Optional[float] = None
504+
smartem_uuid: Optional[str] = None
503505
data_collection_group: Optional["DataCollectionGroup"] = Relationship(
504506
back_populates="grid_squares"
505507
)
@@ -533,6 +535,7 @@ class FoilHole(SQLModel, table=True): # type: ignore
533535
pixel_location_y: Optional[int] = None
534536
diameter: Optional[int] = None
535537
quality_indicator: Optional[float] = None
538+
smartem_uuid: Optional[str] = None
536539

537540

538541
class SearchMap(SQLModel, table=True): # type: ignore
@@ -612,6 +615,7 @@ class Movie(SQLModel, table=True): # type: ignore
612615
fluence: Optional[float] = None
613616
numberOfFrames: Optional[int] = None
614617
templateLabel: Optional[str] = None
618+
smartem_uuid: Optional[str] = None
615619
murfey_ledger: Optional[MurfeyLedger] = Relationship(back_populates="movies")
616620
data_collection: Optional["DataCollection"] = Relationship(back_populates="movies")
617621
foil_hole: Optional[FoilHole] = Relationship(back_populates="movies")

src/murfey/util/route_manifest.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,15 @@ murfey.server.api.session_control.spa_router:
947947
type: int
948948
methods:
949949
- POST
950+
- path: /session_control/spa/sessions/{session_id}/register_square/{gsid}
951+
function: register_square
952+
path_params:
953+
- name: gsid
954+
type: int
955+
- name: session_id
956+
type: int
957+
methods:
958+
- POST
950959
- path: /session_control/spa/sessions/{session_id}/make_atlas_jpg
951960
function: make_atlas_jpg
952961
path_params:

src/murfey/workflows/register_data_collection_group.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def run(message: dict, murfey_db: SQLModelSession) -> dict[str, bool]:
4141
murfey_dcg = DataCollectionGroup(
4242
session_id=message["session_id"],
4343
tag=message.get("tag"),
44+
smartem_grid_uuid=message.get("smartem_grid_uuid"),
4445
)
4546
dcgid = murfey_dcg.id
4647
else:
@@ -86,6 +87,7 @@ def run(message: dict, murfey_db: SQLModelSession) -> dict[str, bool]:
8687
sample=message.get("sample"),
8788
session_id=message["session_id"],
8889
tag=message.get("tag"),
90+
smartem_grid_uuid=message.get("smartem_grid_uuid"),
8991
)
9092
murfey_db.add(murfey_dcg)
9193
murfey_db.commit()

0 commit comments

Comments
 (0)