Source code for nexusLIMS.extractors.quanta_tif

#  NIST Public License - 2019
#
#  This software was developed by employees of the National Institute of
#  Standards and Technology (NIST), an agency of the Federal Government
#  and is being made available as a public service. Pursuant to title 17
#  United States Code Section 105, works of NIST employees are not subject
#  to copyright protection in the United States.  This software may be
#  subject to foreign copyright.  Permission in the United States and in
#  foreign countries, to the extent that NIST may hold copyright, to use,
#  copy, modify, create derivative works, and distribute this software and
#  its documentation without fee is hereby granted on a non-exclusive basis,
#  provided that this notice and disclaimer of warranty appears in all copies.
#
#  THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND,
#  EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED
#  TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY
#  IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
#  AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION
#  WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE
#  ERROR FREE.  IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING,
#  BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES,
#  ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE,
#  WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER
#  OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND
#  WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF,
#  OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
#
import configparser as _cp
import io as _io
import os as _os
from datetime import datetime as _dt
import logging as _logging
from math import degrees
from decimal import Decimal as _Decimal
from decimal import InvalidOperation as _invalidOp

from nexusLIMS.instruments import get_instr_from_filepath as _get_instr
from nexusLIMS.utils import _sort_dict
from nexusLIMS.utils import set_nested_dict_value as _set_nest_dict_val
from nexusLIMS.utils import try_getting_dict_value as _try_get_dict_val

_logger = _logging.getLogger(__name__)
_logger.setLevel(_logging.INFO)


[docs]def get_quanta_metadata(filename): """ Returns the metadata (as a dictionary) from a .tif file saved by the FEI Quanta SEM in the Nexus Microscopy Facility. Specific tags of interest are duplicated under the root-level ``nx_meta`` node in the dictionary. Parameters ---------- filename : str path to a .tif file saved by the Quanta Returns ------- mdict : dict The metadata text extracted from the file """ with open(filename, 'rb') as f: content = f.read() user_idx = content.find(b'[User]') mdict = {'nx_meta': {}} # assume all datasets coming from Quanta are Images, currently mdict['nx_meta']['DatasetType'] = 'Image' mdict['nx_meta']['Data Type'] = 'SEM_Imaging' # get the modification time (as ISO format): mtime = _os.path.getmtime(filename) mtime_iso = _dt.fromtimestamp(mtime).isoformat() instr = _get_instr(filename) # if we found the instrument, then store the name as string, else None instr_name = instr.name if instr is not None else None mdict['nx_meta']['Instrument ID'] = instr_name mdict['nx_meta']['Creation Time'] = mtime_iso mdict['nx_meta']['warnings'] = [] # if the user_idx is -1, it means the [User] tag was not found in the # file, and so the metadata is missing (so we should just return 0) if user_idx == -1: _logger.warning(f'Did not find expected FEI tags in .tif file: ' f'{filename}') mdict['nx_meta']['Data Type'] = '**REMOVED**' mdict['nx_meta']['Extractor Warnings'] = 'Did not find expected FEI ' \ 'tags. Could not read metadata' mdict['nx_meta'] = _sort_dict(mdict['nx_meta']) return mdict metadata_bytes = content[user_idx:] metadata_str = metadata_bytes.decode().replace('\r\n', '\n') buf = _io.StringIO(metadata_str) config = _cp.ConfigParser() # make ConfigParser respect upper/lowercase values config.optionxform = lambda option: option config.read_file(buf) for itm in config.items(): if itm[0] == 'DEFAULT': pass else: mdict[itm[0]] = {} for k, v in itm[1].items(): mdict[itm[0]][k] = v mdict = parse_nx_meta(mdict) # sort the nx_meta dictionary (recursively) for nicer display mdict['nx_meta'] = _sort_dict(mdict['nx_meta']) return mdict
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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