def _process_thickness_metadata(mdict, base): abs_thick = _try_get_dict_val( mdict, base + ['Thickness', 'Absolute', 'Measurement']) abs_units = _try_get_dict_val(mdict, base + ['Thickness', 'Absolute', 'Units']) abs_mfp = _try_get_dict_val( mdict, base + ['Thickness', 'Absolute', 'Mean Free Path']) rel_thick = _try_get_dict_val( mdict, base + ['Thickness', 'Relative', 'Measurement']) if abs_thick != 'not found': _set_nest_dict_val( mdict, ['nx_meta', 'EELS', f'Thickness (absolute) [' f'{abs_units}]'], abs_thick) if abs_mfp != 'not found': _set_nest_dict_val( mdict, ['nx_meta', 'EELS', 'Thickness (absolute) mean ' 'free path'], abs_mfp[0]) if rel_thick != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'EELS', 'Thickness (relative) [t/λ]'], rel_thick) return mdict
def parse_system_info(mdict): """ Parses the `System` portion of the metadata dictionary from the Quanta to get values such as software version, chamber config, etc. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_quanta_metadata` Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ to_parse = [(['System', 'Chamber'], ['Chamber ID']), (['System', 'Pump'], ['Vacuum Pump']), (['System', 'SystemType'], ['System Type']), (['System', 'Stage'], ['Stage Description'])] for m_in, m_out in to_parse: val = _try_get_dict_val(mdict, m_in) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta'] + m_out, val) # Parse software info into one output tag: output_vals = [] val = _try_get_dict_val(mdict, ['System', 'Software']) if val != 'not found': output_vals.append(val) val = _try_get_dict_val(mdict, ['System', 'BuildNr']) if val != 'not found': output_vals.append(f'(build {val})') if len(output_vals) > 0: _set_nest_dict_val(mdict, ['nx_meta'] + ['Software Version'], ' '.join(output_vals)) # parse column and type into one output tag: output_vals = [] val = _try_get_dict_val(mdict, ['System', 'Column']) if val != 'not found': output_vals.append(val) val = _try_get_dict_val(mdict, ['System', 'Type']) if val != 'not found': output_vals.append(val) if len(output_vals) > 0: _set_nest_dict_val(mdict, ['nx_meta'] + ['Column Type'], ' '.join(output_vals)) return mdict
def get_pre_path(mdict): """ Get the path into a dictionary where the important DigitalMicrograph metadata is expected to be found. If the .dm3/.dm4 file contains a stack of images, the important metadata for NexusLIMS is not at its usual place and is instead under a `plan info` tag, so this method will determine if the stack metadata is present and return the correct path. ``pre_path`` will be something like ``['ImageList', 'TagGroup0', 'ImageTags', 'plane info', 'TagGroup0', 'source tags']``. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_dm3_metadata` Returns ------- pre_path : list A list containing the subsequent keys that need to be traversed to get to the point in the `mdict` where the important metadata is stored """ # test if we have a stack stack_val = _try_get_dict_val( mdict, ['ImageList', 'TagGroup0', 'ImageTags', 'plane info']) if stack_val != 'not found': # we're in a stack pre_path = [ 'ImageList', 'TagGroup0', 'ImageTags', 'plane info', 'TagGroup0', 'source tags' ] else: pre_path = ['ImageList', 'TagGroup0', 'ImageTags'] return pre_path
def parse_experimental_description(metadata): """ Parse the metadata that is saved at specific places within the .emi tag structure into a consistent place in the metadata dictionary returned by :py:meth:`get_ser_metadata`. Specifically looks at the "ExperimentalDescription" node of the metadata structure. Parameters ---------- metadata : dict A metadata dictionary as returned by :py:meth:`get_ser_metadata` Returns ------- metadata : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key Notes ----- The terms to extract in this section were """ # These terms were captured by looping through a selection of # representative .ser/.emi datafiles and running something like the # following base = ['ObjectInfo', 'ExperimentalDescription'] experimental_description = _try_get_dict_val(metadata, base) if experimental_description != 'not found' and isinstance( experimental_description, dict): term_mapping = {} for k in metadata['ObjectInfo']['ExperimentalDescription'].keys(): term, unit = split_fei_metadata_units(k) if unit: unit = unit.replace('uA', 'μA').\ replace('um', 'μm').\ replace('deg', '°') term_mapping[(k, )] = f'{term}' + (f' ({unit})' if unit else '') # Make stage position a nested list if 'Stage' in term: term = term.replace('Stage ', '') term_mapping[(k, )] = [ 'Stage Position', f'{term}' + (f' ({unit})' if unit else '') ] # Make filter settings a nested list if 'Filter ' in term: term = term.replace('Filter ', '') term_mapping[(k, )] = [ 'Tecnai Filter', f'{term.title()}' + (f' ({unit})' if unit else '') ] metadata = map_keys(term_mapping, base, metadata) # Microscope Mode often has excess spaces, so fix that if needed: if 'Mode' in metadata['nx_meta']: metadata['nx_meta']['Mode'] = metadata['nx_meta']['Mode'].strip() return metadata
def parse_acquire_info(metadata): """ Parse the metadata that is saved at specific places within the .emi tag structure into a consistent place in the metadata dictionary returned by :py:meth:`get_ser_metadata`. Specifically looks at the "AcquireInfo" node of the metadata structure. Parameters ---------- metadata : dict A metadata dictionary as returned by :py:meth:`get_ser_metadata` Returns ------- metadata : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ term_mapping = { ('AcceleratingVoltage', ): 'Microscope Accelerating Voltage (V)', ('Tilt1', ): 'Microscope Tilt 1', ('Tilt2', ): 'Microscope Tilt 2', } base = ['ObjectInfo', 'ExperimentalConditions', 'MicroscopeConditions'] if _try_get_dict_val(metadata, base) != 'not found': metadata = map_keys(term_mapping, base, metadata) return metadata
def map_keys(term_mapping, base, metadata): """ Given a term mapping dictionary and a metadata dictionary, translate the input keys within the "raw" metadata into a parsed value in the "nx_meta" metadata structure. Parameters ---------- term_mapping : dict Dictionary where keys are tuples of strings (the input terms), and values are either a single string or a list of strings (the output terms). base : list The 'root' path within the metadata dictionary of where to start applying the input terms metadata : dict A metadata dictionary as returned by :py:meth:`get_ser_metadata` Returns ------- metadata : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key, as specified by ``term_mapping`` Notes ----- The ``term_mapping`` parameter should be a dictionary of the form: .. code-block:: python { ('val1_1', 'val1_2') : 'output_val_1', ('val1_1', 'val2_2') : 'output_val_2', etc. } Assuming ``base`` is ``['ObjectInfo', 'AcquireInfo']``, this would map the term present at ``ObjectInfo.AcquireInfo.val1_1.val1_2`` into ``nx_meta.output_val_1``, and ``ObjectInfo.AcquireInfo.val1_1.val2_2`` into ``nx_meta.output_val_2``, and so on. If one of the output terms is a list, the resulting metadata will be nested. `e.g.` ``['output_val_1', 'output_val_2']`` would get mapped to ``nx_meta.output_val_1.output_val_2``. """ for in_term in term_mapping.keys(): out_term = term_mapping[in_term] if isinstance(in_term, tuple): in_term = list(in_term) if isinstance(out_term, str): out_term = [out_term] val = _try_get_dict_val(metadata, base + in_term) # only add the value to this list if we found it if val != 'not found': _set_nest_dict_val(metadata, ['nx_meta'] + out_term, _convert_to_numeric(val)) return metadata
def parse_det_info(mdict, det_name): """ Parses the `Detector` portion of the metadata dictionary from the Quanta to get values such as brightness, contrast, signal, etc. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_quanta_metadata` det_name : str The "detector name" read from the root-level ``Beam`` node of the metadata dictionary Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ to_parse = [([det_name, 'Brightness'], ['Detector Brightness Setting']), ([det_name, 'Contrast'], ['Detector Contrast Setting']), ([det_name, 'EnhancedContrast'], ['Detector Enhanced Contrast ' 'Setting']), ([det_name, 'Signal'], ['Detector Signal']), ([det_name, 'Grid'], ['Detector Grid Voltage (V)']), ([det_name, 'Setting'], ['Detector Setting'])] for m_in, m_out in to_parse: val = _try_get_dict_val(mdict, m_in) if val != 'not found': try: val = _Decimal(val) if m_in == [det_name, 'Setting']: # if "Setting" value is numeric, it's just the Grid # voltage so skip it continue except (ValueError, _invalidOp): pass _set_nest_dict_val( mdict, ['nx_meta'] + m_out, float(val) if isinstance(val, _Decimal) else val) _set_nest_dict_val(mdict, ['nx_meta'] + ['Detector Name'], det_name) return mdict
def parse_scan_info(mdict, scan_name): """ Parses the `Scan` portion of the metadata dictionary (on a Quanta this is always `"EScan"`) to get values such as dwell time, field width, and pixel size Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_quanta_metadata` scan_name : str The "scan name" read from the root-level ``Beam`` node of the metadata dictionary Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ # Values are in SI units, but we want easy to display, so include the # exponential factor that will get us from input unit (such as seconds) # to output unit (such as μs -- meaning factor = 6) to_parse = [ ([scan_name, 'Dwell'], ['Pixel Dwell Time (μs)'], 6), ([scan_name, 'FrameTime'], ['Total Frame Time (s)'], 0), ([scan_name, 'HorFieldsize'], ['Horizontal Field Width (μm)'], 6), ([scan_name, 'VerFieldsize'], ['Vertical Field Width (μm)'], 6), ([scan_name, 'PixelHeight'], ['Pixel Width (nm)'], 9), ([scan_name, 'PixelWidth'], ['Pixel Height (nm)'], 9), ] for m_in, m_out, factor in to_parse: val = _try_get_dict_val(mdict, m_in) if val != 'not found' and val != '': val = _Decimal(val) * _Decimal(str(10**factor)) _set_nest_dict_val( mdict, ['nx_meta'] + m_out, float(val) if isinstance(val, _Decimal) else val) return mdict
def parse_experimental_conditions(metadata): """ Parse the metadata that is saved at specific places within the .emi tag structure into a consistent place in the metadata dictionary returned by :py:meth:`get_ser_metadata`. Specifically looks at the "ExperimentalConditions" node of the metadata structure. Parameters ---------- metadata : dict A metadata dictionary as returned by :py:meth:`get_ser_metadata` Returns ------- metadata : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ term_mapping = { ('DwellTimePath', ): 'Dwell Time Path (s)', ('FrameTime', ): 'Frame Time (s)', ('CameraNamePath', ): 'Camera Name Path', ('Binning', ): 'Binning', ('BeamPosition', ): 'Beam Position (μm)', ('EnergyResolution', ): 'Energy Resolution (eV)', ('IntegrationTime', ): 'Integration Time (s)', ('NumberSpectra', ): 'Number of Spectra', ('ShapingTime', ): 'Shaping Time (s)', ('ScanArea', ): 'Scan Area', } base = ['ObjectInfo', 'AcquireInfo'] if _try_get_dict_val(metadata, base) != 'not found': metadata = map_keys(term_mapping, base, metadata) # remove units from beam position (if present) if 'Beam Position (μm)' in metadata['nx_meta']: metadata['nx_meta']['Beam Position (μm)'] = \ metadata['nx_meta']['Beam Position (μm)'].replace(' um', '') return metadata
def parse_dm3_spectrum_image_info(mdict): """ Parses metadata from the DigitalMicrograph tag structure that concerns any spectrum imaging information (from the "SI" tag) and places it in a "Spectrum Imaging" dictionary underneath the root-level ``nx_meta`` node. Metadata values that are commonly incorrect or may be placeholders are specified in a list under the ``nx_meta.warnings`` node. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_dm3_metadata` Returns ------- mdict : dict The metadata dictionary with all the "EDS-specific" metadata added as sub-node under the ``nx_meta`` root level dictionary """ pre_path = get_pre_path(mdict) # Spectrum imaging .dm3 tags of interest: base = pre_path + ['SI'] for m_in, m_out in \ [(['Acquisition', 'Pixel time (s)'], ['Pixel time (s)']), (['Acquisition', 'SI Application Mode', 'Name'], ['Scan Mode']), (['Acquisition', 'Spatial Sampling', 'Height (pixels)'], ['Spatial Sampling (Vertical)']), (['Acquisition', 'Spatial Sampling', 'Width (pixels)'], ['Spatial Sampling (Horizontal)']), (['Acquisition', 'Scan Options', 'Sub-pixel sampling'], ['Sub-pixel Sampling Factor'])]: val = _try_get_dict_val(mdict, base + m_in) # only add the value to this list if we found it, and it's not # one of the "facility-wide" set values that do not have any meaning: if val != 'not found': # add last value of each parameter to the "EDS" sub-tree of nx_meta _set_nest_dict_val(mdict, ['nx_meta', 'Spectrum Imaging'] + m_out, val) # Check spatial drift correction separately: drift_per_val = _try_get_dict_val( mdict, base + ['Acquisition', 'Artefact Correction', 'Spatial Drift', 'Periodicity']) drift_unit_val = _try_get_dict_val( mdict, base + ['Acquisition', 'Artefact Correction', 'Spatial Drift', 'Units']) if drift_per_val != 'not found' and drift_unit_val != 'not found': val_to_set = f"Spatial drift correction every {drift_per_val} " \ f"{drift_unit_val}" # make sure statement looks gramatically correct if drift_per_val == 1: val_to_set = val_to_set.replace('(s)', '') else: val_to_set = val_to_set.replace('(s)', 's') # fix for "seconds(s)" (stupid DM...) if val_to_set[-2:] == 'ss': val_to_set = val_to_set[:-1] _set_nest_dict_val( mdict, ['nx_meta', 'Spectrum Imaging', 'Artefact Correction'], val_to_set) # Calculate acquisition duration: start_val = _try_get_dict_val(mdict, base + ['Acquisition', 'Start time']) end_val = _try_get_dict_val(mdict, base + ['Acquisition', 'End time']) if start_val != 'not found' and end_val != 'not found': start_dt = _dt.strptime(start_val, '%I:%M:%S %p') end_dt = _dt.strptime(end_val, '%I:%M:%S %p') duration = (end_dt - start_dt).seconds _set_nest_dict_val( mdict, ['nx_meta', 'Spectrum Imaging', 'Acquisition Duration (s)'], duration) # Set the dataset type to SpectrumImage if it is already a Spectrum ( # otherwise it's just a STEM image) and any Spectrum Imaging tags were # added if 'Spectrum Imaging' in mdict['nx_meta']: if mdict['nx_meta']['DatasetType'] == 'Spectrum': _logger.info('Detected file as SpectrumImage type based on ' 'presence of spectral metadata and spectrum imaging ' 'info') mdict['nx_meta']['DatasetType'] = 'SpectrumImage' mdict['nx_meta']['Data Type'] = 'Spectrum_Imaging' if 'EELS' in mdict['nx_meta']: mdict['nx_meta']['Data Type'] = 'EELS_Spectrum_Imaging' if 'EDS' in mdict['nx_meta']: mdict['nx_meta']['Data Type'] = 'EDS_Spectrum_Imaging' return mdict
def get_ser_metadata(filename): """ Returns metadata (as a dict) from an FEI .ser file + its associated .emi files, with some non-relevant information stripped. Parameters ---------- filename : str Path to FEI .ser file Returns ------- metadata : dict Metadata of interest which is extracted from the passed files. If files cannot be opened, at least basic metadata will be returned ( creation time, etc.) """ # ObjectInfo present in emi; ser_header_parameters present in .ser # ObjectInfo should contain all the interesting metadata, # while ser_header_parameters is mostly technical stuff not really of # interest to anyone warning = None emi_filename = None ser_error = False try: # approach here is for every .ser we want to examine, load the # metadata from the corresponding .emi file. If multiple .ser files # are related to this emi, HyperSpy returns a list, so we select out # the right signal from that list if that's what is returned emi_filename, ser_index = get_emi_from_ser(filename) # make sure to load with "only_valid_data" so data shape is correct # loading the emi with HS will try loading the .ser too, so this will # fail if there's an issue with the .ser file emi_s = _hs_load(emi_filename, lazy=True, only_valid_data=True) emi_loaded = True # if there is more than one dataset, emi_s will be a list, so pick # out the matching signal from the list, which will be the "index" # from the filename minus 1: if isinstance(emi_s, list): s = emi_s[ser_index - 1] # otherwise we should just have a regular signal, so make s the same # as the data loaded from the .emi elif isinstance(emi_s, _BaseSignal): s = emi_s except FileNotFoundError: # if emi wasn't found, specifically mention that warning = 'NexusLIMS could not find a corresponding .emi metadata ' + \ 'file for this .ser file. Metadata extraction will be ' + \ 'limited.' _logger.warning(warning) emi_loaded = False emi_filename = None except Exception: # otherwise, HyperSpy could not load the .emi, so give generic warning # that .emi could not be loaded for some reason: warning = 'The .emi metadata file associated with this ' + \ '.ser file could not be opened by NexusLIMS. ' + \ 'Metadata extraction will be limited.' _logger.warning(warning) emi_loaded = False if not emi_loaded: # if we couldn't load the emi, lets at least open the .ser to pull # out the ser_header_info try: s = _hs_load(filename, only_valid_data=True, lazy=True) except Exception: warning = f'The .ser file could not be opened (perhaps file is ' + \ f'corrupted?); Metadata extraction is not possible.' _logger.warning(warning) # set s to an empty signal just so we can process some basic # metadata using same syntax as if we had read it correctly s = _BaseSignal(_np.zeros(1)) ser_error = True metadata = s.original_metadata.as_dictionary() metadata['nx_meta'] = {} # if we've already encountered a warning, add that to the metadata, if warning: metadata['nx_meta']['Extractor Warning'] = warning # otherwise check to ensure we actually have some metadata read from .emi elif 'ObjectInfo' not in metadata or \ ('ExperimentalConditions' not in metadata['ObjectInfo'] and 'ExperimentalDescription' not in metadata['ObjectInfo']): warning = 'No experimental metadata was found in the ' + \ 'corresponding .emi file for this .ser. ' + \ 'Metadata extraction will be limited.' _logger.warning(warning) metadata['nx_meta']['Extractor Warning'] = warning # if we successfully found the .emi file, add it to the metadata if emi_filename: rel_emi_fname = emi_filename.replace( _os.environ["mmfnexus_path"] + '/', '') if emi_filename else None metadata['nx_meta']['emi Filename'] = rel_emi_fname else: metadata['nx_meta']['emi Filename'] = None # Get the instrument object associated with this file instr = _get_instr(filename) # get the modification time (as ISO format): mtime = _os.path.getmtime(filename) mtime_iso = _dt.fromtimestamp(mtime).isoformat() # if we found the instrument, then store the name as string, else None instr_name = instr.name if instr is not None else None metadata['nx_meta']['fname'] = filename metadata['nx_meta']['Creation Time'] = mtime_iso metadata['nx_meta']['Instrument ID'] = instr_name # we could not read the signal, so add some basic metadata and return if ser_error: metadata['nx_meta']['DatasetType'] = 'Misc' metadata['nx_meta']['Data Type'] = '**REMOVED**' metadata['nx_meta']['warnings'] = [] # sort the nx_meta dictionary (recursively) for nicer display metadata['nx_meta'] = _sort_dict(metadata['nx_meta']) del metadata['nx_meta']['fname'] return metadata # try to set creation time to acquisition time from metadata acq_time = _try_get_dict_val(metadata, ['ObjectInfo', 'AcquireDate']) if acq_time != 'not found': metadata['nx_meta']['Creation Time'] = \ _dt.strptime(acq_time, '%a %b %d %H:%M:%S %Y').isoformat() # manufacturer is at high level, so parse it now manufacturer = _try_get_dict_val(metadata, ['ObjectInfo', 'Manufacturer']) if manufacturer != 'not found': metadata['nx_meta']['Manufacturer'] = \ manufacturer metadata['nx_meta']['Data Dimensions'] = str(s.data.shape) metadata['nx_meta']['warnings'] = [] # set type to STEM Image by default (this seems to be most common) metadata['nx_meta']['DatasetType'] = 'Image' metadata['nx_meta']['Data Type'] = 'STEM_Imaging' metadata = parse_acquire_info(metadata) metadata = parse_experimental_conditions(metadata) metadata = parse_experimental_description(metadata) metadata['nx_meta']['Data Type'], metadata['nx_meta']['DatasetType'] = \ parse_data_type(s, metadata) # we don't need to save the filename, it's just for internal processing del metadata['nx_meta']['fname'] # sort the nx_meta dictionary (recursively) for nicer display metadata['nx_meta'] = _sort_dict(metadata['nx_meta']) return metadata
def parse_dm3_eds_info(mdict): """ Parses metadata from the DigitalMicrograph tag structure that concerns any EDS acquisition or spectrometer settings, placing it in an ``EDS`` dictionary underneath the root-level ``nx_meta`` node. Metadata values that are commonly incorrect or may be placeholders are specified in a list under the ``nx_meta.warnings`` node. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_dm3_metadata` Returns ------- mdict : dict The metadata dictionary with all the "EDS-specific" metadata added as sub-node under the ``nx_meta`` root level dictionary """ pre_path = get_pre_path(mdict) # EELS .dm3 tags of interest: base = pre_path + ['EDS'] for m in [['Acquisition', 'Continuous Mode'], ['Acquisition', 'Count Rate Unit'], ['Acquisition', 'Dispersion (eV)'], ['Acquisition', 'Energy Cutoff (V)'], ['Acquisition', 'Exposure (s)'], ['Count rate'], ['Detector Info', 'Active layer'], ['Detector Info', 'Azimuthal angle'], ['Detector Info', 'Dead layer'], ['Detector Info', 'Detector type'], ['Detector Info', 'Elevation angle'], ['Detector Info', 'Fano'], ['Detector Info', 'Gold layer'], ['Detector Info', 'Incidence angle'], ['Detector Info', 'Solid angle'], ['Detector Info', 'Stage tilt'], ['Detector Info', 'Window thickness'], ['Detector Info', 'Window type'], ['Detector Info', 'Zero fwhm'], ['Live time'], ['Real time']]: val = _try_get_dict_val(mdict, base + m) # only add the value to this list if we found it, and it's not # one of the "facility-wide" set values that do not have any meaning: if val != 'not found': # add last value of each parameter to the "EDS" sub-tree of nx_meta _set_nest_dict_val(mdict, ['nx_meta', 'EDS'] + [m[-1] if len(m) > 1 else m[0]], val) # test to see if the SI attribute is present in the metadata dictionary. # If so, then some of the relevant EDS values are located there, rather # than in the root-level EDS tag (all of the EDS.Acquisition tags from # above) if _try_get_dict_val(mdict, pre_path + ['SI']) != 'not found': for m in [['Acquisition', 'Continuous Mode'], ['Acquisition', 'Count Rate Unit'], ['Acquisition', 'Dispersion (eV)'], ['Acquisition', 'Energy Cutoff (V)'], ['Acquisition', 'Exposure (s)']]: val = _try_get_dict_val(mdict, pre_path + ['SI'] + m) if val != 'not found': # add last value of each parameter to the "EDS" sub-tree of # nx_meta _set_nest_dict_val(mdict, ['nx_meta', 'EDS'] + [m[-1]], val) # for an SI EDS dataset, set "Live time", "Real time" and "Count rate" # to the averages stored in the ImageList.TagGroup0.ImageTags.EDS.Images # values im_dict = _try_get_dict_val(mdict, pre_path + ['EDS', 'Images']) if isinstance(im_dict, dict): for k, v in im_dict.items(): if k in mdict['nx_meta']['EDS']: del mdict['nx_meta']['EDS'][k] # this should work for 2D (spectrum image) as well as 1D # (linescan) datasets since DM saves this information as a 1D # list regardless of original data shape avg_val = _np.array(v).mean() _set_nest_dict_val(mdict, ['nx_meta', 'EDS'] + [f'{k} (SI Average)'], avg_val) # Add the .dm3 EDS values to the warnings list, since they might not be # accurate for m in [['Count rate'], ['Detector Info', 'Active layer'], ['Detector Info', 'Azimuthal angle'], ['Detector Info', 'Dead layer'], ['Detector Info', 'Detector type'], ['Detector Info', 'Elevation angle'], ['Detector Info', 'Fano'], ['Detector Info', 'Gold layer'], ['Detector Info', 'Incidence angle'], ['Detector Info', 'Solid angle'], ['Detector Info', 'Stage tilt'], ['Detector Info', 'Window thickness'], ['Detector Info', 'Window type'], ['Detector Info', 'Zero fwhm'], ['Live time'], ['Real time']]: if _try_get_dict_val(mdict, base + m) != 'not found': mdict['nx_meta']['warnings'].append( ['EDS'] + [m[-1] if len(m) > 1 else m[0]]) # Set the dataset type to Spectrum if any EDS tags were added if 'EDS' in mdict['nx_meta']: _logger.info('Detected file as Spectrum type based on presence of ' 'EDS metadata') mdict['nx_meta']['DatasetType'] = 'Spectrum' if 'STEM' in mdict['nx_meta']['Illumination Mode']: mdict['nx_meta']['Data Type'] = 'STEM_EDS' else: # no known files match this mode, so skip for coverage mdict['nx_meta']['Data Type'] = 'TEM_EDS' # pragma: no cover return mdict
def parse_dm3_eels_info(mdict): """ Parses metadata from the DigitalMicrograph tag structure that concerns any EELS acquisition or spectrometer settings, placing it in an ``EELS`` dictionary underneath the root-level ``nx_meta`` node. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_dm3_metadata` Returns ------- mdict : dict The metadata dictionary with all the "EELS-specific" metadata added as sub-node under the ``nx_meta`` root level dictionary """ pre_path = get_pre_path(mdict) # EELS .dm3 tags of interest: base = pre_path + ['EELS'] for m in [['Acquisition', 'Exposure (s)'], ['Acquisition', 'Integration time (s)'], ['Acquisition', 'Number of frames'], ["Experimental Conditions", "Collection semi-angle (mrad)"], ["Experimental Conditions", "Convergence semi-angle (mrad)"]]: val = _try_get_dict_val(mdict, base + m) # only add the value to this list if we found it, and it's not # one of the "facility-wide" set values that do not have any meaning: if val != 'not found': # add last value of each parameter to the "EELS" sub-tree of nx_meta _set_nest_dict_val(mdict, ['nx_meta', 'EELS'] + [m[-1]], val) # different instruments have the spectrometer information in different # places... if mdict['nx_meta']['Instrument ID'] == '**REMOVED**': base = pre_path + ['EELS', 'Acquisition', 'Spectrometer'] elif mdict['nx_meta']['Instrument ID'] == '**REMOVED**': base = pre_path + ['EELS Spectrometer'] else: base = None if base is not None: for m in [ "Aperture label", "Dispersion (eV/ch)", "Energy loss (eV)", "Instrument name", "Drift tube enabled", "Drift tube voltage (V)", "Slit inserted", "Slit width (eV)", "Prism offset (V)", "Prism offset enabled " ]: m = [m] val = _try_get_dict_val(mdict, base + m) if val != 'not found': # add last value of each param to the "EELS" sub-tree of nx_meta _set_nest_dict_val(mdict, ['nx_meta', 'EELS'] + ["Spectrometer " + m[0]], val) # Process known tags under "processing": # ImageTags.Processing will be a list of things done (in multiple # TagGroups) - things like Compute thickness, etc. val = _try_get_dict_val(mdict, pre_path + ['Processing']) if val != 'not found' and isinstance(val, dict): # if val is a dict, then there were processing steps applied eels_ops = [] for k, v in val.items(): # k will be TagGroup0, TagGroup1, etc. # v will be dictionaries specifying the process step # AlignSIByPeak, DataPicker, SpectrumCalibrate, # Compute Thickness, Background Removal, Signal Integration op = v['Operation'] param = v['Parameters'] if op == 'AlignSIByPeak': eels_ops.append('Aligned parent SI By Peak') elif op == 'Background Removal': val = _try_get_dict_val(param, ['Model']) if val != 'not found': _set_nest_dict_val( mdict, ['nx_meta', 'EELS', 'Background Removal Model'], val) eels_ops.append(op) elif op == 'SpectrumCalibrate': eels_ops.append('Calibrated Post-acquisition') elif op == 'Compute Thickness': mdict = _process_thickness_metadata(mdict, pre_path + ['EELS']) eels_ops.append(op) elif op == 'DataPicker': eels_ops.append('Extracted from SI') elif op == 'Signal Integration': eels_ops.append(op) if eels_ops: # remove duplicates (convert to set) and sort alphabetically: _set_nest_dict_val(mdict, ['nx_meta', 'EELS', 'Processing Steps'], ', '.join(sorted(set(eels_ops)))) # Set the dataset type to Spectrum if any EELS tags were added if 'EELS' in mdict['nx_meta']: _logger.info('Detected file as Spectrum type based on presence of ' 'EELS metadata') mdict['nx_meta']['DatasetType'] = 'Spectrum' if 'STEM' in mdict['nx_meta']['Illumination Mode']: mdict['nx_meta']['Data Type'] = 'STEM_EELS' else: mdict['nx_meta']['Data Type'] = 'TEM_EELS' return mdict
def parse_dm3_microscope_info(mdict): """ Parse the "important" metadata that is saved at specific places within the DM3 tag structure into a consistent place in the metadata dictionary returned by :py:meth:`get_dm3_metadata`. Specifically looks at the "Microscope Info", "Session Info", and "Meta Data" nodes of the tag structure (these are not present on every microscope) Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_dm3_metadata` Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ if 'nx_meta' not in mdict: mdict['nx_meta'] = {} pre_path = get_pre_path(mdict) # General "microscope info" .dm3 tags (not present on all instruments): for m in [ 'Indicated Magnification', 'Actual Magnification', 'Cs(mm)', 'STEM Camera Length', 'Voltage', 'Operation Mode', 'Specimen', 'Microscope', 'Operator', 'Imaging Mode', 'Illumination Mode', 'Name', 'Field of View (\u00b5m)', 'Facility', ['Stage Position', 'Stage Alpha'], ['Stage Position', 'Stage Beta'], ['Stage Position', 'Stage X'], ['Stage Position', 'Stage Y'], ['Stage Position', 'Stage Z'] ]: base = pre_path + ['Microscope Info'] if isinstance(m, str): m = [m] val = _try_get_dict_val(mdict, base + m) # only add the value to this list if we found it, and it's not one of # the "facility-wide" set values that do not have any meaning: if val != 'not found' and val not in ['DO NOT EDIT', 'DO NOT ENTER'] \ and val != []: # change output of "Stage Position" to unicode characters if 'Stage Position' in m: m[-1] = m[-1].replace('Alpha', 'α').replace('Beta', 'β').replace('Stage ', '') _set_nest_dict_val(mdict, ['nx_meta'] + m, val) # General "session info" .dm3 tags (sometimes this information is stored # here instead of under "Microscope Info": for m in ['Detector', 'Microscope', 'Operator', 'Specimen']: base = pre_path + ['Session Info'] if isinstance(m, str): m = [m] val = _try_get_dict_val(mdict, base + m) # only add the value to this list if we found it, and it's not # one of the "facility-wide" set values that do not have any meaning: if val != 'not found' and val not in ['DO NOT EDIT', 'DO NOT ENTER'] \ and val != []: _set_nest_dict_val(mdict, ['nx_meta'] + m, val) # General "Meta Data" .dm3 tags for m in [ 'Acquisition Mode', 'Format', 'Signal', # this one is seen sometimes in EDS signals: ['Experiment keywords', 'TagGroup1', 'Label'] ]: base = pre_path + ['Meta Data'] if isinstance(m, str): m = [m] val = _try_get_dict_val(mdict, base + m) # only add the value to this list if we found it, and it's not # one of the "facility-wide" set values that do not have any meaning: if val != 'not found' and val not in ['DO NOT EDIT', 'DO NOT ENTER'] \ and val != []: if 'Label' in m: _set_nest_dict_val(mdict, ['nx_meta'] + ['Analytic Label'], val) else: _set_nest_dict_val(mdict, ['nx_meta'] + [f'Analytic {lbl}' for lbl in m], val) # Get acquisition device name: val = _try_get_dict_val(mdict, pre_path + ['Acquisition', 'Device', 'Name']) if val == 'not found': val = _try_get_dict_val(mdict, pre_path + ['DataBar', 'Device Name']) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Acquisition Device'], val) # Get exposure time: val = _try_get_dict_val( mdict, pre_path + ['Acquisition', 'Parameters', 'High Level', 'Exposure (s)']) if val == 'not found': val = _try_get_dict_val(mdict, pre_path + ['DataBar', 'Exposure Time (s)']) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Exposure Time (s)'], val) # Get GMS version: val = _try_get_dict_val(mdict, pre_path + ['GMS Version', 'Created']) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'GMS Version'], val) # Get camera binning: val = _try_get_dict_val( mdict, pre_path + ['Acquisition', 'Parameters', 'High Level', 'Binning']) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Binning (Horizontal)'], val[0]) _set_nest_dict_val(mdict, ['nx_meta', 'Binning (Vertical)'], val[1]) # Get image processing: # ImageTags.Acquisition.Parameters["High Level"].Processing will be # something like "Gain normalized" - not just for EELS so move this to # general val = _try_get_dict_val( mdict, pre_path + ['Acquisition', 'Parameters', 'High Level', 'Processing']) if val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Camera/Detector Processing'], val) if 'Illumination Mode' in mdict['nx_meta']: if 'STEM' in mdict['nx_meta']['Illumination Mode']: mdict['nx_meta']['Data Type'] = 'STEM_Imaging' return mdict
def parse_nx_meta(mdict): """ Parse the "important" metadata that is saved at specific places within the Quanta tag structure into a consistent place in the metadata dictionary returned by :py:meth:`get_quanta_metadata`. Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_quanta_metadata` Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ # The name of the beam, scan, and detector will determine which sections are # present (have not seen more than one beam/detector -- although likely # will be the case for dual beam FIB/SEM) beam_name = _try_get_dict_val(mdict, ['Beam', 'Beam']) det_name = _try_get_dict_val(mdict, ['Detectors', 'Name']) scan_name = _try_get_dict_val(mdict, ['Beam', 'Scan']) # some parsers are broken off into helper methods: if beam_name != 'not found': mdict = parse_beam_info(mdict, beam_name) if scan_name != 'not found': mdict = parse_scan_info(mdict, scan_name) if det_name != 'not found': mdict = parse_det_info(mdict, det_name) if _try_get_dict_val(mdict, ['System']) != 'not found': mdict = parse_system_info(mdict) # process the rest of the metadata tags: # process beam spot size val = _try_get_dict_val(mdict, ['Beam', 'Spot']) if val != 'not found': try: val = _Decimal(val) except (ValueError, _invalidOp): pass _set_nest_dict_val(mdict, ['nx_meta'] + ['Spot Size'], float(val) if isinstance(val, _Decimal) else val) # process drift correction val = _try_get_dict_val(mdict, ['Image', 'DriftCorrected']) if val != 'not found': # set to true if the value is 'On' val = val == 'On' _set_nest_dict_val(mdict, ['nx_meta'] + ['Drift Correction Applied'], val) # process frame integration val = _try_get_dict_val(mdict, ['Image', 'Integrate']) if val != 'not found': try: val = int(val) if val > 1: _set_nest_dict_val(mdict, ['nx_meta'] + ['Frames Integrated'], val) except ValueError: pass # process mag mode val = _try_get_dict_val(mdict, ['Image', 'MagnificationMode']) if val != 'not found': try: val = int(val) except ValueError: pass _set_nest_dict_val(mdict, ['nx_meta'] + ['Magnification Mode'], val) # Process "ResolutionX/Y" (data size) x_val = _try_get_dict_val(mdict, ['Image', 'ResolutionX']) y_val = _try_get_dict_val(mdict, ['Image', 'ResolutionY']) try: x_val = int(x_val) y_val = int(y_val) except ValueError: pass if x_val != 'not found' and y_val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Data Dimensions'], str((x_val, y_val))) # test for specimen temperature value if present and non-empty temp_val = _try_get_dict_val(mdict, ['Specimen', 'Temperature']) if temp_val != 'not found' and temp_val != '': try: temp_val = _Decimal(temp_val) except (ValueError, _invalidOp): pass _set_nest_dict_val( mdict, ['nx_meta', 'Specimen Temperature (K)'], float(temp_val) if isinstance(temp_val, _Decimal) else temp_val) # # parse SpecTilt (think this is specimen pre-tilt, but not definite) # # tests showed that this is always the same value as StageT, so we do not # # need to parse this one # val = _try_get_dict_val(mdict, ['Stage', 'SpecTilt']) # if val != 'not found' and val != '0': # _set_nest_dict_val(mdict, ['nx_meta', 'Stage Position', # 'Specimen Tilt'], val) # Get user ID (sometimes it's not correct because the person left the # instrument logged in as the previous user, so make sure to add it to # the warnings list user_val = _try_get_dict_val(mdict, ['User', 'User']) if user_val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Operator'], user_val) mdict['nx_meta']['warnings'].append(['Operator']) # parse acquisition date and time acq_date_val = _try_get_dict_val(mdict, ['User', 'Date']) acq_time_val = _try_get_dict_val(mdict, ['User', 'Time']) if acq_date_val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Acquisition Date'], acq_date_val) if acq_time_val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Acquisition Time'], acq_time_val) # parse vacuum mode vac_val = _try_get_dict_val(mdict, ['Vacuum', 'UserMode']) if user_val != 'not found': _set_nest_dict_val(mdict, ['nx_meta', 'Vacuum Mode'], vac_val) # parse chamber pressure ch_pres_val = _try_get_dict_val(mdict, ['Vacuum', 'ChPressure']) if ch_pres_val != 'not found' and ch_pres_val != '': # keep track of original digits so we don't propagate float errors try: ch_pres_val = _Decimal(ch_pres_val) except _invalidOp: ch_pres_val = ch_pres_val if _try_get_dict_val(mdict, ['nx_meta', 'Vacuum Mode']) == 'High vacuum': ch_pres_str = 'Chamber Pressure (mPa)' ch_pres_val = ch_pres_val * 10**3 else: ch_pres_str = 'Chamber Pressure (Pa)' ch_pres_val = ch_pres_val _set_nest_dict_val( mdict, ['nx_meta', ch_pres_str], float(ch_pres_val) if isinstance(ch_pres_val, _Decimal) else ch_pres_val) return mdict
def parse_642_titan(mdict): """ Add/adjust metadata specific to the 642 FEI Titan ('`**REMOVED** in **REMOVED**`') to the metadata dictionary Parameters ---------- mdict : dict "raw" metadata dictionary as parsed by :py:func:`get_dm3_metadata` Returns ------- mdict : dict The original metadata dictionary with added information specific to files originating from this microscope with "important" values contained under the ``nx_meta`` key at the root level """ # DONE: complete 642 titan metadata parsing including Tecnai tag path_to_tecnai = _get_nest_dict_key(mdict, 'Tecnai') if path_to_tecnai is None: # For whatever reason, the expected Tecnai Tag is not present, # so return to prevent errors below return mdict tecnai_value = _get_nest_dict_val_by_path(mdict, path_to_tecnai) microscope_info = tecnai_value['Microscope Info'] tecnai_value['Microscope Info'] = \ process_tecnai_microscope_info(microscope_info) _set_nest_dict_val(mdict, path_to_tecnai, tecnai_value) # - Tecnai info: # - ImageTags.Tecnai.Microscope_Info['Gun_Name'] # - ImageTags.Tecnai.Microscope_Info['Extractor_Voltage'] # - ImageTags.Tecnai.Microscope_Info['Gun_Lens_No'] # - ImageTags.Tecnai.Microscope_Info['Emission_Current'] # - ImageTags.Tecnai.Microscope_Info['Spot'] # - ImageTags.Tecnai.Microscope_Info['Mode'] # - C2, C3, Obj, Dif lens strength: # - ImageTags.Tecnai.Microscope_Info['C2_Strength', # 'C3_Strength', # 'Obj_Strength', # 'Dif_Strength'] # - ImageTags.Tecnai.Microscope_Info['Image_Shift_x'/'Image_Shift_y']) # - ImageTags.Tecnai.Microscope_Info['Stage_Position_x' (y/z/theta/phi)] # - C1/C2/Objective/SA aperture sizes: # - ImageTags.Tecnai.Microscope_Info['(C1/C2/Obj/SA)_Aperture'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings']['Mode'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings']['Dispersion'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings']['Aperture'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings']['Prism_Shift'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings']['Drift_Tube'] # - ImageTags.Tecnai.Microscope_Info['Filter_Settings'][ # 'Total_Energy_Loss'] term_mapping = { 'Gun_Name': 'Gun Name', 'Extractor_Voltage': 'Extractor Voltage (V)', 'Camera_Length': 'Camera Length (m)', 'Gun_Lens_No': 'Gun Lens #', 'Emission_Current': 'Emission Current (μA)', 'Spot': 'Spot', 'Mode': 'Tecnai Mode', 'Defocus': 'Defocus', 'C2_Strength': 'C2 Lens Strength (%)', 'C3_Strength': 'C3 Lens Strength (%)', 'Obj_Strength': 'Objective Lens Strength (%)', 'Dif_Strength': 'Diffraction Lens Strength (%)', 'Microscope_Name': 'Tecnai Microscope Name', 'User': '******', 'Image_Shift_x': 'Image Shift X (μm)', 'Image_Shift_y': 'Image Shift Y (μm)', 'Stage_Position_x': ['Stage Position', 'X (μm)'], 'Stage_Position_y': ['Stage Position', 'Y (μm)'], 'Stage_Position_z': ['Stage Position', 'Z (μm)'], 'Stage_Position_theta': ['Stage Position', 'θ (°)'], 'Stage_Position_phi': ['Stage Position', 'φ (°)'], 'C1_Aperture': 'C1 Aperture (μm)', 'C2_Aperture': 'C2 Aperture (μm)', 'Obj_Aperture': 'Objective Aperture (μm)', 'SA_Aperture': 'Selected Area Aperture (μm)', ('Filter_Settings', 'Mode'): ['Tecnai Filter', 'Mode'], ('Filter_Settings', 'Dispersion'): ['Tecnai Filter', 'Dispersion (eV/channel)'], ('Filter_Settings', 'Aperture'): ['Tecnai Filter', 'Aperture (mm)'], ('Filter_Settings', 'Prism_Shift'): ['Tecnai Filter', 'Prism Shift (eV)'], ('Filter_Settings', 'Drift_Tube'): ['Tecnai Filter', 'Drift Tube (eV)'], ('Filter_Settings', 'Total_Energy_Loss'): ['Tecnai Filter', 'Total Energy Loss (eV)'], } for in_term in term_mapping.keys(): base = list(path_to_tecnai) + ['Microscope Info'] out_term = term_mapping[in_term] if isinstance(in_term, str): in_term = [in_term] elif isinstance(in_term, tuple): in_term = list(in_term) if isinstance(out_term, str): out_term = [out_term] val = _try_get_dict_val(mdict, base + in_term) # only add the value to this list if we found it if val != 'not found' and val not in ['DO NOT EDIT', 'DO NOT ENTER']: _set_nest_dict_val(mdict, ['nx_meta'] + out_term, val) path = list(path_to_tecnai) + ['Specimen Info'] val = _try_get_dict_val(mdict, path) if val != 'not found' and \ val != 'Specimen information is not available yet': _set_nest_dict_val(mdict, ['nx_meta', 'Specimen'], val) # If `Tecnai Mode` is `STEM nP SA Zoom Diffraction`, it's diffraction if 'Tecnai Mode' in mdict['nx_meta'] and \ mdict['nx_meta']['Tecnai Mode'] == 'STEM nP SA Zoom Diffraction': _logger.info('Detected file as Diffraction type based on "Tecnai ' 'Mode" == "STEM nP SA Zoom Diffraction"') mdict['nx_meta']['DatasetType'] = 'Diffraction' mdict['nx_meta']['Data Type'] = 'STEM_Diffraction' # also, if `Operation Mode` is `DIFFRACTION`, it's diffraction elif 'Operation Mode' in mdict['nx_meta'] and \ mdict['nx_meta']['Operation Mode'] == 'DIFFRACTION': _logger.info('Detected file as Diffraction type based on "Operation ' 'Mode" == "DIFFRACTION"') mdict['nx_meta']['DatasetType'] = 'Diffraction' mdict['nx_meta']['Data Type'] = 'TEM_Diffraction' return mdict
def parse_beam_info(mdict, beam_name): """ Parameters ---------- mdict : dict A metadata dictionary as returned by :py:meth:`get_quanta_metadata` beam_name : str The "beam name" read from the root-level ``Beam`` node of the metadata dictionary Returns ------- mdict : dict The same metadata dictionary with some values added under the root-level ``nx_meta`` key """ # Values are in SI units, but we want easy to display, so include the # exponential factor that will get us from input unit (such as seconds) # to output unit (such as μs -- meaning factor = 6) to_parse = [ ([beam_name, 'EmissionCurrent'], ['Emission Current (μA)'], 6), ([beam_name, 'HFW'], ['Horizontal Field Width (μm)'], 6), ([beam_name, 'HV'], ['Voltage (kV)'], -3), ([beam_name, 'SourceTiltX'], ['Beam Tilt X'], 0), ([beam_name, 'SourceTiltY'], ['Beam Tilt Y'], 0), ([beam_name, 'StageR'], ['Stage Position', 'R'], 0), ([beam_name, 'StageTa'], ['Stage Position', 'α'], 0), # all existing quanta images have a value of zero for beta # ([beam_name, 'StageTb'], ['Stage Position', 'β'], 0), ([beam_name, 'StageX'], ['Stage Position', 'X'], 0), ([beam_name, 'StageY'], ['Stage Position', 'Y'], 0), ([beam_name, 'StageZ'], ['Stage Position', 'Z'], 0), ([beam_name, 'StigmatorX'], ['Stigmator X Value'], 0), ([beam_name, 'StigmatorY'], ['Stigmator Y Value'], 0), ([beam_name, 'VFW'], ['Vertical Field Width (μm)'], 6), ([beam_name, 'WD'], ['Working Distance (mm)'], 3), ] for m_in, m_out, factor in to_parse: val = _try_get_dict_val(mdict, m_in) if val != 'not found' and val != '': val = _Decimal(val) * _Decimal(str(10**factor)) _set_nest_dict_val( mdict, ['nx_meta'] + m_out, float(val) if isinstance(val, _Decimal) else val) # Add beam name to metadata: _set_nest_dict_val(mdict, ['nx_meta'] + ['Beam Name'], beam_name) # BeamShiftX and BeamShiftY require an additional test: bs_x_val = _try_get_dict_val(mdict, [beam_name, 'BeamShiftX']) bs_y_val = _try_get_dict_val(mdict, [beam_name, 'BeamShiftY']) if bs_x_val != 'not found' and _Decimal(bs_x_val) != 0: _set_nest_dict_val(mdict, ['nx_meta'] + ['Beam Shift X'], float(_Decimal(bs_x_val))) if bs_y_val != 'not found' and _Decimal(bs_y_val) != 0: _set_nest_dict_val(mdict, ['nx_meta'] + ['Beam Shift Y'], float(_Decimal(bs_y_val))) # only parse scan rotation if value is not zero: # Not sure what the units of this value are... looks like radians because # unique values range from 0 to 6.24811 - convert to degrees for display scan_rot_val = _try_get_dict_val(mdict, [beam_name, 'ScanRotation']) if scan_rot_val != 'not found' and _Decimal(scan_rot_val) != 0: scan_rot_dec = _Decimal(scan_rot_val) # make scan_rot a Decimal # get number of digits in Decimal value (so we don't artificially # introduce extra precision) digits = abs(scan_rot_dec.as_tuple().exponent) # round the final float value to that number of digits scan_rot_val = round(degrees(scan_rot_dec), digits) _set_nest_dict_val(mdict, ['nx_meta', 'Scan Rotation (°)'], scan_rot_val) # TiltCorrectionAngle only if TiltCorrectionIsOn == 'yes' tilt_corr_on = _try_get_dict_val(mdict, [beam_name, 'TiltCorrectionIsOn']) if tilt_corr_on == 'yes': tilt_corr_val = _try_get_dict_val(mdict, [beam_name, 'TiltCorrectionAngle']) if tilt_corr_val != 'not found': tilt_corr_val = float(_Decimal(tilt_corr_val)) _set_nest_dict_val(mdict, ['nx_meta'] + ['Tilt Correction Angle'], tilt_corr_val) return mdict