11from __future__ import annotations
22
33import logging
4+ import threading
45from datetime import datetime
56from pathlib import Path
67from typing import Dict , List , NamedTuple , Optional
8+ from xml .etree import ElementTree as ET
79
810import xmltodict
911
1315
1416logger = logging .getLogger ("murfey.client.contexts.fib" )
1517
18+ lock = threading .Lock ()
19+
1620
1721class Lamella (NamedTuple ):
1822 name : str
@@ -25,18 +29,98 @@ class MillingProgress(NamedTuple):
2529 timestamp : float
2630
2731
32+ class ElectronSnapshotMetadata (NamedTuple ):
33+ image_dir : str # Partial path from EMproject.emxml parent to the image
34+ status : str
35+ x_len : float | None
36+ y_len : float | None
37+ z_len : float | None
38+ x_center : float | None
39+ y_center : float | None
40+ z_center : float | None
41+ extent : tuple [float , float , float , float ] | None
42+ rotation_angle : float | None
43+
44+
2845def _number_from_name (name : str ) -> int :
2946 return int (
3047 name .strip ().replace ("Lamella" , "" ).replace ("(" , "" ).replace (")" , "" ) or 1
3148 )
3249
3350
51+ def _parse_electron_snapshot_metadata (xml_file : Path ):
52+ metadata_dict = {}
53+ root = ET .parse (xml_file ).getroot ()
54+ datasets = root .findall (".//Datasets/Dataset" )
55+ for dataset in datasets :
56+ # Extract all string-based values
57+ name , image_dir , status = [
58+ node .text
59+ if ((node := dataset .find (node_path )) is not None and node .text is not None )
60+ else ""
61+ for node_path in (
62+ ".//Name" ,
63+ ".//FinalImages" ,
64+ ".//Status" ,
65+ )
66+ ]
67+
68+ # Extract all float values
69+ cx , cy , cz , x_len , y_len , z_len , rotation_angle = [
70+ float (node .text )
71+ if ((node := dataset .find (node_path )) is not None and node .text is not None )
72+ else None
73+ for node_path in (
74+ ".//BoxCenter/CenterX" ,
75+ ".//BoxCenter/CenterY" ,
76+ ".//BoxCenter/CenterZ" ,
77+ ".//BoxSize/SizeX" ,
78+ ".//BoxSize/SizeY" ,
79+ ".//BoxSize/SizeZ" ,
80+ ".//RotationAngle" ,
81+ )
82+ ]
83+
84+ # Calculate the extent of the image
85+ extent = None
86+ if (
87+ cx is not None
88+ and cy is not None
89+ and x_len is not None
90+ and y_len is not None
91+ ):
92+ extent = (
93+ x_len - (cx / 2 ),
94+ x_len + (cx / 2 ),
95+ y_len - (cy / 2 ),
96+ y_len - (cy / 2 ),
97+ )
98+
99+ # Append metadata for current site to dict
100+ metadata_dict [name ] = ElectronSnapshotMetadata (
101+ status = status ,
102+ image_dir = image_dir ,
103+ x_len = x_len ,
104+ y_len = y_len ,
105+ z_len = z_len ,
106+ x_center = cx ,
107+ y_center = cy ,
108+ z_center = cz ,
109+ extent = extent ,
110+ rotation_angle = rotation_angle ,
111+ )
112+ return metadata_dict
113+
114+
34115class FIBContext (Context ):
35116 def __init__ (self , acquisition_software : str , basepath : Path , token : str ):
36117 super ().__init__ ("FIB" , acquisition_software , token )
37118 self ._basepath = basepath
38119 self ._milling : Dict [int , List [MillingProgress ]] = {}
39120 self ._lamellae : Dict [int , Lamella ] = {}
121+ self ._electron_snapshots : Dict [str , Path ] = {}
122+ self ._electron_snapshot_metadata : Dict [str , ElectronSnapshotMetadata ] = {}
123+ self ._electron_snapshots_submitted : set [str ] = set ()
40124
41125 def post_transfer (
42126 self ,
@@ -130,7 +214,50 @@ def post_transfer(
130214 # Maps
131215 # -----------------------------------------------------------------------------
132216 elif self ._acquisition_software == "maps" :
133- pass
217+ # Electron snapshot metadata file
218+ if transferred_file .name == "EMproject.emxml" :
219+ # Extract all "Electron Snapshot" metadata and store it
220+ self ._electron_snapshot_metadata = _parse_electron_snapshot_metadata (
221+ transferred_file
222+ )
223+ # If dataset hasn't been transferred, register it
224+ for dataset_name in list (self ._electron_snapshot_metadata .keys ()):
225+ if dataset_name not in self ._electron_snapshots_submitted :
226+ if dataset_name in self ._electron_snapshots :
227+ logger .info (f"Registering { dataset_name !r} " )
228+
229+ ## Workflow to trigger goes here
230+
231+ # Clear old entry after triggering workflow
232+ self ._electron_snapshots_submitted .add (dataset_name )
233+ with lock :
234+ self ._electron_snapshots .pop (dataset_name , None )
235+ self ._electron_snapshot_metadata .pop (dataset_name , None )
236+ else :
237+ logger .debug (f"Waiting for image for { dataset_name } " )
238+ # Electron snapshot image
239+ elif (
240+ "Electron Snapshot" in transferred_file .name
241+ and transferred_file .suffix in (".tif" , ".tiff" )
242+ ):
243+ # Store file in Context memory
244+ dataset_name = transferred_file .stem
245+ self ._electron_snapshots [dataset_name ] = transferred_file
246+
247+ if dataset_name not in self ._electron_snapshots_submitted :
248+ # If the metadata and image are both present, register dataset
249+ if dataset_name in list (self ._electron_snapshot_metadata .keys ()):
250+ logger .info (f"Registering { dataset_name !r} " )
251+
252+ ## Workflow to trigger goes here
253+
254+ # Clear old entry after triggering workflow
255+ self ._electron_snapshots_submitted .add (dataset_name )
256+ with lock :
257+ self ._electron_snapshots .pop (dataset_name , None )
258+ self ._electron_snapshot_metadata .pop (dataset_name , None )
259+ else :
260+ logger .debug (f"Waiting for metadata for { dataset_name } " )
134261 # -----------------------------------------------------------------------------
135262 # Meteor
136263 # -----------------------------------------------------------------------------
0 commit comments